mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 17:00:26 +00:00
Datapack export
- /iris datapack export compiles one pack dimension into a worldgen datapack for Minecraft 26.3 and later - Bukkit and mod loaders take the same version, mode, namespace, output, format, objects and replace arguments - The default overworld mode replaces the Overworld of a new world with the default world type - Preset and dimension modes write a selectable world preset or a separate dimension - The export runs in the background and publishes the zip or folder atomically - Pack compiler stages turn noise, terrain, caves, subterrain, biomes, surface, decoration, objects and structures into a version-neutral IR - Capability lowering fits every field to 26.3 density function ranges - Disjoint min and max nodes fold to the input that always wins - The 26.3 writer emits density functions, noise settings, biomes, features, structures, presets and dimensions - Every export carries the Iris logo as its pack.png - Unused objects, templates and features are pruned from the output - The pruner keeps pack.png without reading references from its bytes - Imported pack datapacks are bundled into the export - Every approximation reports an export limit diagnostic - Generation cost is counted against a node budget and reported as a warning - Cave biomes over the wall palette limit use the closest kept palette - Jigsaw exports share the template encoder, block states, destinations and publisher with the worldgen export - DataVersion carries the pack format minor and world data version - Both platforms expose vanilla data from the server classpath - GoldenHashEngine and the export probes share one chunk spiral scan - Headless 26.3 probes measure export cost and compare column and chunk likeness against Iris - Real-codec tests decode the exported resources with the 26.3 registries - Paper registry tests run in isolated registry scopes - Command help and messages cover the export in every language file
This commit is contained in:
1 parent
b00e28e42e
commit
1ed1044474
385 files changed
+48096
-529
No files matched your search
+563
@@ -0,0 +1,563 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.Aquifer;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseRouter;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
public final class DatapackCostProbe {
|
||||||
|
private static final String LOG_PREFIX = "[datapack-cost]";
|
||||||
|
static final String VANILLA_PRESET = "minecraft:normal";
|
||||||
|
private static final int WARMUP_ORIGIN = -4096;
|
||||||
|
private static final int MINIMUM_CALLS = 8;
|
||||||
|
private static final int TOP_OFFENDERS = 10;
|
||||||
|
private static final long ANALYSIS_STACK_BYTES = 1L << 30;
|
||||||
|
private static final double MEGABYTE = 1024.0 * 1024.0;
|
||||||
|
private static final double NANOS_PER_SECOND = 1_000_000_000.0;
|
||||||
|
|
||||||
|
private DatapackCostProbe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] arguments) {
|
||||||
|
PrintStream diagnostics = System.err;
|
||||||
|
try {
|
||||||
|
execute(Configuration.parse(arguments));
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
failure.printStackTrace(diagnostics);
|
||||||
|
diagnostics.flush();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
System.out.flush();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void execute(Configuration configuration) throws Exception {
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
Files.createDirectories(configuration.work());
|
||||||
|
Path work = Files.createTempDirectory(configuration.work(), "run-");
|
||||||
|
List<HeadlessNativeRegistries> loaded = new ArrayList<>(2);
|
||||||
|
try {
|
||||||
|
log("exporting " + configuration.dimension() + " from " + configuration.pack());
|
||||||
|
ExportRun export = export(configuration, work);
|
||||||
|
log(String.format(Locale.ROOT, "exported in %.1f s: %d files, %d density functions", export.stats().seconds(),
|
||||||
|
export.stats().files(), export.stats().densityFunctions()));
|
||||||
|
DensityJsonBreakdown.Report densityJson = onLargeStack("density-json",
|
||||||
|
() -> DensityJsonBreakdown.analyze(export.files(), configuration.dimension()).report());
|
||||||
|
Path datapack = work.resolve("datapack");
|
||||||
|
writeFiles(export.files(), datapack);
|
||||||
|
Path vanillaPack = Files.createDirectories(work.resolve("vanilla"));
|
||||||
|
ExportStats exportStats = export.stats();
|
||||||
|
export.files().clear();
|
||||||
|
|
||||||
|
long baseline = settledHeap();
|
||||||
|
long loadStarted = System.nanoTime();
|
||||||
|
HeadlessNativeRegistries vanilla = isolatedLoad(vanillaPack);
|
||||||
|
double vanillaLoad = (System.nanoTime() - loadStarted) / NANOS_PER_SECOND;
|
||||||
|
loaded.add(vanilla);
|
||||||
|
long vanillaHeap = settledHeap();
|
||||||
|
loadStarted = System.nanoTime();
|
||||||
|
HeadlessNativeRegistries exported = isolatedLoad(datapack);
|
||||||
|
double exportLoad = (System.nanoTime() - loadStarted) / NANOS_PER_SECOND;
|
||||||
|
loaded.add(exported);
|
||||||
|
long exportHeap = settledHeap();
|
||||||
|
log(String.format(Locale.ROOT, "registries loaded: vanilla %.2f s, export %.2f s", vanillaLoad, exportLoad));
|
||||||
|
|
||||||
|
WorldgenStageBench vanillaBench = new WorldgenStageBench(new WorldgenStageBench.Options("vanilla",
|
||||||
|
presetOverworld(vanilla.registries(), VANILLA_PRESET), vanilla.registries(), configuration.seed()));
|
||||||
|
warm(vanillaBench, configuration);
|
||||||
|
long vanillaSamplers = settledHeap();
|
||||||
|
WorldgenStageBench exportBench = new WorldgenStageBench(new WorldgenStageBench.Options("export",
|
||||||
|
exportedOverworld(exported.registries()), exported.registries(), configuration.seed()));
|
||||||
|
warm(exportBench, configuration);
|
||||||
|
long exportSamplers = settledHeap();
|
||||||
|
|
||||||
|
double loadStart = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
|
||||||
|
Measurement vanillaMeasurement = new Measurement(vanillaBench);
|
||||||
|
Measurement exportMeasurement = new Measurement(exportBench);
|
||||||
|
measure(List.of(vanillaMeasurement, exportMeasurement), configuration);
|
||||||
|
double loadEnd = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
|
||||||
|
Map<String, GeneratorStats> generators = new LinkedHashMap<>();
|
||||||
|
generators.put("vanilla", vanillaMeasurement.stats(vanillaLoad, vanillaHeap - baseline, vanillaSamplers - exportHeap));
|
||||||
|
generators.put("export", exportMeasurement.stats(exportLoad, exportHeap - vanillaHeap, exportSamplers - vanillaSamplers));
|
||||||
|
|
||||||
|
log("analyzing density function graphs");
|
||||||
|
Map<String, List<DensityCostAnalysis.RootCost>> roots = new LinkedHashMap<>();
|
||||||
|
roots.put("vanilla", onLargeStack("density-cost-vanilla", () -> analyzeRoots(vanillaBench.settings(), configuration.dimension())));
|
||||||
|
roots.put("export", onLargeStack("density-cost-export", () -> analyzeRoots(exportBench.settings(), configuration.dimension())));
|
||||||
|
|
||||||
|
Report report = new Report(configuration.pack().toString(), configuration.dimension(), configuration.seed(),
|
||||||
|
configuration.rounds() * configuration.side() * configuration.side(), configuration.warmup() * configuration.warmup(),
|
||||||
|
configuration.rounds(), new MachineLoad(Runtime.getRuntime().availableProcessors(), loadStart, loadEnd),
|
||||||
|
"single thread for chunk stages and calls, vanilla and export interleaved per round; registry load uses HeadlessNativeRegistries' 4-thread loader; export uses Iris's own pools",
|
||||||
|
"feature decoration not measured: Paper's applyBiomeDecoration needs a live ServerLevel and the headless feature world lives in test fixtures",
|
||||||
|
exportStats, generators, densityJson, roots, definitions());
|
||||||
|
writeReport(report, configuration.output());
|
||||||
|
System.out.println(table(report));
|
||||||
|
log("report written to " + configuration.output());
|
||||||
|
} finally {
|
||||||
|
for (HeadlessNativeRegistries registries : loaded) {
|
||||||
|
registries.close();
|
||||||
|
}
|
||||||
|
deleteTree(work);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExportRun export(Configuration configuration, Path work) throws Exception {
|
||||||
|
Path exportRoot = Files.createDirectories(work.resolve("exports"));
|
||||||
|
StageClock clock = new StageClock();
|
||||||
|
WorldgenExportResult result;
|
||||||
|
long started;
|
||||||
|
long finished;
|
||||||
|
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||||
|
configuration.pack().toFile(), configuration.dimension(), LOG_PREFIX, new HeadlessNativeRuntime(), work))) {
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(workspace.pack().toPath())
|
||||||
|
.dimension(configuration.dimension())
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.objects(true)
|
||||||
|
.build();
|
||||||
|
started = System.nanoTime();
|
||||||
|
clock.start(started);
|
||||||
|
result = WorldgenDatapackExporter.create().export(request, clock);
|
||||||
|
finished = System.nanoTime();
|
||||||
|
}
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new IllegalStateException("Export failed: " + result.message() + " " + result.diagnostics());
|
||||||
|
}
|
||||||
|
SortedMap<String, byte[]> files = zipEntries(result.output());
|
||||||
|
ExportStats stats = new ExportStats((finished - started) / NANOS_PER_SECOND, clock.seconds(finished), Files.size(result.output()),
|
||||||
|
files.size(), count(files, "/worldgen/density_function/"), bytes(files, "/worldgen/density_function/"),
|
||||||
|
count(files, "/worldgen/noise/"), count(files, "/worldgen/biome/"), result.diagnostics().size());
|
||||||
|
Files.delete(result.output());
|
||||||
|
return new ExportRun(stats, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warm(WorldgenStageBench bench, Configuration configuration) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
bench.chunks(WARMUP_ORIGIN, WARMUP_ORIGIN, configuration.warmup(), new WorldgenStageBench.ChunkSamples());
|
||||||
|
WorldgenStageBench.CallBudget budget = new WorldgenStageBench.CallBudget(MINIMUM_CALLS * 4, MINIMUM_CALLS,
|
||||||
|
TimeUnit.SECONDS.toNanos(configuration.callSeconds()) / 4);
|
||||||
|
bench.uncachedBiome(budget, new WorldgenStageBench.CallSamples());
|
||||||
|
bench.baseHeight(budget, Heightmap.Types.WORLD_SURFACE_WG, new WorldgenStageBench.CallSamples());
|
||||||
|
bench.baseColumn(budget, new WorldgenStageBench.CallSamples());
|
||||||
|
log(String.format(Locale.ROOT, "%s warmed in %.1f s", bench.label(), (System.nanoTime() - started) / NANOS_PER_SECOND));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void measure(List<Measurement> measurements, Configuration configuration) {
|
||||||
|
int stride = configuration.side() + 4;
|
||||||
|
WorldgenStageBench.CallBudget budget = new WorldgenStageBench.CallBudget(
|
||||||
|
Math.max(1, configuration.calls() / configuration.rounds()), Math.max(1, MINIMUM_CALLS / configuration.rounds()),
|
||||||
|
TimeUnit.SECONDS.toNanos(configuration.callSeconds()) / configuration.rounds());
|
||||||
|
for (int round = 0; round < configuration.rounds(); round++) {
|
||||||
|
for (int index = 0; index < measurements.size(); index++) {
|
||||||
|
Measurement measurement = measurements.get((index + round) % measurements.size());
|
||||||
|
long started = System.nanoTime();
|
||||||
|
measurement.bench.chunks(round * stride, round * stride, configuration.side(), measurement.chunks);
|
||||||
|
measurement.bench.uncachedBiome(budget, measurement.uncachedBiome);
|
||||||
|
measurement.bench.baseHeight(budget, Heightmap.Types.WORLD_SURFACE_WG, measurement.surfaceHeight);
|
||||||
|
measurement.bench.baseHeight(budget, Heightmap.Types.OCEAN_FLOOR_WG, measurement.floorHeight);
|
||||||
|
measurement.bench.baseColumn(budget, measurement.column);
|
||||||
|
log(String.format(Locale.ROOT, "%s round %d measured in %.1f s", measurement.bench.label(), round,
|
||||||
|
(System.nanoTime() - started) / NANOS_PER_SECOND));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DensityCostAnalysis.RootCost> analyzeRoots(NoiseGeneratorSettings settings, String dimension) {
|
||||||
|
DensityCostAnalysis analysis = new DensityCostAnalysis(id -> DensityJsonBreakdown.category(dimension, id), TOP_OFFENDERS);
|
||||||
|
Map<String, DensityFunction> roots = new LinkedHashMap<>();
|
||||||
|
NoiseRouter router = settings.noiseRouter();
|
||||||
|
roots.put("temperature", router.temperature());
|
||||||
|
roots.put("vegetation", router.vegetation());
|
||||||
|
roots.put("continents", router.continents());
|
||||||
|
roots.put("erosion", router.erosion());
|
||||||
|
roots.put("depth", router.depth());
|
||||||
|
roots.put("ridges", router.ridges());
|
||||||
|
roots.put("chunk_surface_level", router.chunkSurfaceLevel());
|
||||||
|
roots.put("final_density", router.finalDensity());
|
||||||
|
if (settings.aquifers().isPresent()) {
|
||||||
|
Aquifer.Config aquifer = settings.aquifers().get();
|
||||||
|
roots.put("aquifer.barrier", aquifer.barrierNoise());
|
||||||
|
roots.put("aquifer.fluid_level_floodedness", aquifer.fluidLevelFloodednessNoise());
|
||||||
|
roots.put("aquifer.fluid_level_spread", aquifer.fluidLevelSpreadNoise());
|
||||||
|
roots.put("aquifer.lava", aquifer.lavaNoise());
|
||||||
|
roots.put("aquifer.exclusion", aquifer.exclusion());
|
||||||
|
roots.put("aquifer.surface_level", aquifer.surfaceLevel());
|
||||||
|
}
|
||||||
|
List<DensityCostAnalysis.RootCost> costs = new ArrayList<>(roots.size());
|
||||||
|
for (Map.Entry<String, DensityFunction> root : roots.entrySet()) {
|
||||||
|
costs.add(analysis.analyze(root.getKey(), root.getValue()));
|
||||||
|
}
|
||||||
|
return costs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static LevelStem presetOverworld(RegistryAccess registries, String preset) {
|
||||||
|
return registries.lookupOrThrow(Registries.WORLD_PRESET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, Identifier.parse(preset))).value().overworld()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("World preset " + preset + " has no overworld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static LevelStem exportedOverworld(RegistryAccess registries) {
|
||||||
|
return registries.lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(LevelStem.OVERWORLD).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries isolatedLoad(Path directory) {
|
||||||
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
|
return HeadlessNativeRegistries.load(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long settledHeap() throws InterruptedException {
|
||||||
|
for (int pass = 0; pass < 4; pass++) {
|
||||||
|
System.gc();
|
||||||
|
Thread.sleep(200L);
|
||||||
|
}
|
||||||
|
return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T onLargeStack(String name, Callable<T> work) throws Exception {
|
||||||
|
AtomicReference<T> result = new AtomicReference<>();
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
Thread thread = new Thread(null, () -> {
|
||||||
|
try {
|
||||||
|
result.set(work.call());
|
||||||
|
} catch (Throwable problem) {
|
||||||
|
failure.set(problem);
|
||||||
|
}
|
||||||
|
}, name, ANALYSIS_STACK_BYTES);
|
||||||
|
thread.start();
|
||||||
|
thread.join();
|
||||||
|
if (failure.get() instanceof Exception exception) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
if (failure.get() instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> zipEntries(Path zip) throws IOException {
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
try (ZipFile archive = new ZipFile(zip.toFile())) {
|
||||||
|
Enumeration<? extends ZipEntry> entries = archive.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
files.put(entry.getName(), archive.getInputStream(entry).readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeFiles(SortedMap<String, byte[]> files, Path directory) throws IOException {
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
Path target = directory.resolve(file.getKey()).normalize();
|
||||||
|
if (!target.startsWith(directory)) {
|
||||||
|
throw new IllegalArgumentException("Export file escapes the datapack root: " + file.getKey());
|
||||||
|
}
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.write(target, file.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int count(SortedMap<String, byte[]> files, String folder) {
|
||||||
|
int count = 0;
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
if (path.contains(folder) && path.endsWith(".json")) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long bytes(SortedMap<String, byte[]> files, String folder) {
|
||||||
|
long bytes = 0;
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().contains(folder) && file.getKey().endsWith(".json")) {
|
||||||
|
bytes += file.getValue().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeReport(Report report, Path output) throws IOException {
|
||||||
|
Files.createDirectories(output.getParent());
|
||||||
|
Gson gson = new GsonBuilder().setPrettyPrinting().serializeSpecialFloatingPointValues().create();
|
||||||
|
Files.writeString(output, gson.toJson(report), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> definitions() {
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
definitions.put("chunks", "per stage: wall ms and current-thread CPU ms per chunk; rounds of side x side chunks alternate vanilla and export");
|
||||||
|
definitions.put("chunks.biomes", "ChunkGenerator.createBiomes on a fresh ProtoChunk, ms per chunk over the grid plus a one-chunk ring");
|
||||||
|
definitions.put("chunks.noise", "NoiseChunk construction (aquifer init) plus NoiseBasedChunkGenerator.doFill, ms per chunk");
|
||||||
|
definitions.put("chunks.surface", "MaterialSystem.buildSurface with the fill's NoiseChunk and a region biome manager, ms per chunk");
|
||||||
|
definitions.put("chunks.carverBiome", "one uncached BiomeSource lookup at the chunk origin, as ChunkAccess.carverBiome memoizes per chunk, ms per chunk");
|
||||||
|
definitions.put("uncachedBiome", "BiomeSource.createUncachedResolver(...).getNoiseBiome at random quart positions within 32k blocks, wall us per call and mean thread CPU us per call");
|
||||||
|
definitions.put("baseHeight", "NoiseBasedChunkGenerator.getBaseHeight as structure placement calls it, us per call");
|
||||||
|
definitions.put("baseColumn", "NoiseBasedChunkGenerator.getBaseColumn, us per call");
|
||||||
|
definitions.put("retainedHeapBytes", "used heap after four System.gc passes, delta across the registry load");
|
||||||
|
definitions.put("samplerHeapBytes", "used heap delta across RandomState creation and warmup (compiled samplers and buffer pools)");
|
||||||
|
definitions.put("roots.dagNodes", "distinct non-constant density function and spline nodes reachable, references shared");
|
||||||
|
definitions.put("roots.cachedNodes", "non-constant evaluations per point with caches enabled: every distinct cache body once, all branches");
|
||||||
|
definitions.put("roots.expandedNodes", "non-constant evaluations per point with every reference and cache re-expanded, all branches");
|
||||||
|
definitions.put("roots.uncachedPointWorst", "worst single uncached point evaluation with lazy splines, interval_select and range_choice; interpolated off-lattice costs 8 corners at expanded cost; find_top_surface counts its probe steps");
|
||||||
|
definitions.put("roots.dagShare", "share of the distinct nodes by the category of the named file that holds them; offset copies are Grad2 and Taps duplicates");
|
||||||
|
definitions.put("roots.exclusive", "named density functions ranked by multiplicity x own nodes in the expanded tree");
|
||||||
|
definitions.put("roots.inclusive", "named density functions ranked by multiplicity x subtree nodes in the expanded tree");
|
||||||
|
definitions.put("densityJson.structurallyDuplicateBytes", "bytes beyond one file per JSON shape, with numbers, density function references and noise ids abstracted");
|
||||||
|
return definitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String table(Report report) {
|
||||||
|
StringBuilder out = new StringBuilder(4096);
|
||||||
|
GeneratorStats vanilla = report.generators().get("vanilla");
|
||||||
|
GeneratorStats export = report.generators().get("export");
|
||||||
|
out.append(String.format(Locale.ROOT, "%s %s seed %d: %d chunks in %d interleaved rounds after %d warmup chunks, one thread, load %.0f-%.0f on %d cores%n",
|
||||||
|
LOG_PREFIX, report.dimension(), report.seed(), report.chunks(), report.rounds(), report.warmupChunks(),
|
||||||
|
report.load().loadAverageStart(), report.load().loadAverageEnd(), report.load().processors()));
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10s %10s %8s %10s %10s %8s%n", "stage (mean)", "vanilla", "export", "ratio",
|
||||||
|
"cpu van", "cpu exp", "ratio"));
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10s %10.1f%n", "export (s)", "-", report.export().seconds()));
|
||||||
|
row(out, "registry load (s)", vanilla.registryLoadSeconds(), export.registryLoadSeconds());
|
||||||
|
row(out, "retained heap after load (MB)", vanilla.retainedHeapBytes() / MEGABYTE, export.retainedHeapBytes() / MEGABYTE);
|
||||||
|
row(out, "sampler heap after warmup (MB)", vanilla.samplerHeapBytes() / MEGABYTE, export.samplerHeapBytes() / MEGABYTE);
|
||||||
|
stage(out, "biome fill (ms/chunk)", vanilla.chunks().biomes(), export.chunks().biomes());
|
||||||
|
stage(out, "noise fill (ms/chunk)", vanilla.chunks().noise(), export.chunks().noise());
|
||||||
|
stage(out, "surface (ms/chunk)", vanilla.chunks().surface(), export.chunks().surface());
|
||||||
|
stage(out, "carver biome lookup (ms/chunk)", vanilla.chunks().carverBiome(), export.chunks().carverBiome());
|
||||||
|
row(out, "chunk total (ms/chunk)", vanilla.chunks().totalMean(), export.chunks().totalMean(),
|
||||||
|
vanilla.chunks().totalCpuMean(), export.chunks().totalCpuMean());
|
||||||
|
call(out, "uncached getNoiseBiome (us/call)", vanilla.uncachedBiome(), export.uncachedBiome());
|
||||||
|
call(out, "getBaseHeight WORLD_SURFACE (us)", vanilla.baseHeightSurface(), export.baseHeightSurface());
|
||||||
|
call(out, "getBaseHeight OCEAN_FLOOR (us)", vanilla.baseHeightFloor(), export.baseHeightFloor());
|
||||||
|
call(out, "getBaseColumn (us/call)", vanilla.baseColumn(), export.baseColumn());
|
||||||
|
out.append(String.format(Locale.ROOT, "%-24s %9s %9s %9s %10s %10s %10s %10s%n", "router root", "dag(v)", "dag",
|
||||||
|
"cached", "expand(v)", "expanded", "uncPt(v)", "uncachedPt"));
|
||||||
|
List<DensityCostAnalysis.RootCost> vanillaRoots = report.roots().get("vanilla");
|
||||||
|
List<DensityCostAnalysis.RootCost> exportRoots = report.roots().get("export");
|
||||||
|
for (DensityCostAnalysis.RootCost root : exportRoots) {
|
||||||
|
DensityCostAnalysis.RootCost reference = find(vanillaRoots, root.root());
|
||||||
|
out.append(String.format(Locale.ROOT, "%-24s %9s %9s %9s %10s %10s %10s %10s%n", root.root(),
|
||||||
|
reference == null ? "-" : compact(reference.dagNodes()), compact(root.dagNodes()), compact(root.cachedNodes()),
|
||||||
|
reference == null ? "-" : compact(reference.expandedNodes()), compact(root.expandedNodes()),
|
||||||
|
reference == null ? "-" : compact(reference.uncachedPointWorst()), compact(root.uncachedPointWorst())));
|
||||||
|
}
|
||||||
|
out.append("top expanded offenders (exclusive share of root, multiplicity)\n");
|
||||||
|
for (DensityCostAnalysis.RootCost root : exportRoots) {
|
||||||
|
if (root.exclusive().isEmpty() || root.expandedNodes() < 1.0e4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StringBuilder line = new StringBuilder(String.format(Locale.ROOT, " %-22s", root.root()));
|
||||||
|
for (int index = 0; index < Math.min(2, root.exclusive().size()); index++) {
|
||||||
|
DensityCostAnalysis.Offender offender = root.exclusive().get(index);
|
||||||
|
line.append(String.format(Locale.ROOT, " %4.1f%% %s x%s;", 100.0 * offender.contribution() / root.expandedNodes(),
|
||||||
|
shortName(offender.name()), compact(offender.multiplicity())));
|
||||||
|
}
|
||||||
|
out.append(line).append('\n');
|
||||||
|
}
|
||||||
|
for (DensityCostAnalysis.RootCost root : exportRoots) {
|
||||||
|
if (root.dagNodes() < 1.0e4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Map.Entry<String, Double>> shares = new ArrayList<>(root.dagShare().entrySet());
|
||||||
|
shares.sort(Map.Entry.<String, Double>comparingByValue().reversed());
|
||||||
|
StringBuilder line = new StringBuilder(String.format(Locale.ROOT, " dag %-18s", root.root()));
|
||||||
|
for (int index = 0; index < Math.min(3, shares.size()); index++) {
|
||||||
|
line.append(String.format(Locale.ROOT, " %4.1f%% %s;", 100.0 * shares.get(index).getValue(), shares.get(index).getKey()));
|
||||||
|
}
|
||||||
|
out.append(line).append('\n');
|
||||||
|
}
|
||||||
|
DensityJsonBreakdown.Report json = report.densityJson();
|
||||||
|
out.append(String.format(Locale.ROOT, "density function JSON %.0f MB in %d files, %d cellular instances in %d shapes%n",
|
||||||
|
json.bytes() / MEGABYTE, json.files(), json.cellularInstances(), json.cellularShapes().size()));
|
||||||
|
for (int index = 0; index < Math.min(9, json.categories().size()); index++) {
|
||||||
|
DensityJsonBreakdown.CategoryBytes category = json.categories().get(index);
|
||||||
|
out.append(String.format(Locale.ROOT, " %-22s %8.1f MB %7d files %7d shapes %8.1f MB dup%n", category.category(),
|
||||||
|
category.bytes() / MEGABYTE, category.files(), category.distinctShapes(), category.structurallyDuplicateBytes() / MEGABYTE));
|
||||||
|
}
|
||||||
|
for (int index = 0; index < Math.min(4, json.libraryOwners().size()); index++) {
|
||||||
|
DensityJsonBreakdown.Bytes owner = json.libraryOwners().get(index);
|
||||||
|
out.append(String.format(Locale.ROOT, " %-48s %8.1f MB%n", owner.name(), owner.bytes() / MEGABYTE));
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void row(StringBuilder out, String label, double vanilla, double export) {
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10.3f %10.3f %8s%n", label, vanilla, export, ratio(vanilla, export)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void row(StringBuilder out, String label, double vanilla, double export, double vanillaCpu, double exportCpu) {
|
||||||
|
out.append(String.format(Locale.ROOT, "%-34s %10.3f %10.3f %8s %10.3f %10.3f %8s%n", label, vanilla, export, ratio(vanilla, export),
|
||||||
|
vanillaCpu, exportCpu, ratio(vanillaCpu, exportCpu)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stage(StringBuilder out, String label, WorldgenStageBench.StageTiming vanilla, WorldgenStageBench.StageTiming export) {
|
||||||
|
row(out, label, vanilla.wall().mean(), export.wall().mean(), vanilla.cpu().mean(), export.cpu().mean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void call(StringBuilder out, String label, WorldgenStageBench.CallTiming vanilla, WorldgenStageBench.CallTiming export) {
|
||||||
|
row(out, label, vanilla.wall().mean(), export.wall().mean(), vanilla.cpuMean(), export.cpuMean());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ratio(double vanilla, double export) {
|
||||||
|
return vanilla > 0.0 ? String.format(Locale.ROOT, "%.1fx", export / vanilla) : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityCostAnalysis.RootCost find(List<DensityCostAnalysis.RootCost> roots, String name) {
|
||||||
|
for (DensityCostAnalysis.RootCost root : roots) {
|
||||||
|
if (root.root().equals(name)) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortName(String name) {
|
||||||
|
int separator = name.indexOf(':');
|
||||||
|
String path = separator < 0 ? name : name.substring(separator + 1);
|
||||||
|
return path.length() <= 34 ? path : "..." + path.substring(path.length() - 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String compact(double value) {
|
||||||
|
if (value < 1.0e4) {
|
||||||
|
return String.format(Locale.ROOT, "%.0f", value);
|
||||||
|
}
|
||||||
|
return String.format(Locale.ROOT, "%.2e", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void log(String message) {
|
||||||
|
System.out.println(LOG_PREFIX + " " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteTree(Path root) throws IOException {
|
||||||
|
if (!Files.exists(root)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HeadlessNativeRegistries.delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Configuration(Path pack, String dimension, long seed, int side, int rounds, int warmup, int calls, long callSeconds,
|
||||||
|
Path output, Path work) {
|
||||||
|
Configuration {
|
||||||
|
if (side < 1 || rounds < 1 || warmup < 1 || calls < MINIMUM_CALLS || callSeconds < 1) {
|
||||||
|
throw new IllegalArgumentException("side, rounds, warmup and callSeconds must be positive and calls at least " + MINIMUM_CALLS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Configuration parse(String[] arguments) {
|
||||||
|
if (arguments.length != 10) {
|
||||||
|
throw new IllegalArgumentException("Usage: pack dimension seed side rounds warmupSide calls callSeconds output work");
|
||||||
|
}
|
||||||
|
return new Configuration(new File(arguments[0]).toPath().toAbsolutePath(), arguments[1], Long.parseLong(arguments[2]),
|
||||||
|
Integer.parseInt(arguments[3]), Integer.parseInt(arguments[4]), Integer.parseInt(arguments[5]),
|
||||||
|
Integer.parseInt(arguments[6]), Long.parseLong(arguments[7]), new File(arguments[8]).toPath().toAbsolutePath(),
|
||||||
|
new File(arguments[9]).toPath().toAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ExportStats(double seconds, Map<String, Double> stageSeconds, long zipBytes, int files, int densityFunctions,
|
||||||
|
long densityFunctionBytes, int noises, int biomes, int diagnostics) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record GeneratorStats(double registryLoadSeconds, long retainedHeapBytes, long samplerHeapBytes, WorldgenStageBench.ChunkTimings chunks,
|
||||||
|
WorldgenStageBench.CallTiming uncachedBiome, WorldgenStageBench.CallTiming baseHeightSurface,
|
||||||
|
WorldgenStageBench.CallTiming baseHeightFloor, WorldgenStageBench.CallTiming baseColumn) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MachineLoad(int processors, double loadAverageStart, double loadAverageEnd) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Report(String pack, String dimension, long seed, int chunks, int warmupChunks, int rounds, MachineLoad load,
|
||||||
|
String threading, String decoration,
|
||||||
|
ExportStats export, Map<String, GeneratorStats> generators, DensityJsonBreakdown.Report densityJson,
|
||||||
|
Map<String, List<DensityCostAnalysis.RootCost>> roots, Map<String, String> definitions) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExportRun(ExportStats stats, SortedMap<String, byte[]> files) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Measurement {
|
||||||
|
private final WorldgenStageBench bench;
|
||||||
|
private final WorldgenStageBench.ChunkSamples chunks = new WorldgenStageBench.ChunkSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples uncachedBiome = new WorldgenStageBench.CallSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples surfaceHeight = new WorldgenStageBench.CallSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples floorHeight = new WorldgenStageBench.CallSamples();
|
||||||
|
private final WorldgenStageBench.CallSamples column = new WorldgenStageBench.CallSamples();
|
||||||
|
|
||||||
|
private Measurement(WorldgenStageBench bench) {
|
||||||
|
this.bench = bench;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GeneratorStats stats(double loadSeconds, long retainedHeap, long samplerHeap) {
|
||||||
|
return new GeneratorStats(loadSeconds, retainedHeap, samplerHeap, chunks.timings(), uncachedBiome.timing(),
|
||||||
|
surfaceHeight.timing(), floorHeight.timing(), column.timing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StageClock implements WorldgenExportProgress {
|
||||||
|
private final Map<String, Long> starts = new LinkedHashMap<>();
|
||||||
|
private long origin;
|
||||||
|
|
||||||
|
private void start(long nanos) {
|
||||||
|
origin = nanos;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
starts.put(stage, System.nanoTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Double> seconds(long finished) {
|
||||||
|
Map<String, Double> seconds = new LinkedHashMap<>();
|
||||||
|
String previous = "setup";
|
||||||
|
long previousStart = origin;
|
||||||
|
for (Map.Entry<String, Long> entry : starts.entrySet()) {
|
||||||
|
seconds.put(previous, (entry.getValue() - previousStart) / NANOS_PER_SECOND);
|
||||||
|
previous = entry.getKey();
|
||||||
|
previousStart = entry.getValue();
|
||||||
|
}
|
||||||
|
seconds.put(previous, (finished - previousStart) / NANOS_PER_SECOND);
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+331
@@ -0,0 +1,331 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.surface.MaterialProgramCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.probe.fidelity.BiomeShareReport;
|
||||||
|
import art.arcane.iris.probe.fidelity.BiomeTypes;
|
||||||
|
import art.arcane.iris.probe.fidelity.ColumnSource;
|
||||||
|
import art.arcane.iris.probe.fidelity.ColumnSurvey;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityComparison;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityReport;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelitySampling;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelitySamples;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityScanner;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelitySource;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityStats;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityThresholds;
|
||||||
|
import art.arcane.iris.probe.fidelity.IrisEngineColumnSource;
|
||||||
|
import art.arcane.iris.probe.fidelity.IrisEngineFidelitySource;
|
||||||
|
import art.arcane.iris.probe.fidelity.SurveyWindow;
|
||||||
|
import art.arcane.iris.probe.fidelity.VanillaNoiseColumnSource;
|
||||||
|
import art.arcane.iris.probe.fidelity.VanillaNoiseFidelitySource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public final class DatapackFidelityProbe {
|
||||||
|
private static final String LOG_PREFIX = "[datapack-fidelity]";
|
||||||
|
private static final String USAGE = "Usage: datapackFidelityProbe pack dimension seed radiusChunks stride output surveyBlocks surveyStride surveySeeds mode";
|
||||||
|
private static final String TARGET_VERSION = "26.3";
|
||||||
|
private static final String EXPORT_FORMAT = "directory";
|
||||||
|
private static final String EXPORT_OUTPUT = "export";
|
||||||
|
private static final int MAX_STRIDE = 16;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final int ARGUMENTS = 10;
|
||||||
|
private static final String COLUMN_SCOPE = "columns";
|
||||||
|
private static final String CHUNK_SCOPE = "chunks";
|
||||||
|
private static final int EXIT_ERROR = 1;
|
||||||
|
private static final int EXIT_FIDELITY_FAILED = 3;
|
||||||
|
|
||||||
|
private DatapackFidelityProbe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] arguments) {
|
||||||
|
int status;
|
||||||
|
try {
|
||||||
|
status = execute(arguments, WorldgenDatapackExporter.create()).passed() ? 0 : EXIT_FIDELITY_FAILED;
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
failure.printStackTrace(System.err);
|
||||||
|
status = EXIT_ERROR;
|
||||||
|
}
|
||||||
|
System.out.flush();
|
||||||
|
System.err.flush();
|
||||||
|
System.exit(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
static FidelityReport execute(String[] arguments, WorldgenExportService exporter) throws Exception {
|
||||||
|
Objects.requireNonNull(exporter, "exporter");
|
||||||
|
Configuration configuration = Configuration.parse(arguments);
|
||||||
|
System.out.println(LOG_PREFIX + " pack=" + configuration.pack() + " dimension=" + configuration.dimension()
|
||||||
|
+ " seed=" + configuration.seed() + " radiusChunks=" + configuration.radiusChunks() + " stride=" + configuration.stride()
|
||||||
|
+ " surveyBlocks=" + configuration.survey().size() + " surveyStride=" + configuration.survey().stride()
|
||||||
|
+ " surveySeeds=" + configuration.surveySeeds() + " mode=" + configuration.mode().label());
|
||||||
|
Outcome outcome = compare(configuration, exporter);
|
||||||
|
writeReport(configuration.output(), outcome.json());
|
||||||
|
System.out.print(outcome.table());
|
||||||
|
System.out.println(LOG_PREFIX + " report=" + configuration.output());
|
||||||
|
return outcome.report();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome compare(Configuration configuration, WorldgenExportService exporter) throws Exception {
|
||||||
|
Path scratch = Files.createTempDirectory("iris-datapack-fidelity-");
|
||||||
|
try {
|
||||||
|
return compareIn(scratch, configuration, exporter);
|
||||||
|
} finally {
|
||||||
|
ProbeWorkspaceCleanup.deleteRecursively(scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome compareIn(Path scratch, Configuration configuration, WorldgenExportService exporter) throws Exception {
|
||||||
|
HeadlessNativeRuntime runtime = new HeadlessNativeRuntime();
|
||||||
|
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||||
|
configuration.pack().toFile(), configuration.dimension(), LOG_PREFIX, runtime, scratch))) {
|
||||||
|
WorldgenExportResult export = export(exporter, workspace.pack().toPath(), configuration.dimension(), scratch.resolve("exports"));
|
||||||
|
Target target = new Target(export.output(), WorldgenExportRequest.DEFAULT_NAMESPACE,
|
||||||
|
VanillaResourceIdentifier.sanitizePath(configuration.dimension()), export.loadKeyByBiomeId());
|
||||||
|
return switch (configuration.mode()) {
|
||||||
|
case FULL -> Outcome.of(compareAll(workspace, target, configuration));
|
||||||
|
case BIOMES -> Outcome.of(compareBiomes(workspace, target, configuration));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelityReport compareAll(RealPackProbeSupport.Workspace workspace, Target target, Configuration configuration) throws Exception {
|
||||||
|
List<Long> seeds = configuration.surveySeeds();
|
||||||
|
FidelityStats irisColumns = null;
|
||||||
|
FidelityStats exportColumns = null;
|
||||||
|
FidelitySamples chunks = null;
|
||||||
|
for (long seed : seeds) {
|
||||||
|
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "fidelity-" + seed)) {
|
||||||
|
Surveyed surveyed = survey(session.engine(), target, seed, configuration.survey());
|
||||||
|
irisColumns = irisColumns == null ? surveyed.iris() : irisColumns.merge(surveyed.iris());
|
||||||
|
exportColumns = exportColumns == null ? surveyed.export() : exportColumns.merge(surveyed.export());
|
||||||
|
if (seed == configuration.seed()) {
|
||||||
|
chunks = scanChunks(session.engine(), target, configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FidelitySampling columnSampling = new FidelitySampling(COLUMN_SCOPE, configuration.survey().size(), configuration.survey().stride(),
|
||||||
|
seeds, irisColumns.columns());
|
||||||
|
return FidelityComparison.compare(new FidelitySamples(irisColumns, exportColumns, columnSampling), chunks, FidelityThresholds.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeShareReport compareBiomes(RealPackProbeSupport.Workspace workspace, Target target, Configuration configuration) throws Exception {
|
||||||
|
List<FidelitySamples> seeds = new ArrayList<>(configuration.seedCount());
|
||||||
|
Map<String, String> biomeTypes = Map.of();
|
||||||
|
for (long seed : configuration.surveySeeds()) {
|
||||||
|
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "biomes-" + seed)) {
|
||||||
|
Engine engine = session.engine();
|
||||||
|
Surveyed surveyed = survey(engine, target, seed, configuration.survey());
|
||||||
|
if (biomeTypes.isEmpty()) {
|
||||||
|
biomeTypes = BiomeTypes.of(engine.getDimension().getAllRegions(engine), key -> engine.getData().getBiomeLoader().load(key));
|
||||||
|
}
|
||||||
|
seeds.add(new FidelitySamples(surveyed.iris(), surveyed.export(), new FidelitySampling(COLUMN_SCOPE, configuration.survey().size(),
|
||||||
|
configuration.survey().stride(), List.of(seed), surveyed.iris().columns())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FidelityComparison.compareBiomeShares(seeds, biomeTypes, FidelityThresholds.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Surveyed survey(Engine engine, Target target, long seed, SurveyWindow window) throws Exception {
|
||||||
|
try (ColumnSource iris = new IrisEngineColumnSource(engine);
|
||||||
|
ColumnSource vanilla = new VanillaNoiseColumnSource(target.sampler(seed), target.loadKeyByBiomeId())) {
|
||||||
|
requireSameHeightRange(iris.minY(), iris.maxY(), vanilla.minY(), vanilla.maxY());
|
||||||
|
return new Surveyed(surveyed(iris, window, seed), surveyed(vanilla, window, seed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelityStats surveyed(ColumnSource source, SurveyWindow window, long seed) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
FidelityStats stats = ColumnSurvey.scan(source, window, MultiBurst.burst.parallelism());
|
||||||
|
System.out.println(LOG_PREFIX + " surveyed " + source.label() + " seed=" + seed + " columns=" + stats.columns()
|
||||||
|
+ " in " + (System.nanoTime() - started) / 1_000_000L + "ms");
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelitySamples scanChunks(Engine engine, Target target, Configuration configuration) throws Exception {
|
||||||
|
try (IrisEngineFidelitySource iris = new IrisEngineFidelitySource(engine);
|
||||||
|
VanillaNoiseFidelitySource vanilla = new VanillaNoiseFidelitySource(target.sampler(configuration.seed()), target.loadKeyByBiomeId(),
|
||||||
|
target.namespace() + ":" + MaterialProgramCompiler.WALL_SHELL)) {
|
||||||
|
requireSameHeightRange(iris.minY(), iris.maxY(), vanilla.minY(), vanilla.maxY());
|
||||||
|
FidelityStats irisStats = scan(iris, configuration);
|
||||||
|
FidelityStats exportStats = scan(vanilla, configuration);
|
||||||
|
FidelitySampling sampling = new FidelitySampling(CHUNK_SCOPE, (2 * configuration.radiusChunks() + 1) * CHUNK_WIDTH,
|
||||||
|
configuration.stride(), List.of(configuration.seed()), irisStats.columns());
|
||||||
|
return new FidelitySamples(irisStats, exportStats, sampling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportResult export(WorldgenExportService exporter, Path pack, String dimension, Path exportRoot) throws IOException {
|
||||||
|
Files.createDirectories(exportRoot);
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack)
|
||||||
|
.dimension(dimension)
|
||||||
|
.version(TARGET_VERSION)
|
||||||
|
.format(EXPORT_FORMAT)
|
||||||
|
.output(EXPORT_OUTPUT)
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.build();
|
||||||
|
WorldgenExportResult result = exporter.export(request, WorldgenExportProgress.NONE);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new IllegalStateException("Datapack export of dimension '" + dimension + "' was " + result.status() + ": " + result.message());
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " exported target=" + TARGET_VERSION + " biomes=" + result.loadKeyByBiomeId().size()
|
||||||
|
+ " diagnostics=" + result.diagnostics().size() + " " + diagnosticCounts(result.diagnostics()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FidelityStats scan(FidelitySource source, Configuration configuration) {
|
||||||
|
long started = System.nanoTime();
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, configuration.radiusChunks(), configuration.stride(),
|
||||||
|
MultiBurst.burst.parallelism());
|
||||||
|
System.out.println(LOG_PREFIX + " sampled " + source.label() + " columns=" + stats.columns() + " terrainColumns=" + stats.heightColumns()
|
||||||
|
+ " in " + (System.nanoTime() - started) / 1_000_000L + "ms");
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireSameHeightRange(int irisMinY, int irisMaxY, int exportMinY, int exportMaxY) {
|
||||||
|
if (irisMinY != exportMinY || irisMaxY != exportMaxY) {
|
||||||
|
throw new IllegalStateException("Iris height range [" + irisMinY + ", " + irisMaxY
|
||||||
|
+ ") differs from the exported dimension range [" + exportMinY + ", " + exportMaxY + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, Integer> diagnosticCounts(List<ExportDiagnostic> diagnostics) {
|
||||||
|
SortedMap<String, Integer> counts = new TreeMap<>();
|
||||||
|
for (ExportDiagnostic diagnostic : diagnostics) {
|
||||||
|
counts.merge(diagnostic.limit().name(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeReport(Path output, String json) throws IOException {
|
||||||
|
Path parent = output.getParent();
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
Path staged = Files.createTempFile(parent, ".datapack-fidelity-", ".json");
|
||||||
|
try {
|
||||||
|
Files.writeString(staged, json + "\n", StandardCharsets.UTF_8);
|
||||||
|
Files.move(staged, output, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(staged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Mode {
|
||||||
|
FULL,
|
||||||
|
BIOMES;
|
||||||
|
|
||||||
|
private static Mode parse(String value) {
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
for (Mode mode : values()) {
|
||||||
|
if (mode.label().equals(normalized)) {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("mode must be full or biomes, got '" + value + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String label() {
|
||||||
|
return name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Outcome(FidelityReport report, String json, String table) {
|
||||||
|
private static Outcome of(FidelityReport report) {
|
||||||
|
return new Outcome(report, report.toJson(), report.toTable());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome of(BiomeShareReport report) {
|
||||||
|
return new Outcome(report.mean(), report.toJson(), report.toTable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Surveyed(FidelityStats iris, FidelityStats export) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Target(Path output, String namespace, String dimension, Map<String, String> loadKeyByBiomeId) {
|
||||||
|
private WorldgenExportSampler sampler(long seed) throws IOException {
|
||||||
|
return WorldgenExportSampler.open(output, WorldgenExportMode.OVERWORLD, namespace + ":" + dimension, seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Configuration(Path pack, String dimension, long seed, int radiusChunks, int stride, Path output, SurveyWindow survey,
|
||||||
|
int seedCount, Mode mode) {
|
||||||
|
private static Configuration parse(String[] arguments) {
|
||||||
|
if (arguments.length != ARGUMENTS) {
|
||||||
|
throw new IllegalArgumentException(USAGE);
|
||||||
|
}
|
||||||
|
Path pack = Path.of(arguments[0]).toAbsolutePath().normalize();
|
||||||
|
if (!Files.isDirectory(pack)) {
|
||||||
|
throw new IllegalArgumentException("Pack folder " + pack + " does not exist");
|
||||||
|
}
|
||||||
|
String dimension = arguments[1].trim();
|
||||||
|
if (dimension.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Dimension key must not be blank");
|
||||||
|
}
|
||||||
|
long seed = parseLong(arguments[2], "seed");
|
||||||
|
int radiusChunks = parseInt(arguments[3], "radiusChunks");
|
||||||
|
if (radiusChunks < 0) {
|
||||||
|
throw new IllegalArgumentException("radiusChunks must be zero or positive, got " + radiusChunks);
|
||||||
|
}
|
||||||
|
int stride = parseInt(arguments[4], "stride");
|
||||||
|
if (stride < 1 || stride > MAX_STRIDE) {
|
||||||
|
throw new IllegalArgumentException("stride must be within [1, " + MAX_STRIDE + "], got " + stride);
|
||||||
|
}
|
||||||
|
Path output = Path.of(arguments[5]).toAbsolutePath().normalize();
|
||||||
|
if (Files.isDirectory(output)) {
|
||||||
|
throw new IllegalArgumentException("Report output " + output + " is a directory");
|
||||||
|
}
|
||||||
|
SurveyWindow survey = new SurveyWindow(0, 0, parseInt(arguments[6], "surveyBlocks"), parseInt(arguments[7], "surveyStride"));
|
||||||
|
int seedCount = parseInt(arguments[8], "surveySeeds");
|
||||||
|
if (seedCount < 1) {
|
||||||
|
throw new IllegalArgumentException("surveySeeds must be at least 1, got " + seedCount);
|
||||||
|
}
|
||||||
|
Mode mode = Mode.parse(arguments[9]);
|
||||||
|
return new Configuration(pack, dimension, seed, radiusChunks, stride, output, survey, seedCount, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> surveySeeds() {
|
||||||
|
List<Long> seeds = new ArrayList<>(seedCount);
|
||||||
|
for (int index = 0; index < seedCount; index++) {
|
||||||
|
seeds.add(seed + index);
|
||||||
|
}
|
||||||
|
return seeds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long parseLong(String value, String name) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(value.trim());
|
||||||
|
} catch (NumberFormatException failure) {
|
||||||
|
throw new IllegalArgumentException(name + " must be an integer, got '" + value + "'", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseInt(String value, String name) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value.trim());
|
||||||
|
} catch (NumberFormatException failure) {
|
||||||
|
throw new IllegalArgumentException(name + " must be an integer, got '" + value + "'", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+349
@@ -0,0 +1,349 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.util.CubicSpline;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunctions;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.generator.ConstantFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.CacheFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.FindTopSurfaceFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.InterpolatedFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.IntervalSelectFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.RangeChoiceFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.SplineFunction;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
final class DensityCostAnalysis {
|
||||||
|
private static final double LATTICE_CORNERS = 8.0;
|
||||||
|
private static final int MAX_SURFACE_STEPS = 4096;
|
||||||
|
private static final String INLINE = "<inline>";
|
||||||
|
|
||||||
|
private final Function<String, String> categories;
|
||||||
|
private final int topCount;
|
||||||
|
private final Map<Object, Costs> costs = new IdentityHashMap<>();
|
||||||
|
private final Map<DensityFunction, Body> bodies = new IdentityHashMap<>();
|
||||||
|
private final Map<DensityFunction, String> names = new IdentityHashMap<>();
|
||||||
|
|
||||||
|
DensityCostAnalysis(Function<String, String> categories, int topCount) {
|
||||||
|
this.categories = categories;
|
||||||
|
this.topCount = topCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootCost analyze(String root, DensityFunction function) {
|
||||||
|
Costs total = cost(function);
|
||||||
|
Reach reach = new Reach();
|
||||||
|
reach(function, reach, categories.apply(rootName(function, root)));
|
||||||
|
double cached = total.cachedLocal();
|
||||||
|
for (CacheFunction cache : reach.caches.keySet()) {
|
||||||
|
cached += cost(cache.input()).cachedLocal();
|
||||||
|
}
|
||||||
|
Body rootBody = new Body();
|
||||||
|
local(function, rootBody, true);
|
||||||
|
Map<DensityFunction, Double> multiplicity = multiplicity(rootBody);
|
||||||
|
List<Offender> offenders = new ArrayList<>(multiplicity.size());
|
||||||
|
Map<String, Double> shares = new TreeMap<>();
|
||||||
|
double expanded = total.expanded();
|
||||||
|
shares.merge(categories.apply(rootName(function, root)), rootBody.self / expanded, Double::sum);
|
||||||
|
for (Map.Entry<DensityFunction, Double> entry : multiplicity.entrySet()) {
|
||||||
|
DensityFunction value = entry.getKey();
|
||||||
|
Body body = body(value);
|
||||||
|
String name = names.get(value);
|
||||||
|
String category = categories.apply(name);
|
||||||
|
double contribution = entry.getValue() * body.self;
|
||||||
|
shares.merge(category, contribution / expanded, Double::sum);
|
||||||
|
offenders.add(new Offender(name, category, entry.getValue(), body.self, cost(value).expanded(),
|
||||||
|
contribution, entry.getValue() * cost(value).expanded()));
|
||||||
|
}
|
||||||
|
List<Offender> exclusive = top(offenders, Comparator.comparingDouble(Offender::contribution).reversed());
|
||||||
|
List<Offender> inclusive = top(offenders, Comparator.comparingDouble(Offender::inclusive).reversed());
|
||||||
|
Map<String, Double> dagShares = new TreeMap<>();
|
||||||
|
for (Map.Entry<String, Double> entry : reach.categories.entrySet()) {
|
||||||
|
dagShares.put(entry.getKey(), entry.getValue() / Math.max(1L, reach.nodes));
|
||||||
|
}
|
||||||
|
return new RootCost(root, reach.nodes, cached, expanded, total.lazy(), multiplicity.size(), exclusive, inclusive, shares, dagShares);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Offender> top(List<Offender> offenders, Comparator<Offender> order) {
|
||||||
|
List<Offender> sorted = new ArrayList<>(offenders);
|
||||||
|
sorted.sort(order);
|
||||||
|
return List.copyOf(sorted.subList(0, Math.min(topCount, sorted.size())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rootName(DensityFunction function, String root) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
return name(holder);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Costs cost(DensityFunction function) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
DensityFunction value = holder.value();
|
||||||
|
names.putIfAbsent(value, name(holder));
|
||||||
|
return cost(value);
|
||||||
|
}
|
||||||
|
Costs known = costs.get(function);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
Costs computed = compute(function);
|
||||||
|
costs.put(function, computed);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Costs compute(DensityFunction function) {
|
||||||
|
return switch (function) {
|
||||||
|
case ConstantFunction ignored -> new Costs(0.0, 0.0, 0.0);
|
||||||
|
case CacheFunction cache -> {
|
||||||
|
Costs input = cost(cache.input());
|
||||||
|
yield new Costs(1.0 + input.expanded(), 1.0 + input.lazy(), 1.0);
|
||||||
|
}
|
||||||
|
case SplineFunction spline -> {
|
||||||
|
Costs inner = spline(spline.spline());
|
||||||
|
yield new Costs(1.0 + inner.expanded(), 1.0 + inner.lazy(), 1.0 + inner.cachedLocal());
|
||||||
|
}
|
||||||
|
case IntervalSelectFunction select -> {
|
||||||
|
Costs input = cost(select.input());
|
||||||
|
double expanded = 1.0 + input.expanded();
|
||||||
|
double cached = 1.0 + input.cachedLocal();
|
||||||
|
double lazy = 0.0;
|
||||||
|
for (DensityFunction branch : select.functions()) {
|
||||||
|
Costs branchCost = cost(branch);
|
||||||
|
expanded += branchCost.expanded();
|
||||||
|
cached += branchCost.cachedLocal();
|
||||||
|
lazy = Math.max(lazy, branchCost.lazy());
|
||||||
|
}
|
||||||
|
yield new Costs(expanded, 1.0 + input.lazy() + lazy, cached);
|
||||||
|
}
|
||||||
|
case RangeChoiceFunction choice -> {
|
||||||
|
Costs input = cost(choice.input());
|
||||||
|
Costs inside = cost(choice.whenInRange());
|
||||||
|
Costs outside = cost(choice.whenOutOfRange());
|
||||||
|
yield new Costs(1.0 + input.expanded() + inside.expanded() + outside.expanded(),
|
||||||
|
1.0 + input.lazy() + Math.max(inside.lazy(), outside.lazy()),
|
||||||
|
1.0 + input.cachedLocal() + inside.cachedLocal() + outside.cachedLocal());
|
||||||
|
}
|
||||||
|
case InterpolatedFunction grid -> {
|
||||||
|
Costs input = cost(grid.input());
|
||||||
|
yield new Costs(1.0 + input.expanded(), 1.0 + LATTICE_CORNERS * input.expanded(), 1.0 + input.cachedLocal());
|
||||||
|
}
|
||||||
|
case FindTopSurfaceFunction surface -> {
|
||||||
|
Costs density = cost(surface.density());
|
||||||
|
Costs upper = cost(surface.upperBound());
|
||||||
|
double steps = surfaceSteps(surface);
|
||||||
|
yield new Costs(1.0 + density.expanded() + upper.expanded(), 1.0 + upper.lazy() + steps * density.lazy(),
|
||||||
|
1.0 + density.cachedLocal() + upper.cachedLocal());
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
double expanded = 1.0;
|
||||||
|
double lazy = 1.0;
|
||||||
|
double cached = 1.0;
|
||||||
|
for (DensityFunction child : children(function)) {
|
||||||
|
Costs childCost = cost(child);
|
||||||
|
expanded += childCost.expanded();
|
||||||
|
lazy += childCost.lazy();
|
||||||
|
cached += childCost.cachedLocal();
|
||||||
|
}
|
||||||
|
yield new Costs(expanded, lazy, cached);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Costs spline(CubicSpline<SplineFunction.Coordinate> spline) {
|
||||||
|
if (!(spline instanceof CubicSpline.Multipoint<SplineFunction.Coordinate> multipoint)) {
|
||||||
|
return new Costs(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
Costs known = costs.get(multipoint);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
Costs coordinate = cost(multipoint.coordinate().function());
|
||||||
|
List<CubicSpline<SplineFunction.Coordinate>> values = multipoint.values();
|
||||||
|
double expanded = 1.0 + coordinate.expanded();
|
||||||
|
double cached = 1.0 + coordinate.cachedLocal();
|
||||||
|
double[] lazy = new double[values.size()];
|
||||||
|
for (int index = 0; index < values.size(); index++) {
|
||||||
|
Costs value = spline(values.get(index));
|
||||||
|
expanded += value.expanded();
|
||||||
|
cached += value.cachedLocal();
|
||||||
|
lazy[index] = value.lazy();
|
||||||
|
}
|
||||||
|
double worstPair = lazy.length == 1 ? lazy[0] : 0.0;
|
||||||
|
for (int index = 0; index + 1 < lazy.length; index++) {
|
||||||
|
worstPair = Math.max(worstPair, lazy[index] + lazy[index + 1]);
|
||||||
|
}
|
||||||
|
Costs computed = new Costs(expanded, 1.0 + coordinate.lazy() + worstPair, cached);
|
||||||
|
costs.put(multipoint, computed);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reach(DensityFunction function, Reach reach, String category) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
reach(holder.value(), reach, categories.apply(name(holder)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (function instanceof ConstantFunction || reach.visited.put(function, Boolean.TRUE) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reach.count(category);
|
||||||
|
if (function instanceof CacheFunction cache) {
|
||||||
|
reach.caches.put(cache, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
if (function instanceof SplineFunction spline) {
|
||||||
|
reachSpline(spline.spline(), reach, category);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (DensityFunction child : children(function)) {
|
||||||
|
reach(child, reach, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reachSpline(CubicSpline<SplineFunction.Coordinate> spline, Reach reach, String category) {
|
||||||
|
if (!(spline instanceof CubicSpline.Multipoint<SplineFunction.Coordinate> multipoint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reach.visited.put(multipoint, Boolean.TRUE) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reach.count(category);
|
||||||
|
reach(multipoint.coordinate().function(), reach, category);
|
||||||
|
for (CubicSpline<SplineFunction.Coordinate> value : multipoint.values()) {
|
||||||
|
reachSpline(value, reach, category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Body body(DensityFunction value) {
|
||||||
|
Body known = bodies.get(value);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
Body body = new Body();
|
||||||
|
local(value, body, true);
|
||||||
|
bodies.put(value, body);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void local(DensityFunction function, Body body, boolean top) {
|
||||||
|
if (function instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
if (!top) {
|
||||||
|
DensityFunction value = holder.value();
|
||||||
|
names.putIfAbsent(value, name(holder));
|
||||||
|
body.references.merge(value, 1.0, Double::sum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
local(holder.value(), body, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (function instanceof ConstantFunction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.self++;
|
||||||
|
if (function instanceof SplineFunction spline) {
|
||||||
|
localSpline(spline.spline(), body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (DensityFunction child : children(function)) {
|
||||||
|
local(child, body, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void localSpline(CubicSpline<SplineFunction.Coordinate> spline, Body body) {
|
||||||
|
if (!(spline instanceof CubicSpline.Multipoint<SplineFunction.Coordinate> multipoint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.self++;
|
||||||
|
local(multipoint.coordinate().function(), body, false);
|
||||||
|
for (CubicSpline<SplineFunction.Coordinate> value : multipoint.values()) {
|
||||||
|
localSpline(value, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<DensityFunction, Double> multiplicity(Body root) {
|
||||||
|
List<DensityFunction> order = new ArrayList<>();
|
||||||
|
Map<DensityFunction, Boolean> visited = new IdentityHashMap<>();
|
||||||
|
for (DensityFunction child : root.references.keySet()) {
|
||||||
|
postOrder(child, visited, order);
|
||||||
|
}
|
||||||
|
Collections.reverse(order);
|
||||||
|
Map<DensityFunction, Double> multiplicity = new IdentityHashMap<>(order.size() * 2);
|
||||||
|
for (Map.Entry<DensityFunction, Double> entry : root.references.entrySet()) {
|
||||||
|
multiplicity.merge(entry.getKey(), entry.getValue(), Double::sum);
|
||||||
|
}
|
||||||
|
for (DensityFunction value : order) {
|
||||||
|
double times = multiplicity.getOrDefault(value, 0.0);
|
||||||
|
for (Map.Entry<DensityFunction, Double> entry : body(value).references.entrySet()) {
|
||||||
|
multiplicity.merge(entry.getKey(), times * entry.getValue(), Double::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return multiplicity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postOrder(DensityFunction value, Map<DensityFunction, Boolean> visited, List<DensityFunction> order) {
|
||||||
|
if (visited.put(value, Boolean.TRUE) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (DensityFunction child : body(value).references.keySet()) {
|
||||||
|
postOrder(child, visited, order);
|
||||||
|
}
|
||||||
|
order.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double surfaceSteps(FindTopSurfaceFunction surface) {
|
||||||
|
double upper = surface.upperBound().range().max();
|
||||||
|
if (!Double.isFinite(upper)) {
|
||||||
|
return MAX_SURFACE_STEPS;
|
||||||
|
}
|
||||||
|
double steps = Math.floor((upper - surface.lowerBound()) / surface.cellHeight()) + 1.0;
|
||||||
|
return Math.clamp(steps, 1.0, MAX_SURFACE_STEPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DensityFunction> children(DensityFunction function) {
|
||||||
|
List<DensityFunction> children = new ArrayList<>(4);
|
||||||
|
function.rewriteChildren(child -> {
|
||||||
|
children.add(child);
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String name(Holder<DensityFunction> holder) {
|
||||||
|
return holder.unwrapKey().map(key -> key.identifier().toString()).orElse(INLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Costs(double expanded, double lazy, double cachedLocal) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Offender(String name, String category, double multiplicity, double selfNodes, double subtreeNodes,
|
||||||
|
double contribution, double inclusive) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record RootCost(String root, long dagNodes, double cachedNodes, double expandedNodes, double uncachedPointWorst,
|
||||||
|
int namedSubtrees, List<Offender> exclusive, List<Offender> inclusive, Map<String, Double> categoryShare,
|
||||||
|
Map<String, Double> dagShare) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Reach {
|
||||||
|
private final Map<Object, Boolean> visited = new IdentityHashMap<>();
|
||||||
|
private final Map<CacheFunction, Boolean> caches = new IdentityHashMap<>();
|
||||||
|
private final Map<String, Double> categories = new TreeMap<>();
|
||||||
|
private long nodes;
|
||||||
|
|
||||||
|
private void count(String category) {
|
||||||
|
nodes++;
|
||||||
|
categories.merge(category, 1.0, Double::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Body {
|
||||||
|
private final Map<DensityFunction, Double> references = new IdentityHashMap<>();
|
||||||
|
private double self;
|
||||||
|
}
|
||||||
|
}
|
||||||
+409
@@ -0,0 +1,409 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
final class DensityJsonBreakdown {
|
||||||
|
private static final String DENSITY_FUNCTIONS = "/worldgen/density_function/";
|
||||||
|
private static final String NOISES = "/worldgen/noise/";
|
||||||
|
private static final String JSON = ".json";
|
||||||
|
private static final String CELLULAR = "cellular";
|
||||||
|
private static final String NOISE_STYLE = "noise style";
|
||||||
|
private static final String UNREFERENCED = "unreferenced";
|
||||||
|
private static final String OFFSET_MARKER = "/offset_";
|
||||||
|
private static final String OFFSET_SUFFIX = " offset copies";
|
||||||
|
private static final int INSTANCE_SEGMENTS = 3;
|
||||||
|
private static final Set<String> ROUTER_ROOTS = Set.of("final_density", "temperature", "vegetation", "continents", "erosion",
|
||||||
|
"depth", "ridges", "chunk_surface_level", "barrier", "fluid_level_floodedness", "fluid_level_spread", "lava", "exclusion",
|
||||||
|
"surface_level", "spawn_elevation");
|
||||||
|
private static final long FNV_OFFSET = 0xcbf29ce484222325L;
|
||||||
|
private static final long FNV_PRIME = 0x100000001b3L;
|
||||||
|
private static final int TOP = 10;
|
||||||
|
|
||||||
|
private final String dimension;
|
||||||
|
private final String dimensionPrefix;
|
||||||
|
private final Map<String, Integer> categoryIndex = new HashMap<>();
|
||||||
|
private final List<String> categoryNames = new ArrayList<>();
|
||||||
|
private final Map<String, Entry> entries = new TreeMap<>();
|
||||||
|
private final Map<String, List<String>> parents = new HashMap<>();
|
||||||
|
private final Map<String, Long> owners = new HashMap<>();
|
||||||
|
|
||||||
|
private DensityJsonBreakdown(String dimension) {
|
||||||
|
this.dimension = dimension;
|
||||||
|
dimensionPrefix = dimension + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
static DensityJsonBreakdown analyze(SortedMap<String, byte[]> files, String dimension) {
|
||||||
|
DensityJsonBreakdown breakdown = new DensityJsonBreakdown(dimension);
|
||||||
|
breakdown.load(files);
|
||||||
|
return breakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String category(String dimension, String id) {
|
||||||
|
String base = baseCategory(dimension, id);
|
||||||
|
return id.contains(OFFSET_MARKER) ? base + OFFSET_SUFFIX : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String baseCategory(String dimension, String id) {
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
String path = separator < 0 ? id : id.substring(separator + 1);
|
||||||
|
String dimensionPrefix = dimension + "/";
|
||||||
|
if (path.startsWith(dimensionPrefix)) {
|
||||||
|
path = path.substring(dimensionPrefix.length());
|
||||||
|
}
|
||||||
|
if (ROUTER_ROOTS.contains(path)) {
|
||||||
|
return "router";
|
||||||
|
}
|
||||||
|
if (path.startsWith("cell/")) {
|
||||||
|
return CELLULAR;
|
||||||
|
}
|
||||||
|
if (path.startsWith("style/")) {
|
||||||
|
return NOISE_STYLE;
|
||||||
|
}
|
||||||
|
if (path.startsWith("biome/selector/")) {
|
||||||
|
return "biome selector";
|
||||||
|
}
|
||||||
|
if (path.startsWith("biome/carving/")) {
|
||||||
|
return "biome carving";
|
||||||
|
}
|
||||||
|
if (path.startsWith("biome/")) {
|
||||||
|
return "biome decision";
|
||||||
|
}
|
||||||
|
if (path.equals("height") || path.startsWith("height/") || path.startsWith("terrain/height/")) {
|
||||||
|
return "terrain height";
|
||||||
|
}
|
||||||
|
if (path.startsWith("terrain/generator/")) {
|
||||||
|
return "terrain generator";
|
||||||
|
}
|
||||||
|
if (path.startsWith("terrain/volume/")) {
|
||||||
|
return "terrain volumetric";
|
||||||
|
}
|
||||||
|
if (path.startsWith("terrain/")) {
|
||||||
|
return "terrain bands";
|
||||||
|
}
|
||||||
|
if (path.startsWith("cave")) {
|
||||||
|
return "caves";
|
||||||
|
}
|
||||||
|
if (path.startsWith("surface/")) {
|
||||||
|
return "surface";
|
||||||
|
}
|
||||||
|
int slash = path.indexOf('/');
|
||||||
|
return slash < 0 ? path : path.substring(0, slash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Report report() {
|
||||||
|
Map<String, CategoryAccumulator> categories = new TreeMap<>();
|
||||||
|
Map<String, CategoryAccumulator> groups = new TreeMap<>();
|
||||||
|
Map<String, Long> libraryOwners = new TreeMap<>();
|
||||||
|
long totalBytes = 0;
|
||||||
|
long totalNodes = 0;
|
||||||
|
for (Map.Entry<String, Entry> item : entries.entrySet()) {
|
||||||
|
String id = item.getKey();
|
||||||
|
Entry entry = item.getValue();
|
||||||
|
totalBytes += entry.bytes;
|
||||||
|
totalNodes += entry.nodes;
|
||||||
|
String category = category(dimension, id);
|
||||||
|
categories.computeIfAbsent(category, ignored -> new CategoryAccumulator()).add(entry);
|
||||||
|
groups.computeIfAbsent(group(id), ignored -> new CategoryAccumulator()).add(entry);
|
||||||
|
if (isLibrary(category)) {
|
||||||
|
libraryOwners.merge(category + " via " + ownerLabel(owners(id)), entry.bytes, Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<CategoryBytes> categoryRows = rows(categories, Integer.MAX_VALUE);
|
||||||
|
List<CategoryBytes> groupRows = rows(groups, 40);
|
||||||
|
List<Bytes> ownerRows = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Long> owner : libraryOwners.entrySet()) {
|
||||||
|
ownerRows.add(new Bytes(owner.getKey(), owner.getValue()));
|
||||||
|
}
|
||||||
|
ownerRows.sort(Comparator.comparingLong(Bytes::bytes).reversed());
|
||||||
|
List<Bytes> largest = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Entry> item : entries.entrySet()) {
|
||||||
|
largest.add(new Bytes(item.getKey(), item.getValue().bytes));
|
||||||
|
}
|
||||||
|
largest.sort(Comparator.comparingLong(Bytes::bytes).reversed());
|
||||||
|
return new Report(entries.size(), totalBytes, totalNodes, categoryRows, groupRows, ownerRows,
|
||||||
|
List.copyOf(largest.subList(0, Math.min(TOP, largest.size()))), cellularInstances());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load(SortedMap<String, byte[]> files) {
|
||||||
|
Map<String, byte[]> bodies = new HashMap<>();
|
||||||
|
Set<String> noises = new HashSet<>();
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
String id = resourceId(file.getKey(), DENSITY_FUNCTIONS);
|
||||||
|
if (id != null) {
|
||||||
|
bodies.put(id, file.getValue());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String noise = resourceId(file.getKey(), NOISES);
|
||||||
|
if (noise != null) {
|
||||||
|
noises.add(noise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, byte[]> body : bodies.entrySet()) {
|
||||||
|
JsonElement json = JsonParser.parseString(new String(body.getValue(), StandardCharsets.UTF_8));
|
||||||
|
Entry entry = new Entry(body.getValue().length);
|
||||||
|
entry.shape = shape(json, bodies.keySet(), noises, entry);
|
||||||
|
entries.put(body.getKey(), entry);
|
||||||
|
for (String reference : entry.references) {
|
||||||
|
parents.computeIfAbsent(reference, ignored -> new ArrayList<>()).add(body.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long shape(JsonElement element, Set<String> functions, Set<String> noises, Entry entry) {
|
||||||
|
if (element.isJsonObject()) {
|
||||||
|
entry.nodes++;
|
||||||
|
JsonObject object = element.getAsJsonObject();
|
||||||
|
long hash = mix(FNV_OFFSET, '{');
|
||||||
|
for (String key : new TreeMap<>(object.asMap()).keySet()) {
|
||||||
|
hash = mix(mix(hash, key.hashCode()), shape(object.get(key), functions, noises, entry));
|
||||||
|
}
|
||||||
|
return mix(hash, '}');
|
||||||
|
}
|
||||||
|
if (element.isJsonArray()) {
|
||||||
|
JsonArray array = element.getAsJsonArray();
|
||||||
|
long hash = mix(FNV_OFFSET, '[');
|
||||||
|
for (JsonElement item : array) {
|
||||||
|
hash = mix(hash, shape(item, functions, noises, entry));
|
||||||
|
}
|
||||||
|
return mix(hash, ']');
|
||||||
|
}
|
||||||
|
if (element.isJsonPrimitive()) {
|
||||||
|
JsonPrimitive primitive = element.getAsJsonPrimitive();
|
||||||
|
if (primitive.isNumber()) {
|
||||||
|
return mix(FNV_OFFSET, 'N');
|
||||||
|
}
|
||||||
|
if (primitive.isString()) {
|
||||||
|
String value = primitive.getAsString();
|
||||||
|
if (functions.contains(value)) {
|
||||||
|
entry.references.add(value);
|
||||||
|
return mix(FNV_OFFSET, 'R');
|
||||||
|
}
|
||||||
|
if (noises.contains(value)) {
|
||||||
|
return mix(FNV_OFFSET, 'Z');
|
||||||
|
}
|
||||||
|
return mix(FNV_OFFSET, value.hashCode());
|
||||||
|
}
|
||||||
|
return mix(FNV_OFFSET, primitive.getAsBoolean() ? 'T' : 'F');
|
||||||
|
}
|
||||||
|
return mix(FNV_OFFSET, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CellularShape> cellularInstances() {
|
||||||
|
Map<String, List<String>> instances = new TreeMap<>();
|
||||||
|
for (String id : entries.keySet()) {
|
||||||
|
if (!isLibrary(category(dimension, id)) || !id.contains(":cell/")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
instances.computeIfAbsent(instanceKey(id), ignored -> new ArrayList<>()).add(id);
|
||||||
|
}
|
||||||
|
Map<Long, CellularAccumulator> shapes = new HashMap<>();
|
||||||
|
for (Map.Entry<String, List<String>> instance : instances.entrySet()) {
|
||||||
|
List<String> members = instance.getValue();
|
||||||
|
members.sort(String::compareTo);
|
||||||
|
long hash = FNV_OFFSET;
|
||||||
|
long bytes = 0;
|
||||||
|
long owner = 0L;
|
||||||
|
StringBuilder layout = new StringBuilder();
|
||||||
|
for (String member : members) {
|
||||||
|
Entry entry = entries.get(member);
|
||||||
|
String suffix = member.substring(instance.getKey().length());
|
||||||
|
hash = mix(mix(hash, suffix.hashCode()), entry.shape);
|
||||||
|
bytes += entry.bytes;
|
||||||
|
owner |= owners(member);
|
||||||
|
if (!layout.isEmpty()) {
|
||||||
|
layout.append(',');
|
||||||
|
}
|
||||||
|
layout.append(suffix);
|
||||||
|
}
|
||||||
|
CellularAccumulator accumulator = shapes.computeIfAbsent(hash, ignored -> new CellularAccumulator(instance.getKey(), layout.toString()));
|
||||||
|
accumulator.instances++;
|
||||||
|
accumulator.bytes += bytes;
|
||||||
|
accumulator.owners |= owner;
|
||||||
|
}
|
||||||
|
List<CellularShape> rows = new ArrayList<>(shapes.size());
|
||||||
|
for (CellularAccumulator accumulator : shapes.values()) {
|
||||||
|
rows.add(new CellularShape(accumulator.example, accumulator.layout, accumulator.instances, accumulator.bytes, ownerLabel(accumulator.owners)));
|
||||||
|
}
|
||||||
|
rows.sort(Comparator.comparingLong(CellularShape::bytes).reversed());
|
||||||
|
return List.copyOf(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long owners(String id) {
|
||||||
|
Long known = owners.get(id);
|
||||||
|
if (known != null) {
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
owners.put(id, 0L);
|
||||||
|
long mask = 0L;
|
||||||
|
List<String> referrers = parents.get(id);
|
||||||
|
if (referrers == null) {
|
||||||
|
mask = bit(UNREFERENCED);
|
||||||
|
} else {
|
||||||
|
for (String parent : referrers) {
|
||||||
|
String category = category(dimension, parent);
|
||||||
|
mask |= isLibrary(category) ? owners(parent) : bit(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owners.put(id, mask);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long bit(String category) {
|
||||||
|
int index = categoryIndex.computeIfAbsent(category, name -> {
|
||||||
|
categoryNames.add(name);
|
||||||
|
return categoryNames.size() - 1;
|
||||||
|
});
|
||||||
|
if (index >= Long.SIZE) {
|
||||||
|
throw new IllegalStateException("More than " + Long.SIZE + " density function owner categories");
|
||||||
|
}
|
||||||
|
return 1L << index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ownerLabel(long mask) {
|
||||||
|
if (mask == 0L) {
|
||||||
|
return UNREFERENCED;
|
||||||
|
}
|
||||||
|
List<String> labels = new ArrayList<>(Long.bitCount(mask));
|
||||||
|
for (int index = 0; index < categoryNames.size(); index++) {
|
||||||
|
if ((mask & (1L << index)) != 0L) {
|
||||||
|
labels.add(categoryNames.get(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels.sort(String::compareTo);
|
||||||
|
return String.join("+", labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String group(String id) {
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
String path = separator < 0 ? id : id.substring(separator + 1);
|
||||||
|
if (path.startsWith(dimensionPrefix)) {
|
||||||
|
path = path.substring(dimensionPrefix.length());
|
||||||
|
}
|
||||||
|
String[] segments = path.split("/");
|
||||||
|
if (segments.length == 1) {
|
||||||
|
return segments[0];
|
||||||
|
}
|
||||||
|
if (segments[0].equals("cell") || segments[0].equals("style")) {
|
||||||
|
return segments[0] + "/*/" + segments[segments.length - 1];
|
||||||
|
}
|
||||||
|
return segments[0] + "/" + segments[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLibrary(String category) {
|
||||||
|
return category.startsWith(CELLULAR) || category.startsWith(NOISE_STYLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String instanceKey(String id) {
|
||||||
|
int end = id.indexOf(':') + 1;
|
||||||
|
for (int segment = 0; segment < INSTANCE_SEGMENTS && end > 0; segment++) {
|
||||||
|
int slash = id.indexOf('/', end);
|
||||||
|
if (slash < 0) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
end = slash + 1;
|
||||||
|
}
|
||||||
|
return id.substring(0, end - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CategoryBytes> rows(Map<String, CategoryAccumulator> accumulators, int limit) {
|
||||||
|
List<CategoryBytes> rows = new ArrayList<>(accumulators.size());
|
||||||
|
for (Map.Entry<String, CategoryAccumulator> item : accumulators.entrySet()) {
|
||||||
|
CategoryAccumulator accumulator = item.getValue();
|
||||||
|
long representative = 0;
|
||||||
|
for (long bytes : accumulator.representativeBytes.values()) {
|
||||||
|
representative += bytes;
|
||||||
|
}
|
||||||
|
rows.add(new CategoryBytes(item.getKey(), accumulator.files, accumulator.bytes, accumulator.nodes,
|
||||||
|
accumulator.representativeBytes.size(), accumulator.bytes - representative));
|
||||||
|
}
|
||||||
|
rows.sort(Comparator.comparingLong(CategoryBytes::bytes).reversed());
|
||||||
|
return List.copyOf(rows.subList(0, Math.min(limit, rows.size())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resourceId(String path, String folder) {
|
||||||
|
int marker = path.indexOf(folder);
|
||||||
|
if (!path.startsWith("data/") || marker < 0 || !path.endsWith(JSON)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return path.substring("data/".length(), marker) + ":" + path.substring(marker + folder.length(), path.length() - JSON.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long hash, long value) {
|
||||||
|
long mixed = hash ^ value;
|
||||||
|
mixed *= FNV_PRIME;
|
||||||
|
return mixed ^ (mixed >>> 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Report(int files, long bytes, long nodes, List<CategoryBytes> categories, List<CategoryBytes> groups,
|
||||||
|
List<Bytes> libraryOwners, List<Bytes> largest, List<CellularShape> cellularShapes) {
|
||||||
|
int cellularInstances() {
|
||||||
|
int count = 0;
|
||||||
|
for (CellularShape shape : cellularShapes) {
|
||||||
|
count += shape.instances();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CategoryBytes(String category, int files, long bytes, long nodes, int distinctShapes, long structurallyDuplicateBytes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Bytes(String name, long bytes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record CellularShape(String example, String members, int instances, long bytes, String owners) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Entry {
|
||||||
|
private final long bytes;
|
||||||
|
private final List<String> references = new ArrayList<>();
|
||||||
|
private long nodes;
|
||||||
|
private long shape;
|
||||||
|
|
||||||
|
private Entry(long bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CategoryAccumulator {
|
||||||
|
private final Map<Long, Long> representativeBytes = new HashMap<>();
|
||||||
|
private int files;
|
||||||
|
private long bytes;
|
||||||
|
private long nodes;
|
||||||
|
|
||||||
|
private void add(Entry entry) {
|
||||||
|
files++;
|
||||||
|
bytes += entry.bytes;
|
||||||
|
nodes += entry.nodes;
|
||||||
|
representativeBytes.putIfAbsent(entry.shape, entry.bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CellularAccumulator {
|
||||||
|
private final String example;
|
||||||
|
private final String layout;
|
||||||
|
private int instances;
|
||||||
|
private long bytes;
|
||||||
|
private long owners;
|
||||||
|
|
||||||
|
private CellularAccumulator(String example, String layout) {
|
||||||
|
this.example = example;
|
||||||
|
this.layout = layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||||
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
|
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
|
||||||
import art.arcane.iris.spi.IrisPlatform;
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
@@ -9,6 +10,7 @@ import art.arcane.iris.spi.PlatformBiomeWriter;
|
|||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.iris.spi.PlatformScheduler;
|
import art.arcane.iris.spi.PlatformScheduler;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
||||||
import art.arcane.volmlib.nativelib.item.NativeItem;
|
import art.arcane.volmlib.nativelib.item.NativeItem;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.ModdedBlockState;
|
||||||
@@ -35,6 +37,7 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
private final NativeRegistryAccess nativeAccess;
|
private final NativeRegistryAccess nativeAccess;
|
||||||
private final PlatformBiomeWriter biomeWriter;
|
private final PlatformBiomeWriter biomeWriter;
|
||||||
private final PlatformGenerationRegistry generationRegistry;
|
private final PlatformGenerationRegistry generationRegistry;
|
||||||
|
private final ClasspathVanillaDataSource vanillaData = new ClasspathVanillaDataSource(() -> SharedConstants.class);
|
||||||
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
||||||
private final NativeBlockState air = blocks.getAir();
|
private final NativeBlockState air = blocks.getAir();
|
||||||
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||||
@@ -215,6 +218,11 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
|||||||
return biomeWriter;
|
return biomeWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VanillaDataSource vanillaData() {
|
||||||
|
return vanillaData;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File dataFolder() {
|
public File dataFolder() {
|
||||||
return services.dataFolder();
|
return services.dataFolder();
|
||||||
|
|||||||
+11
-4
@@ -4,6 +4,7 @@ import art.arcane.iris.pack.datapack.IrisDatapackCompiler;
|
|||||||
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.RegistryAccess;
|
import net.minecraft.core.RegistryAccess;
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -25,6 +26,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
final class HeadlessNativeRegistries implements AutoCloseable {
|
final class HeadlessNativeRegistries implements AutoCloseable {
|
||||||
@@ -70,7 +72,7 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HeadlessNativeRegistries load(Path directory) {
|
static HeadlessNativeRegistries load(Path directory) {
|
||||||
PackLocationInfo location = new PackLocationInfo("iris-headless", Component.literal("Iris"),
|
PackLocationInfo location = new PackLocationInfo("iris-headless", Component.literal("Iris"),
|
||||||
PackSource.WORLD, Optional.empty());
|
PackSource.WORLD, Optional.empty());
|
||||||
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
|
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
|
||||||
@@ -78,12 +80,17 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
|||||||
try {
|
try {
|
||||||
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||||
RegistryAccess.Frozen loaded;
|
RegistryAccess.Frozen loaded;
|
||||||
|
RegistryAccess.Frozen dimensions;
|
||||||
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
||||||
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
|
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
|
||||||
RegistryDataLoader.WORLD_REGISTRIES, executor).join();
|
RegistryDataLoader.WORLD_REGISTRIES, executor).join();
|
||||||
|
List<HolderLookup.RegistryLookup<?>> dimensionContext =
|
||||||
|
Stream.concat(builtins.listRegistries(), loaded.listRegistries()).toList();
|
||||||
|
dimensions = RegistryDataLoader.load(resources, dimensionContext,
|
||||||
|
RegistryDataLoader.DIMENSION_REGISTRIES, executor).join();
|
||||||
}
|
}
|
||||||
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(
|
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(Stream.of(
|
||||||
Stream.concat(builtins.registries(), loaded.registries())).freeze();
|
builtins.registries(), loaded.registries(), dimensions.registries()).flatMap(Function.identity())).freeze();
|
||||||
HeadlessNativeBootstrap.bindComponents(registries);
|
HeadlessNativeBootstrap.bindComponents(registries);
|
||||||
return new HeadlessNativeRegistries(directory, resources, registries);
|
return new HeadlessNativeRegistries(directory, resources, registries);
|
||||||
} catch (RuntimeException failure) {
|
} catch (RuntimeException failure) {
|
||||||
@@ -106,7 +113,7 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
|||||||
delete(directory);
|
delete(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void delete(Path directory) throws IOException {
|
static void delete(Path directory) throws IOException {
|
||||||
try (Stream<Path> paths = Files.walk(directory)) {
|
try (Stream<Path> paths = Files.walk(directory)) {
|
||||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
||||||
Files.delete(path);
|
Files.delete(path);
|
||||||
|
|||||||
+8
-2
@@ -32,6 +32,7 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
private final Path stagingRoot;
|
private final Path stagingRoot;
|
||||||
private NativeRegionTerrainWriter writer;
|
private NativeRegionTerrainWriter writer;
|
||||||
private HeadlessNativeRegistries nativeRegistries;
|
private HeadlessNativeRegistries nativeRegistries;
|
||||||
|
private File preparedPack;
|
||||||
private HolderLookup.Provider registries;
|
private HolderLookup.Provider registries;
|
||||||
|
|
||||||
HeadlessNativeRuntime() {
|
HeadlessNativeRuntime() {
|
||||||
@@ -60,12 +61,16 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
@Override
|
@Override
|
||||||
public void prepare(IrisData data, IrisDimension dimension) {
|
public void prepare(IrisData data, IrisDimension dimension) {
|
||||||
try {
|
try {
|
||||||
if (nativeRegistries != null) {
|
File pack = data.getDataFolder().getCanonicalFile();
|
||||||
|
if (nativeRegistries != null && !pack.equals(preparedPack)) {
|
||||||
throw new IllegalStateException("Headless native runtime supports one immutable pack per process");
|
throw new IllegalStateException("Headless native runtime supports one immutable pack per process");
|
||||||
}
|
}
|
||||||
|
if (nativeRegistries == null) {
|
||||||
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
||||||
registries = nativeRegistries.registries();
|
preparedPack = pack;
|
||||||
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
||||||
|
}
|
||||||
|
registries = nativeRegistries.registries();
|
||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw new UncheckedIOException("Cannot load native generation registries", failure);
|
throw new UncheckedIOException("Cannot load native generation registries", failure);
|
||||||
}
|
}
|
||||||
@@ -92,6 +97,7 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
|||||||
if (nativeRegistries != null) {
|
if (nativeRegistries != null) {
|
||||||
nativeRegistries.close();
|
nativeRegistries.close();
|
||||||
nativeRegistries = null;
|
nativeRegistries = null;
|
||||||
|
preparedPack = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import io.papermc.paper.registry.PaperRegistryAccess;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
final class PaperRegistryScope implements AutoCloseable {
|
||||||
|
private final Map<Object, Object> registries;
|
||||||
|
private final Map<Object, Object> previous;
|
||||||
|
|
||||||
|
private PaperRegistryScope(Map<Object, Object> registries) {
|
||||||
|
this.registries = registries;
|
||||||
|
this.previous = new HashMap<>(registries);
|
||||||
|
registries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static PaperRegistryScope open() {
|
||||||
|
return new PaperRegistryScope(registries());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
registries.clear();
|
||||||
|
registries.putAll(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<Object, Object> registries() {
|
||||||
|
try {
|
||||||
|
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Paper registry access has no registries map to isolate", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+301
@@ -0,0 +1,301 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.datafix.DataFixers;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeResolver;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.ScopedDensityBuffer;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public final class WorldgenExportSampler implements AutoCloseable {
|
||||||
|
private static final String DENSITY_FUNCTIONS = "/worldgen/density_function/";
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final Method CREATE_NOISE_CHUNK = generatorMethod("createNoiseChunk", ChunkAccess.class, StructureManager.class, Blender.class,
|
||||||
|
RandomState.class, NoiseSettings.class);
|
||||||
|
private static final Method DO_FILL = generatorMethod("doFill", NoiseChunk.class, ChunkAccess.class);
|
||||||
|
|
||||||
|
private final HeadlessNativeRegistries loaded;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final RandomState randomState;
|
||||||
|
private final ThreadLocal<BiomeResolver> resolvers;
|
||||||
|
private final LevelHeightAccessor heights;
|
||||||
|
private final SortedSet<String> densityFunctionIds;
|
||||||
|
private final ThreadLocal<SamplerContext> densityContexts = ThreadLocal.withInitial(() -> SamplerContext.builder().enableCaches().build());
|
||||||
|
private final PalettedContainerFactory containers;
|
||||||
|
private final long seed;
|
||||||
|
private final long biomeZoomSeed;
|
||||||
|
|
||||||
|
private WorldgenExportSampler(HeadlessNativeRegistries loaded, LevelStem stem, long seed, SortedSet<String> densityFunctionIds) {
|
||||||
|
this.loaded = loaded;
|
||||||
|
if (!(stem.generator() instanceof NoiseBasedChunkGenerator noiseGenerator)) {
|
||||||
|
throw new IllegalStateException("Exported dimension uses " + stem.generator().getClass().getSimpleName() + ", not a noise generator");
|
||||||
|
}
|
||||||
|
this.generator = noiseGenerator;
|
||||||
|
this.randomState = RandomState.create(loaded.registries().lookupOrThrow(Registries.NOISE), seed, noiseGenerator.generatorSettings().value());
|
||||||
|
this.resolvers = ThreadLocal.withInitial(() -> noiseGenerator.getBiomeSource().createCachingResolver(randomState));
|
||||||
|
DimensionType type = stem.type().value();
|
||||||
|
this.heights = LevelHeightAccessor.create(type.minY(), type.height());
|
||||||
|
this.densityFunctionIds = densityFunctionIds;
|
||||||
|
this.containers = PalettedContainerFactory.create(loaded.registries());
|
||||||
|
this.seed = seed;
|
||||||
|
this.biomeZoomSeed = BiomeManager.obfuscateSeed(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportSampler open(Map<String, byte[]> files, WorldgenExportMode mode, String dimensionId, long seed) throws IOException {
|
||||||
|
Objects.requireNonNull(mode);
|
||||||
|
Identifier dimension = Identifier.parse(dimensionId);
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
Path directory = Files.createTempDirectory("iris-worldgen-export-");
|
||||||
|
HeadlessNativeRegistries loaded;
|
||||||
|
try {
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
Path target = directory.resolve(file.getKey()).normalize();
|
||||||
|
if (!target.startsWith(directory)) {
|
||||||
|
throw new IllegalArgumentException("Export file escapes the datapack root: " + file.getKey());
|
||||||
|
}
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.write(target, file.getValue());
|
||||||
|
}
|
||||||
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
|
loaded = HeadlessNativeRegistries.load(directory);
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException failure) {
|
||||||
|
HeadlessNativeRegistries.delete(directory);
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new WorldgenExportSampler(loaded, stem(loaded.registries(), mode, dimension), seed, densityFunctionIds(files));
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
loaded.close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportSampler open(Path datapack, WorldgenExportMode mode, String dimensionId, long seed) throws IOException {
|
||||||
|
return open(files(datapack), mode, dimensionId, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float density(String functionId, int x, int y, int z) {
|
||||||
|
DensityFunction function = loaded.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(functionId))).value();
|
||||||
|
return randomState.getSampler(function).sampleValue(densityContexts.get(), x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float[] densityChunk(DensityFunction function, int chunkX, int chunkZ) {
|
||||||
|
DensityVolume volume = new DensityVolume(CHUNK_WIDTH, heights.getHeight(), CHUNK_WIDTH, chunkX * CHUNK_WIDTH, heights.getMinY(), chunkZ * CHUNK_WIDTH);
|
||||||
|
return sampleVolume(function, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DensityFunction finalDensity() {
|
||||||
|
return generator.generatorSettings().value().noiseRouter().finalDensity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int baseHeight(int x, int z) {
|
||||||
|
return generator.getBaseHeight(x, z, Heightmap.Types.WORLD_SURFACE_WG, heights, randomState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int floorHeight(int x, int z) {
|
||||||
|
return generator.getBaseHeight(x, z, Heightmap.Types.OCEAN_FLOOR_WG, heights, randomState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProtoChunk terrainChunk(int chunkX, int chunkZ) {
|
||||||
|
ProtoChunk chunk = new ProtoChunk(new ChunkPos(chunkX, chunkZ), UpgradeData.EMPTY, heights, containers, null);
|
||||||
|
NoiseGeneratorSettings settings = generator.generatorSettings().value();
|
||||||
|
NoiseSettings noise = settings.noiseSettings().clampToHeightAccessor(chunk.getHeightAccessorForGeneration());
|
||||||
|
try (NoiseChunk noiseChunk = (NoiseChunk) invoke(CREATE_NOISE_CHUNK, chunk, NoStructures.INSTANCE, Blender.empty(), randomState, noise)) {
|
||||||
|
invoke(DO_FILL, noiseChunk, chunk);
|
||||||
|
randomState.surfaceSystem().buildSurface(randomState, new BiomeManager(resolvers.get(), biomeZoomSeed),
|
||||||
|
new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration()), chunk, noiseChunk, settings.materialRule().value(), null);
|
||||||
|
}
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StructureStart structureStart(String structureId, int chunkX, int chunkZ) throws IOException {
|
||||||
|
Holder.Reference<Structure> structure = loaded.registries().lookupOrThrow(Registries.STRUCTURE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(structureId)));
|
||||||
|
Path storageRoot = Files.createTempDirectory("iris-worldgen-export-structures-");
|
||||||
|
try (LevelStorageSource.LevelStorageAccess storage = LevelStorageSource.createDefault(storageRoot).createAccess("export")) {
|
||||||
|
StructureTemplateManager templates = new StructureTemplateManager(loaded.resources(), storage, DataFixers.getDataFixer(),
|
||||||
|
loaded.registries().lookupOrThrow(Registries.BLOCK));
|
||||||
|
return structure.value().generate(structure, Level.OVERWORLD, loaded.registries(), generator, generator.getBiomeSource(),
|
||||||
|
randomState.createClimateSampler(SamplerContext.builder().enableCaches().build()), randomState, templates, seed,
|
||||||
|
new ChunkPos(chunkX, chunkZ), 0, heights, structure.value().biomes()::contains);
|
||||||
|
} finally {
|
||||||
|
HeadlessNativeRegistries.delete(storageRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String biome(int x, int y, int z) {
|
||||||
|
Holder<Biome> biome = resolvers.get().getNoiseBiome(QuartPos.fromBlock(x), QuartPos.fromBlock(y), QuartPos.fromBlock(z));
|
||||||
|
return biome.unwrapKey().orElseThrow().identifier().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> possibleBiomes() {
|
||||||
|
Set<String> biomes = new TreeSet<>();
|
||||||
|
for (Holder<Biome> biome : generator.getBiomeSource().possibleBiomes()) {
|
||||||
|
biomes.add(biome.unwrapKey().orElseThrow().identifier().toString());
|
||||||
|
}
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedSet<String> densityFunctionIds() {
|
||||||
|
return densityFunctionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int minY() {
|
||||||
|
return heights.getMinY();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int height() {
|
||||||
|
return heights.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int seaLevel() {
|
||||||
|
return generator.getSeaLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegistryAccess registries() {
|
||||||
|
return loaded.registries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
loaded.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] sampleVolume(DensityFunction function, DensityVolume volume) {
|
||||||
|
try (ScopedDensityBuffer buffer = randomState.getSampler(function).bind(densityContexts.get()).sampleVolume(volume)) {
|
||||||
|
float[] values = new float[volume.size()];
|
||||||
|
for (int index = 0; index < values.length; index++) {
|
||||||
|
values[index] = buffer.get(index);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object invoke(Method method, Object... arguments) {
|
||||||
|
try {
|
||||||
|
return method.invoke(generator, arguments);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
if (failure.getCause() instanceof RuntimeException runtime) {
|
||||||
|
throw runtime;
|
||||||
|
}
|
||||||
|
if (failure.getCause() instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Terrain stage " + method.getName() + " failed", failure.getCause());
|
||||||
|
} catch (IllegalAccessException failure) {
|
||||||
|
throw new IllegalStateException("Terrain stage " + method.getName() + " is not accessible", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method generatorMethod(String name, Class<?>... parameters) {
|
||||||
|
try {
|
||||||
|
Method method = NoiseBasedChunkGenerator.class.getDeclaredMethod(name, parameters);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalStateException("NoiseBasedChunkGenerator has no terrain stage " + name, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LevelStem stem(RegistryAccess registries, WorldgenExportMode mode, Identifier dimension) {
|
||||||
|
return switch (mode) {
|
||||||
|
case PRESET -> registries.lookupOrThrow(Registries.WORLD_PRESET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, dimension)).value().overworld()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("World preset " + dimension + " has no overworld"));
|
||||||
|
case DIMENSION -> levelStem(registries, dimension);
|
||||||
|
case OVERWORLD -> levelStem(registries, LevelStem.OVERWORLD.identifier());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LevelStem levelStem(RegistryAccess registries, Identifier id) {
|
||||||
|
return registries.lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(ResourceKey.create(Registries.LEVEL_STEM, id)).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> files(Path datapack) throws IOException {
|
||||||
|
if (!Files.isDirectory(datapack)) {
|
||||||
|
throw new IOException("Exported datapack " + datapack + " is not a directory");
|
||||||
|
}
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
try (Stream<Path> paths = Files.walk(datapack)) {
|
||||||
|
List<Path> regular = paths.filter(Files::isRegularFile).toList();
|
||||||
|
for (Path file : regular) {
|
||||||
|
files.put(datapack.relativize(file).toString().replace('\\', '/'), Files.readAllBytes(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedSet<String> densityFunctionIds(Map<String, byte[]> files) {
|
||||||
|
SortedSet<String> ids = new TreeSet<>();
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
int marker = path.indexOf(DENSITY_FUNCTIONS);
|
||||||
|
if (path.startsWith("data/") && marker > 0 && path.endsWith(".json")) {
|
||||||
|
String namespace = path.substring("data/".length(), marker);
|
||||||
|
ids.add(namespace + ":" + path.substring(marker + DENSITY_FUNCTIONS.length(), path.length() - ".json".length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NoStructures extends StructureManager {
|
||||||
|
private static final NoStructures INSTANCE = new NoStructures();
|
||||||
|
|
||||||
|
private NoStructures() {
|
||||||
|
super(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StructureStart> startsForStructure(int chunkX, int chunkZ, Predicate<Structure> predicate) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+394
@@ -0,0 +1,394 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import io.papermc.paper.configuration.WorldConfiguration;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.biome.BiomeResolver;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.PalettedContainerFactory;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.chunk.UpgradeData;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.Aquifer;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityVolume;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||||
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.ThreadMXBean;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class WorldgenStageBench {
|
||||||
|
private static final int SAMPLE_RADIUS_BLOCKS = 32_768;
|
||||||
|
private static final double NANOS_PER_MILLI = 1_000_000.0;
|
||||||
|
private static final double NANOS_PER_MICRO = 1_000.0;
|
||||||
|
private static final ThreadMXBean THREADS = ManagementFactory.getThreadMXBean();
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
private final NoiseBasedChunkGenerator generator;
|
||||||
|
private final NoiseGeneratorSettings settings;
|
||||||
|
private final RandomState randomState;
|
||||||
|
private final LevelHeightAccessor heights;
|
||||||
|
private final PalettedContainerFactory containers;
|
||||||
|
private final long seed;
|
||||||
|
private final Method fill;
|
||||||
|
private final Aquifer.FluidPicker fluidPicker;
|
||||||
|
private final BiomeResolver uncached;
|
||||||
|
private final Level surfaceLevel;
|
||||||
|
|
||||||
|
WorldgenStageBench(Options options) {
|
||||||
|
label = options.label();
|
||||||
|
if (!(options.stem().generator() instanceof NoiseBasedChunkGenerator noiseGenerator)) {
|
||||||
|
throw new IllegalStateException(label + " uses " + options.stem().generator().getClass().getSimpleName() + ", not a noise generator");
|
||||||
|
}
|
||||||
|
generator = noiseGenerator;
|
||||||
|
settings = noiseGenerator.generatorSettings().value();
|
||||||
|
seed = options.seed();
|
||||||
|
randomState = RandomState.create(options.registries().lookupOrThrow(Registries.NOISE), seed, settings);
|
||||||
|
DimensionType type = options.stem().type().value();
|
||||||
|
heights = LevelHeightAccessor.create(type.minY(), type.height());
|
||||||
|
containers = PalettedContainerFactory.create(options.registries());
|
||||||
|
fill = fillMethod();
|
||||||
|
fluidPicker = fluidPicker(noiseGenerator);
|
||||||
|
uncached = noiseGenerator.getBiomeSource().createUncachedResolver(randomState);
|
||||||
|
surfaceLevel = paperConfiguredLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
String label() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
NoiseGeneratorSettings settings() {
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
void chunks(int originChunkX, int originChunkZ, int side, ChunkSamples samples) {
|
||||||
|
int ring = side + 2;
|
||||||
|
Map<Long, ProtoChunk> chunks = new HashMap<>(ring * ring * 2);
|
||||||
|
for (int dz = -1; dz <= side; dz++) {
|
||||||
|
for (int dx = -1; dx <= side; dx++) {
|
||||||
|
ChunkPos position = new ChunkPos(originChunkX + dx, originChunkZ + dz);
|
||||||
|
ProtoChunk chunk = new ProtoChunk(position, UpgradeData.EMPTY, heights, containers, null);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
generator.createBiomes(randomState, Blender.empty(), null, chunk).join();
|
||||||
|
samples.biomes.add(System.nanoTime() - started, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
chunk.setPersistedStatus(ChunkStatus.BIOMES);
|
||||||
|
chunks.put(ChunkPos.pack(position.x(), position.z()), chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegionBiomes region = new RegionBiomes(chunks, uncached);
|
||||||
|
BiomeManager biomeManager = new BiomeManager(region, BiomeManager.obfuscateSeed(seed));
|
||||||
|
MaterialRule materialRule = settings.materialRule().value();
|
||||||
|
for (int dz = 0; dz < side; dz++) {
|
||||||
|
for (int dx = 0; dx < side; dx++) {
|
||||||
|
int chunkX = originChunkX + dx;
|
||||||
|
int chunkZ = originChunkZ + dz;
|
||||||
|
ProtoChunk chunk = chunks.get(ChunkPos.pack(chunkX, chunkZ));
|
||||||
|
Set<Holder<Biome>> possible = possibleBiomes(chunks, chunkX, chunkZ);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
try (NoiseChunk noiseChunk = new NoiseChunk(randomState, null, settings, fluidPicker, Blender.empty(), volume(chunk))) {
|
||||||
|
fill(noiseChunk, chunk);
|
||||||
|
long filled = System.nanoTime();
|
||||||
|
long filledCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
randomState.surfaceSystem().buildSurface(randomState, biomeManager,
|
||||||
|
new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration(), surfaceLevel), chunk, noiseChunk, materialRule, possible);
|
||||||
|
samples.surface.add(System.nanoTime() - filled, THREADS.getCurrentThreadCpuTime() - filledCpu);
|
||||||
|
samples.noise.add(filled - started, filledCpu - startedCpu);
|
||||||
|
}
|
||||||
|
ChunkPos position = chunk.getPos();
|
||||||
|
long carverStarted = System.nanoTime();
|
||||||
|
long carverStartedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
uncached.getNoiseBiome(QuartPos.fromBlock(position.getMinBlockX()), 0, QuartPos.fromBlock(position.getMinBlockZ()));
|
||||||
|
samples.carverBiome.add(System.nanoTime() - carverStarted, THREADS.getCurrentThreadCpuTime() - carverStartedCpu);
|
||||||
|
samples.chunks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
samples.biomeFallbacks += region.fallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uncachedBiome(CallBudget budget, CallSamples samples) {
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ 0x5DEECE66DL ^ samples.count);
|
||||||
|
int minQuart = QuartPos.fromBlock(heights.getMinY());
|
||||||
|
int quartHeight = QuartPos.fromBlock(heights.getHeight());
|
||||||
|
long deadline = System.nanoTime() + budget.limitNanos();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < budget.calls() && (taken < budget.minimumCalls() || System.nanoTime() < deadline)) {
|
||||||
|
int quartX = QuartPos.fromBlock(random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS));
|
||||||
|
int quartY = minQuart + random.nextInt(quartHeight);
|
||||||
|
int quartZ = QuartPos.fromBlock(random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS));
|
||||||
|
long started = System.nanoTime();
|
||||||
|
uncached.getNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
samples.wall.add(System.nanoTime() - started);
|
||||||
|
taken++;
|
||||||
|
}
|
||||||
|
samples.finish(taken, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void baseHeight(CallBudget budget, Heightmap.Types type, CallSamples samples) {
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ type.ordinal() ^ 0x2545F4914F6CDD1DL ^ samples.count);
|
||||||
|
long deadline = System.nanoTime() + budget.limitNanos();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < budget.calls() && (taken < budget.minimumCalls() || System.nanoTime() < deadline)) {
|
||||||
|
int x = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
int z = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
generator.getBaseHeight(x, z, type, heights, randomState);
|
||||||
|
samples.wall.add(System.nanoTime() - started);
|
||||||
|
taken++;
|
||||||
|
}
|
||||||
|
samples.finish(taken, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
void baseColumn(CallBudget budget, CallSamples samples) {
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ 0x9E3779B97F4A7C15L ^ samples.count);
|
||||||
|
long deadline = System.nanoTime() + budget.limitNanos();
|
||||||
|
long startedCpu = THREADS.getCurrentThreadCpuTime();
|
||||||
|
int taken = 0;
|
||||||
|
while (taken < budget.calls() && (taken < budget.minimumCalls() || System.nanoTime() < deadline)) {
|
||||||
|
int x = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
int z = random.nextInt(-SAMPLE_RADIUS_BLOCKS, SAMPLE_RADIUS_BLOCKS);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
generator.getBaseColumn(x, z, heights, randomState);
|
||||||
|
samples.wall.add(System.nanoTime() - started);
|
||||||
|
taken++;
|
||||||
|
}
|
||||||
|
samples.finish(taken, THREADS.getCurrentThreadCpuTime() - startedCpu);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fill(NoiseChunk noiseChunk, ChunkAccess chunk) {
|
||||||
|
try {
|
||||||
|
fill.invoke(generator, noiseChunk, chunk);
|
||||||
|
} catch (IllegalAccessException failure) {
|
||||||
|
throw new IllegalStateException("Cannot invoke NoiseBasedChunkGenerator.doFill for " + label, failure);
|
||||||
|
} catch (InvocationTargetException failure) {
|
||||||
|
if (failure.getCause() instanceof RuntimeException runtime) {
|
||||||
|
throw runtime;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Noise fill failed for " + label, failure.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DensityVolume volume(ChunkAccess chunk) {
|
||||||
|
NoiseSettings noise = settings.noiseSettings().clampToHeightAccessor(chunk.getHeightAccessorForGeneration());
|
||||||
|
ChunkPos position = chunk.getPos();
|
||||||
|
return new DensityVolume(16, noise.height(), 16, position.getMinBlockX(), noise.minY(), position.getMinBlockZ());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Holder<Biome>> possibleBiomes(Map<Long, ProtoChunk> chunks, int chunkX, int chunkZ) {
|
||||||
|
Set<Holder<Biome>> possible = new HashSet<>();
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
chunks.get(ChunkPos.pack(chunkX + dx, chunkZ + dz)).collectBiomesInPalette(possible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return possible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method fillMethod() {
|
||||||
|
try {
|
||||||
|
Method method = NoiseBasedChunkGenerator.class.getDeclaredMethod("doFill", NoiseChunk.class, ChunkAccess.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (NoSuchMethodException failure) {
|
||||||
|
throw new IllegalStateException("NoiseBasedChunkGenerator has no doFill(NoiseChunk, ChunkAccess)", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Aquifer.FluidPicker fluidPicker(NoiseBasedChunkGenerator generator) {
|
||||||
|
try {
|
||||||
|
Field field = NoiseBasedChunkGenerator.class.getDeclaredField("globalFluidPicker");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return ((Supplier<Aquifer.FluidPicker>) field.get(generator)).get();
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("NoiseBasedChunkGenerator has no globalFluidPicker supplier", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Level paperConfiguredLevel() {
|
||||||
|
try {
|
||||||
|
Constructor<WorldConfiguration> constructor = WorldConfiguration.class.getDeclaredConstructor(SpigotWorldConfig.class, Identifier.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
WorldConfiguration configuration = constructor.newInstance(null, Identifier.withDefaultNamespace("overworld"));
|
||||||
|
configuration.environment = configuration.new Environment();
|
||||||
|
Class<?> unsafeType = Class.forName("sun.misc.Unsafe");
|
||||||
|
Field unsafeField = unsafeType.getDeclaredField("theUnsafe");
|
||||||
|
unsafeField.setAccessible(true);
|
||||||
|
Method allocate = unsafeType.getMethod("allocateInstance", Class.class);
|
||||||
|
Level level = (Level) allocate.invoke(unsafeField.get(null), ServerLevel.class);
|
||||||
|
Field paperConfig = Level.class.getDeclaredField("paperConfig");
|
||||||
|
paperConfig.setAccessible(true);
|
||||||
|
paperConfig.set(level, configuration);
|
||||||
|
return level;
|
||||||
|
} catch (ReflectiveOperationException failure) {
|
||||||
|
throw new IllegalStateException("Cannot build the Paper world configuration that surface rules read", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Options(String label, LevelStem stem, RegistryAccess registries, long seed) {
|
||||||
|
Options {
|
||||||
|
Objects.requireNonNull(label, "label");
|
||||||
|
Objects.requireNonNull(stem, "stem");
|
||||||
|
Objects.requireNonNull(registries, "registries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CallBudget(int calls, int minimumCalls, long limitNanos) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record ChunkTimings(int chunks, StageTiming biomes, StageTiming noise, StageTiming surface, StageTiming carverBiome,
|
||||||
|
long biomeFallbacks) {
|
||||||
|
double totalMean() {
|
||||||
|
return biomes.wall().mean() + noise.wall().mean() + surface.wall().mean() + carverBiome.wall().mean();
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalCpuMean() {
|
||||||
|
return biomes.cpu().mean() + noise.cpu().mean() + surface.cpu().mean() + carverBiome.cpu().mean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record StageTiming(Summary wall, Summary cpu) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record CallTiming(Summary wall, int calls, double cpuMean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record Summary(int count, double mean, double median, double p90, double min, double max) {
|
||||||
|
private static Summary of(long[] nanos, double unit) {
|
||||||
|
if (nanos.length == 0) {
|
||||||
|
return new Summary(0, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN);
|
||||||
|
}
|
||||||
|
long[] sorted = nanos.clone();
|
||||||
|
Arrays.sort(sorted);
|
||||||
|
double total = 0.0;
|
||||||
|
for (long value : sorted) {
|
||||||
|
total += value;
|
||||||
|
}
|
||||||
|
return new Summary(sorted.length, total / sorted.length / unit, percentile(sorted, 0.5) / unit,
|
||||||
|
percentile(sorted, 0.9) / unit, sorted[0] / unit, sorted[sorted.length - 1] / unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double percentile(long[] sorted, double fraction) {
|
||||||
|
int index = (int) Math.min(sorted.length - 1, Math.round(fraction * (sorted.length - 1)));
|
||||||
|
return sorted[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class ChunkSamples {
|
||||||
|
private final StageSamples biomes = new StageSamples();
|
||||||
|
private final StageSamples noise = new StageSamples();
|
||||||
|
private final StageSamples surface = new StageSamples();
|
||||||
|
private final StageSamples carverBiome = new StageSamples();
|
||||||
|
private int chunks;
|
||||||
|
private long biomeFallbacks;
|
||||||
|
|
||||||
|
ChunkTimings timings() {
|
||||||
|
return new ChunkTimings(chunks, biomes.timing(), noise.timing(), surface.timing(), carverBiome.timing(), biomeFallbacks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class CallSamples {
|
||||||
|
private final Samples wall = new Samples();
|
||||||
|
private int count;
|
||||||
|
private long cpuNanos;
|
||||||
|
|
||||||
|
CallTiming timing() {
|
||||||
|
return new CallTiming(Summary.of(wall.values(), NANOS_PER_MICRO), count,
|
||||||
|
count == 0 ? Double.NaN : cpuNanos / (double) count / NANOS_PER_MICRO);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish(int calls, long cpu) {
|
||||||
|
count += calls;
|
||||||
|
cpuNanos += cpu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StageSamples {
|
||||||
|
private final Samples wall = new Samples();
|
||||||
|
private final Samples cpu = new Samples();
|
||||||
|
|
||||||
|
private void add(long wallNanos, long cpuNanos) {
|
||||||
|
wall.add(wallNanos);
|
||||||
|
cpu.add(cpuNanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StageTiming timing() {
|
||||||
|
return new StageTiming(Summary.of(wall.values(), NANOS_PER_MILLI), Summary.of(cpu.values(), NANOS_PER_MILLI));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Samples {
|
||||||
|
private long[] values = new long[64];
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
private void add(long value) {
|
||||||
|
if (size == values.length) {
|
||||||
|
values = Arrays.copyOf(values, size * 2);
|
||||||
|
}
|
||||||
|
values[size++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] values() {
|
||||||
|
return Arrays.copyOf(values, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RegionBiomes implements BiomeResolver {
|
||||||
|
private final Map<Long, ProtoChunk> chunks;
|
||||||
|
private final BiomeResolver fallback;
|
||||||
|
private long fallbacks;
|
||||||
|
|
||||||
|
private RegionBiomes(Map<Long, ProtoChunk> chunks, BiomeResolver fallback) {
|
||||||
|
this.chunks = chunks;
|
||||||
|
this.fallback = fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
|
||||||
|
ProtoChunk chunk = chunks.get(ChunkPos.pack(QuartPos.toSection(quartX), QuartPos.toSection(quartZ)));
|
||||||
|
if (chunk == null) {
|
||||||
|
fallbacks++;
|
||||||
|
return fallback.getNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
}
|
||||||
|
return chunk.getNoiseBiome(quartX, quartY, quartZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long fallbacks() {
|
||||||
|
return fallbacks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class VanillaNoiseColumnSource implements ColumnSource {
|
||||||
|
private final WorldgenExportSampler sampler;
|
||||||
|
private final Map<String, String> loadKeyByBiomeId;
|
||||||
|
private final int minY;
|
||||||
|
private final int maxY;
|
||||||
|
private final int seaLevel;
|
||||||
|
|
||||||
|
public VanillaNoiseColumnSource(WorldgenExportSampler sampler, Map<String, String> loadKeyByBiomeId) {
|
||||||
|
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||||
|
this.loadKeyByBiomeId = Map.copyOf(loadKeyByBiomeId);
|
||||||
|
this.minY = sampler.minY();
|
||||||
|
this.maxY = sampler.minY() + sampler.height();
|
||||||
|
this.seaLevel = sampler.seaLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String label() {
|
||||||
|
return "vanilla:26.3";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int minY() {
|
||||||
|
return minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int maxY() {
|
||||||
|
return maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FidelityColumn column(int x, int z) {
|
||||||
|
int floor = sampler.floorHeight(x, z);
|
||||||
|
int terrainY = floor - 1;
|
||||||
|
boolean wet = floor < seaLevel && sampler.baseHeight(x, z) > floor;
|
||||||
|
return new FidelityColumn(loadKey(x, Math.max(minY, terrainY), z), terrainY, wet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
sampler.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadKey(int x, int y, int z) {
|
||||||
|
String biomeId = sampler.biome(x, y, z);
|
||||||
|
String loadKey = loadKeyByBiomeId.get(biomeId);
|
||||||
|
if (loadKey == null) {
|
||||||
|
throw new IllegalStateException("Exported biome " + biomeId + " at " + x + "," + y + "," + z
|
||||||
|
+ " has no Iris load key in the export result");
|
||||||
|
}
|
||||||
|
return loadKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunctions;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.BinaryFunction;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class VanillaNoiseFidelitySource implements FidelitySource {
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final byte SOLID = 0;
|
||||||
|
private static final byte OPEN = 1;
|
||||||
|
private static final byte FLUID = 2;
|
||||||
|
private static final byte LAVA = 3;
|
||||||
|
|
||||||
|
private final WorldgenExportSampler sampler;
|
||||||
|
private final Map<String, String> loadKeyByBiomeId;
|
||||||
|
private final String wallShell;
|
||||||
|
private final DensityFunction massDensity;
|
||||||
|
private final boolean walls;
|
||||||
|
private final int minY;
|
||||||
|
private final int maxY;
|
||||||
|
|
||||||
|
public VanillaNoiseFidelitySource(WorldgenExportSampler sampler, Map<String, String> loadKeyByBiomeId, String wallShell) {
|
||||||
|
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||||
|
this.loadKeyByBiomeId = Map.copyOf(loadKeyByBiomeId);
|
||||||
|
this.wallShell = Objects.requireNonNull(wallShell, "wallShell");
|
||||||
|
this.massDensity = terrainMass(sampler.finalDensity());
|
||||||
|
this.walls = sampler.densityFunctionIds().contains(wallShell);
|
||||||
|
this.minY = sampler.minY();
|
||||||
|
this.maxY = sampler.minY() + sampler.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String label() {
|
||||||
|
return "vanilla:26.3";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int minY() {
|
||||||
|
return minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int maxY() {
|
||||||
|
return maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FidelityChunk sample(int chunkX, int chunkZ) {
|
||||||
|
return new TerrainChunk(sampler.terrainChunk(chunkX, chunkZ), chunkX * CHUNK_WIDTH, chunkZ * CHUNK_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
sampler.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadKey(int x, int y, int z) {
|
||||||
|
String biomeId = sampler.biome(x, y, z);
|
||||||
|
String loadKey = loadKeyByBiomeId.get(biomeId);
|
||||||
|
if (loadKey == null) {
|
||||||
|
throw new IllegalStateException("Exported biome " + biomeId + " at " + x + "," + y + "," + z
|
||||||
|
+ " has no Iris load key in the export result");
|
||||||
|
}
|
||||||
|
return loadKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
static DensityFunction terrainMass(DensityFunction finalDensity) {
|
||||||
|
DensityFunction root = finalDensity;
|
||||||
|
while (root instanceof DensityFunctions.HolderHolder(Holder<DensityFunction> holder)) {
|
||||||
|
root = holder.value();
|
||||||
|
}
|
||||||
|
return root instanceof BinaryFunction(BinaryFunction.Type type, DensityFunction terrain, DensityFunction ignored) && type == BinaryFunction.Type.MIN
|
||||||
|
? terrain : root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte classify(Block block) {
|
||||||
|
String id = BuiltInRegistries.BLOCK.getKey(block).toString();
|
||||||
|
if (FidelityBlocks.open(id)) {
|
||||||
|
return OPEN;
|
||||||
|
}
|
||||||
|
if (FidelityBlocks.lava(id)) {
|
||||||
|
return LAVA;
|
||||||
|
}
|
||||||
|
return FidelityBlocks.fluid(id) ? FLUID : SOLID;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Column(int surfaceY, String biome, byte[] kinds) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class TerrainChunk implements FidelityChunk {
|
||||||
|
private final ProtoChunk terrain;
|
||||||
|
private final int blockX;
|
||||||
|
private final int blockZ;
|
||||||
|
private final Column[] columns = new Column[CHUNK_WIDTH * CHUNK_WIDTH];
|
||||||
|
private final Map<Block, Byte> kinds = new IdentityHashMap<>();
|
||||||
|
private float[] masses;
|
||||||
|
|
||||||
|
private TerrainChunk(ProtoChunk terrain, int blockX, int blockZ) {
|
||||||
|
this.terrain = terrain;
|
||||||
|
this.blockX = blockX;
|
||||||
|
this.blockZ = blockZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int surfaceY(int localX, int localZ) {
|
||||||
|
return column(localX, localZ).surfaceY();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String surfaceBiome(int localX, int localZ) {
|
||||||
|
return column(localX, localZ).biome();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean open(int localX, int y, int localZ) {
|
||||||
|
return kind(localX, y, localZ) == OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean fluid(int localX, int y, int localZ) {
|
||||||
|
byte kind = kind(localX, y, localZ);
|
||||||
|
return kind == FLUID || kind == LAVA;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean lava(int localX, int y, int localZ) {
|
||||||
|
return kind(localX, y, localZ) == LAVA;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String block(int localX, int y, int localZ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean terrainMass(int localX, int y, int localZ) {
|
||||||
|
if (y < minY || y >= maxY) {
|
||||||
|
throw new IllegalArgumentException("Y " + y + " is outside [" + minY + ", " + maxY + ")");
|
||||||
|
}
|
||||||
|
if (localX < 0 || localX >= CHUNK_WIDTH || localZ < 0 || localZ >= CHUNK_WIDTH) {
|
||||||
|
throw new IllegalArgumentException("Column " + localX + "," + localZ + " is outside the chunk");
|
||||||
|
}
|
||||||
|
if (masses == null) {
|
||||||
|
masses = sampler.densityChunk(massDensity, Math.floorDiv(blockX, CHUNK_WIDTH), Math.floorDiv(blockZ, CHUNK_WIDTH));
|
||||||
|
}
|
||||||
|
return masses[(localZ * CHUNK_WIDTH + localX) * (maxY - minY) + y - minY] > 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wallsKnown() {
|
||||||
|
return walls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wall(int localX, int y, int localZ) {
|
||||||
|
return walls && sampler.density(wallShell, blockX + localX, y, blockZ + localZ) > 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte kind(int localX, int y, int localZ) {
|
||||||
|
if (y < minY || y >= maxY) {
|
||||||
|
throw new IllegalArgumentException("Y " + y + " is outside [" + minY + ", " + maxY + ")");
|
||||||
|
}
|
||||||
|
return column(localX, localZ).kinds()[y - minY];
|
||||||
|
}
|
||||||
|
|
||||||
|
private Column column(int localX, int localZ) {
|
||||||
|
if (localX < 0 || localX >= CHUNK_WIDTH || localZ < 0 || localZ >= CHUNK_WIDTH) {
|
||||||
|
throw new IllegalArgumentException("Column " + localX + "," + localZ + " is outside the chunk");
|
||||||
|
}
|
||||||
|
int index = localZ * CHUNK_WIDTH + localX;
|
||||||
|
Column column = columns[index];
|
||||||
|
if (column == null) {
|
||||||
|
column = sampleColumn(localX, localZ);
|
||||||
|
columns[index] = column;
|
||||||
|
}
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Column sampleColumn(int localX, int localZ) {
|
||||||
|
byte[] column = new byte[maxY - minY];
|
||||||
|
int surfaceY = minY - 1;
|
||||||
|
for (int y = maxY - 1; y >= minY; y--) {
|
||||||
|
LevelChunkSection section = terrain.getSection(terrain.getSectionIndex(y));
|
||||||
|
byte kind = kinds.computeIfAbsent(section.getBlockState(localX, y & (CHUNK_WIDTH - 1), localZ).getBlock(), VanillaNoiseFidelitySource::classify);
|
||||||
|
column[y - minY] = kind;
|
||||||
|
if (kind == SOLID && surfaceY < minY) {
|
||||||
|
surfaceY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Column(surfaceY, loadKey(blockX + localX, Math.max(minY, surfaceY), blockZ + localZ), column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.packs.PackResources;
|
||||||
|
import net.minecraft.server.packs.PackType;
|
||||||
|
import net.minecraft.server.packs.repository.ServerPacksSource;
|
||||||
|
import net.minecraft.server.packs.resources.IoSupplier;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class BukkitVanillaDataTest {
|
||||||
|
private static final String PLAINS = "data/minecraft/worldgen/biome/plains.json";
|
||||||
|
private static final String BIOME_TAGS = "data/minecraft/tags/worldgen/biome";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrap() {
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bukkitPlatformReadsTheServerPlainsBiome() throws IOException {
|
||||||
|
byte[] plains = new BukkitPlatform().vanillaData().read(PLAINS).orElseThrow();
|
||||||
|
assertTrue(new String(plains, StandardCharsets.UTF_8).contains("\"temperature\""));
|
||||||
|
try (PackResources vanilla = ServerPacksSource.createVanillaPackSource().fullResources()) {
|
||||||
|
IoSupplier<InputStream> resource = vanilla.getResource(PackType.SERVER_DATA,
|
||||||
|
Identifier.withDefaultNamespace("worldgen/biome/plains.json"));
|
||||||
|
try (InputStream input = resource.get()) {
|
||||||
|
assertArrayEquals(input.readAllBytes(), plains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNull(Bukkit.getServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bukkitPlatformListsTheBiomeTagsTheGameLoads() {
|
||||||
|
List<String> expected = new ArrayList<>();
|
||||||
|
try (PackResources vanilla = ServerPacksSource.createVanillaPackSource().fullResources()) {
|
||||||
|
vanilla.listResources(PackType.SERVER_DATA, "minecraft", "tags/worldgen/biome",
|
||||||
|
(id, resource) -> expected.add("data/" + id.getNamespace() + "/" + id.getPath()));
|
||||||
|
}
|
||||||
|
Collections.sort(expected);
|
||||||
|
List<String> listed = new BukkitPlatform().vanillaData().list(BIOME_TAGS);
|
||||||
|
assertFalse(listed.isEmpty());
|
||||||
|
assertTrue(listed.contains(BIOME_TAGS + "/is_overworld.json"));
|
||||||
|
assertEquals(expected, listed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void headlessPlatformReadsTheSameVanillaData() {
|
||||||
|
HeadlessNativePlatform platform = new HeadlessNativePlatform(new File("unused-native-platform"),
|
||||||
|
HeadlessNativeTestRegistries::get);
|
||||||
|
VanillaDataSource bukkit = new BukkitPlatform().vanillaData();
|
||||||
|
assertArrayEquals(bukkit.read(PLAINS).orElseThrow(), platform.vanillaData().read(PLAINS).orElseThrow());
|
||||||
|
assertEquals(bukkit.list(BIOME_TAGS), platform.vanillaData().list(BIOME_TAGS));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.CachingDensitySampler;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensitySampler;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.ConditionRule;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.OreVeinRule;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.SequenceRule;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
final class CompiledCaches {
|
||||||
|
private static final String MINECRAFT_PACKAGE = "net.minecraft.";
|
||||||
|
private static final String DENSITY_PACKAGE = "net.minecraft.world.level.levelgen.densityfunction.";
|
||||||
|
private static final String SPLINE_PACKAGE = "net.minecraft.util.";
|
||||||
|
private static final String SPLINE_NAME = "Spline";
|
||||||
|
|
||||||
|
private final RegistryAccess registries;
|
||||||
|
private final NoiseGeneratorSettings settings;
|
||||||
|
private final RandomState state;
|
||||||
|
|
||||||
|
private CompiledCaches(RegistryAccess registries, NoiseGeneratorSettings settings, long seed) {
|
||||||
|
this.registries = registries;
|
||||||
|
this.settings = settings;
|
||||||
|
this.state = RandomState.create(registries.lookupOrThrow(Registries.NOISE), seed, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompiledCaches open(RegistryAccess registries, long seed) {
|
||||||
|
if (!(DatapackCostProbe.exportedOverworld(registries).generator() instanceof NoiseBasedChunkGenerator generator)) {
|
||||||
|
throw new IllegalStateException("Exported overworld has no noise generator");
|
||||||
|
}
|
||||||
|
return new CompiledCaches(registries, generator.generatorSettings().value(), seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
DensityFunction finalDensity() {
|
||||||
|
return settings.noiseRouter().finalDensity();
|
||||||
|
}
|
||||||
|
|
||||||
|
DensityFunction function(String id) {
|
||||||
|
return registries.lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(id))).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OreVeinRule> oreVeins() {
|
||||||
|
List<OreVeinRule> veins = new ArrayList<>();
|
||||||
|
Deque<MaterialRule> pending = new ArrayDeque<>();
|
||||||
|
pending.push(settings.materialRule().value());
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
MaterialRule rule = pending.pop();
|
||||||
|
switch (rule) {
|
||||||
|
case OreVeinRule vein -> veins.add(vein);
|
||||||
|
case SequenceRule sequence -> sequence.sequence().reversed().forEach(pending::push);
|
||||||
|
case ConditionRule condition -> pending.push(condition.thenRun());
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return veins;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rootId(DensityFunction function) {
|
||||||
|
if (!(state.getSampler(function) instanceof CachingDensitySampler cache)) {
|
||||||
|
throw new IllegalStateException("Density function " + function + " does not compile to a cache");
|
||||||
|
}
|
||||||
|
return cache.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Integer> ids(DensityFunction function) {
|
||||||
|
Set<Integer> ids = new TreeSet<>();
|
||||||
|
Set<Object> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
Deque<Object> pending = new ArrayDeque<>();
|
||||||
|
pending.push(state.getSampler(function));
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
Object node = pending.pop();
|
||||||
|
if (node == null || !visited.add(node)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node instanceof CachingDensitySampler cache) {
|
||||||
|
ids.add(cache.id());
|
||||||
|
}
|
||||||
|
for (Object child : children(node)) {
|
||||||
|
pending.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> children(Object node) {
|
||||||
|
List<Object> children = new ArrayList<>();
|
||||||
|
if (node instanceof Object[] array) {
|
||||||
|
Collections.addAll(children, array);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (node instanceof Collection<?> collection) {
|
||||||
|
children.addAll(collection);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
if (!traversed(node)) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
for (Class<?> type = node.getClass(); type != null && type.getName().startsWith(MINECRAFT_PACKAGE); type = type.getSuperclass()) {
|
||||||
|
for (Field field : type.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(field.getModifiers()) || field.getType().isPrimitive()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
children.add(read(field, node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean traversed(Object node) {
|
||||||
|
if (node instanceof Enum<?>) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String name = node.getClass().getName();
|
||||||
|
return node instanceof DensitySampler || name.startsWith(DENSITY_PACKAGE) || name.startsWith(SPLINE_PACKAGE) && name.contains(SPLINE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object read(Field field, Object owner) {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(owner);
|
||||||
|
} catch (IllegalAccessException | RuntimeException failure) {
|
||||||
|
throw new IllegalStateException("Cannot read " + field + " of " + owner.getClass().getName(), failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityReport;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class DatapackFidelityProbeBiomeModeTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test(timeout = 300_000L)
|
||||||
|
public void biomeModeJudgesTheSeedMeanWithoutTheChunkScan() throws Exception {
|
||||||
|
Path pack = DatapackFidelityProbeTest.pack(temporary);
|
||||||
|
Path output = temporary.getRoot().toPath().resolve("reports").resolve("biomes.json");
|
||||||
|
|
||||||
|
FidelityReport report = DatapackFidelityProbe.execute(
|
||||||
|
new String[]{pack.toString(), "main", "1337", "0", "1", output.toString(), "256", "32", "3", "biomes"}, DatapackFidelityProbeTest.exporter());
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
assertTrue(report.toTable(), report.passed());
|
||||||
|
assertTrue(json.get("passed").getAsBoolean());
|
||||||
|
assertEquals(1.0D, DatapackFidelityProbeTest.metric(report, "biome.share.plains").export(), 0.0D);
|
||||||
|
assertEquals(1, report.sampling().size());
|
||||||
|
assertEquals(List.of(1337L, 1338L, 1339L), report.sampling().getFirst().seeds());
|
||||||
|
assertEquals(3, json.getAsJsonArray("seeds").size());
|
||||||
|
assertEquals(3, json.getAsJsonObject("seedsPassing").getAsJsonObject("biome.share.plains").get("passing").getAsInt());
|
||||||
|
assertTrue(report.observations().stream().anyMatch(observation -> observation.name().equals("biome.type.land")
|
||||||
|
&& observation.iris() == 1.0D && observation.export() == 1.0D));
|
||||||
|
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.DimensionStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.ImageMapStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityMetric;
|
||||||
|
import art.arcane.iris.probe.fidelity.FidelityReport;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class DatapackFidelityProbeTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test(timeout = 300_000L)
|
||||||
|
public void probeExportsTheSyntheticPackAndComparesBothGenerators() throws Exception {
|
||||||
|
Path pack = pack(temporary);
|
||||||
|
Path output = temporary.getRoot().toPath().resolve("reports").resolve("fidelity.json");
|
||||||
|
|
||||||
|
FidelityReport report = DatapackFidelityProbe.execute(
|
||||||
|
new String[]{pack.toString(), "main", "1337", "1", "8", output.toString(), "256", "32", "2", "full"}, exporter());
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
assertTrue(report.toTable(), report.passed());
|
||||||
|
assertTrue(json.get("passed").getAsBoolean());
|
||||||
|
assertEquals(report.metrics().size(), json.getAsJsonArray("metrics").size());
|
||||||
|
FidelityMetric biome = metric(report, "biome.share.plains");
|
||||||
|
assertEquals(1.0D, biome.iris(), 0.0D);
|
||||||
|
assertEquals(1.0D, biome.export(), 0.0D);
|
||||||
|
assertTrue(biome.pass());
|
||||||
|
assertEquals(0.0D, metric(report, "biome.totalVariation").delta(), 0.0D);
|
||||||
|
assertEquals(1.0D, metric(report, "height.landFraction").export(), 0.0D);
|
||||||
|
JsonObject columns = json.getAsJsonArray("sampling").get(0).getAsJsonObject();
|
||||||
|
assertEquals("columns", columns.get("scope").getAsString());
|
||||||
|
assertEquals(256, columns.get("windowBlocks").getAsInt());
|
||||||
|
assertEquals(32, columns.get("strideBlocks").getAsInt());
|
||||||
|
assertEquals(2, columns.getAsJsonArray("seeds").size());
|
||||||
|
assertEquals(128L, columns.get("columns").getAsLong());
|
||||||
|
JsonObject chunks = json.getAsJsonArray("sampling").get(1).getAsJsonObject();
|
||||||
|
assertEquals("chunks", chunks.get("scope").getAsString());
|
||||||
|
assertEquals(48, chunks.get("windowBlocks").getAsInt());
|
||||||
|
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void malformedArgumentsAreRejectedBeforeThePackOpens() throws Exception {
|
||||||
|
Path pack = pack(temporary);
|
||||||
|
String output = temporary.getRoot().toPath().resolve("unused.json").toString();
|
||||||
|
|
||||||
|
IllegalArgumentException missing = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main"}, exporter()));
|
||||||
|
IllegalArgumentException stride = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "17", output, "256", "32", "1", "full"}, exporter()));
|
||||||
|
IllegalArgumentException survey = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "8", output, "250", "32", "1", "full"}, exporter()));
|
||||||
|
IllegalArgumentException seeds = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "8", output, "256", "32", "0", "full"}, exporter()));
|
||||||
|
IllegalArgumentException mode = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> DatapackFidelityProbe.execute(new String[]{pack.toString(), "main", "1337", "1", "8", output, "256", "32", "1", "caves"}, exporter()));
|
||||||
|
|
||||||
|
assertTrue(missing.getMessage(), missing.getMessage().startsWith("Usage:"));
|
||||||
|
assertTrue(stride.getMessage(), stride.getMessage().contains("stride"));
|
||||||
|
assertTrue(survey.getMessage(), survey.getMessage().contains("multiple"));
|
||||||
|
assertTrue(seeds.getMessage(), seeds.getMessage().contains("surveySeeds"));
|
||||||
|
assertTrue(mode.getMessage(), mode.getMessage().contains("caves"));
|
||||||
|
assertFalse(Files.exists(Path.of(output)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static WorldgenDatapackExporter exporter() {
|
||||||
|
return new WorldgenDatapackExporter(WorldgenJsonWriter::forTarget, new PackCompiler(List.of(new DimensionStage(), new ImageMapStage())));
|
||||||
|
}
|
||||||
|
|
||||||
|
static FidelityMetric metric(FidelityReport report, String name) {
|
||||||
|
for (FidelityMetric metric : report.metrics()) {
|
||||||
|
if (metric.name().equals(name)) {
|
||||||
|
return metric;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Report has no metric " + name + ": " + report.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Path pack(TemporaryFolder temporary) throws Exception {
|
||||||
|
Path pack = temporary.newFolder().toPath();
|
||||||
|
for (String directory : List.of("dimensions", "regions", "biomes", "generators")) {
|
||||||
|
Files.createDirectories(pack.resolve(directory));
|
||||||
|
}
|
||||||
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
|
{"name":"Fidelity","focus":"plains","regions":["owned"],"landChance":1,"carvingEnabled":false}
|
||||||
|
""");
|
||||||
|
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"plains\"]}");
|
||||||
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
Files.writeString(pack.resolve("biomes/plains.json"), """
|
||||||
|
{"derivative":"minecraft:plains","generators":[{"generator":"flat","min":0,"max":0}]}
|
||||||
|
""");
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public final class DensityCostAnalysisTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final double EPSILON = 1.0E-9;
|
||||||
|
private static final String LEAF = "{\"type\":\"noise\",\"noise\":\"minecraft:temperature\",\"xz_scale\":1.0,\"y_scale\":0.0}";
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries registries;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadGraphs() throws IOException {
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
definitions.put(path("leaf"), LEAF);
|
||||||
|
definitions.put(path("pair"), "{\"type\":\"add\",\"left\":\"t:leaf\",\"right\":\"t:leaf\"}");
|
||||||
|
definitions.put(path("quad"), "{\"type\":\"mul\",\"left\":\"t:pair\",\"right\":\"t:pair\"}");
|
||||||
|
definitions.put(path("cached"), "{\"type\":\"cache\",\"input\":\"t:pair\"}");
|
||||||
|
definitions.put(path("twice"), "{\"type\":\"add\",\"left\":\"t:cached\",\"right\":\"t:cached\"}");
|
||||||
|
definitions.put(path("select"), "{\"type\":\"interval_select\",\"input\":\"t:leaf\",\"thresholds\":[0.0],\"functions\":[\"t:quad\",\"t:pair\"]}");
|
||||||
|
definitions.put(path("spline"), "{\"type\":\"spline\",\"spline\":{\"coordinate\":\"t:leaf\",\"points\":["
|
||||||
|
+ "{\"location\":-1.0,\"value\":" + nested("t:quad") + ",\"derivative\":0.0},"
|
||||||
|
+ "{\"location\":0.0,\"value\":0.0,\"derivative\":0.0},"
|
||||||
|
+ "{\"location\":1.0,\"value\":" + nested("t:quad") + ",\"derivative\":0.0}]}}");
|
||||||
|
definitions.put(path("grid"), "{\"type\":\"interpolated\",\"input\":\"t:pair\",\"cell_size_xz\":4,\"cell_size_y\":8}");
|
||||||
|
registries = HeadlessNativeRegistries.load(definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeRegistries() throws IOException {
|
||||||
|
if (registries != null) {
|
||||||
|
registries.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void referencesExpandWhileTheDagSharesThem() {
|
||||||
|
DensityCostAnalysis.RootCost quad = analysis().analyze("quad", function("quad"));
|
||||||
|
assertEquals(3L, quad.dagNodes());
|
||||||
|
assertEquals(7.0, quad.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(7.0, quad.cachedNodes(), EPSILON);
|
||||||
|
assertEquals(2, quad.namedSubtrees());
|
||||||
|
assertEquals("t:leaf", quad.exclusive().getFirst().name());
|
||||||
|
assertEquals(4.0, quad.exclusive().getFirst().multiplicity(), EPSILON);
|
||||||
|
assertEquals(4.0, quad.exclusive().getFirst().contribution(), EPSILON);
|
||||||
|
assertEquals("t:pair", quad.inclusive().getFirst().name());
|
||||||
|
assertEquals(6.0, quad.inclusive().getFirst().inclusive(), EPSILON);
|
||||||
|
assertEquals(1.0 / 3.0, quad.dagShare().get("leaf"), EPSILON);
|
||||||
|
assertEquals(1.0 / 3.0, quad.dagShare().get("quad"), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cachesEvaluateTheirBodyOnceWhenEnabled() {
|
||||||
|
DensityCostAnalysis.RootCost twice = analysis().analyze("twice", function("twice"));
|
||||||
|
assertEquals(4L, twice.dagNodes());
|
||||||
|
assertEquals(9.0, twice.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(6.0, twice.cachedNodes(), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void uncachedPointEvaluationTakesOneSelectedBranch() {
|
||||||
|
DensityCostAnalysis analysis = analysis();
|
||||||
|
DensityCostAnalysis.RootCost select = analysis.analyze("select", function("select"));
|
||||||
|
assertEquals(12.0, select.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(9.0, select.uncachedPointWorst(), EPSILON);
|
||||||
|
DensityCostAnalysis.RootCost spline = analysis.analyze("spline", function("spline"));
|
||||||
|
assertEquals(19.0, spline.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(11.0, spline.uncachedPointWorst(), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void offLatticeGridPointsSampleEightCorners() {
|
||||||
|
DensityCostAnalysis.RootCost grid = analysis().analyze("grid", function("grid"));
|
||||||
|
assertEquals(4.0, grid.expandedNodes(), EPSILON);
|
||||||
|
assertEquals(25.0, grid.uncachedPointWorst(), EPSILON);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityCostAnalysis analysis() {
|
||||||
|
return new DensityCostAnalysis(id -> DensityJsonBreakdown.category("overworld", id), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityFunction function(String name) {
|
||||||
|
return registries.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse("t:" + name))).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nested(String coordinate) {
|
||||||
|
return "{\"coordinate\":\"" + coordinate + "\",\"points\":[{\"location\":0.0,\"value\":1.0,\"derivative\":0.0},"
|
||||||
|
+ "{\"location\":1.0,\"value\":2.0,\"derivative\":0.0}]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String path(String name) {
|
||||||
|
return "data/t/worldgen/density_function/" + name + ".json";
|
||||||
|
}
|
||||||
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public final class DensityJsonBreakdownTest {
|
||||||
|
private static final String TEMPERATURE = "{\"type\":\"add\",\"left\":\"x:biome/selector/0\",\"right\":1.0}";
|
||||||
|
private static final String SELECTOR = "{\"type\":\"mul\",\"left\":\"x:cell/cellular/a/value\",\"right\":2.0}";
|
||||||
|
private static final String CELL_A = "{\"type\":\"noise\",\"noise\":\"x:hash_a\",\"xz_scale\":0.5,\"y_scale\":0.0}";
|
||||||
|
private static final String CELL_B = "{\"type\":\"noise\",\"noise\":\"x:hash_b\",\"xz_scale\":0.25,\"y_scale\":0.0}";
|
||||||
|
private static final String FINAL_DENSITY = "{\"type\":\"min\",\"left\":\"x:cell/cellular/b/value\",\"right\":0.0}";
|
||||||
|
private static final String NOISE = "{\"firstOctave\":-1,\"amplitudes\":[1.0]}";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void categoriesFollowProducingCompilers() {
|
||||||
|
assertEquals("router", DensityJsonBreakdown.category("overworld", "x:overworld/final_density"));
|
||||||
|
assertEquals("cellular", DensityJsonBreakdown.category("overworld", "x:cell/cracks/abc/distance"));
|
||||||
|
assertEquals("biome selector", DensityJsonBreakdown.category("overworld", "x:biome/selector/12"));
|
||||||
|
assertEquals("biome decision", DensityJsonBreakdown.category("overworld", "x:biome/base_code"));
|
||||||
|
assertEquals("terrain height", DensityJsonBreakdown.category("overworld", "x:height"));
|
||||||
|
assertEquals("terrain bands", DensityJsonBreakdown.category("overworld", "x:terrain/plains/blend/3"));
|
||||||
|
assertEquals("noise style", DensityJsonBreakdown.category("overworld", "x:style/shared/ab12"));
|
||||||
|
assertEquals("cellular offset copies", DensityJsonBreakdown.category("overworld", "x:cell/cellular/abc/vx_0_0/offset_x4_z0"));
|
||||||
|
assertEquals("biome selector offset copies", DensityJsonBreakdown.category("overworld", "x:biome/selector/4/offset_x0_z4"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reportsBytesShapesAndCellularOwners() {
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
put(files, "density_function/overworld/temperature", TEMPERATURE);
|
||||||
|
put(files, "density_function/biome/selector/0", SELECTOR);
|
||||||
|
put(files, "density_function/cell/cellular/a/value", CELL_A);
|
||||||
|
put(files, "density_function/cell/cellular/b/value", CELL_B);
|
||||||
|
put(files, "density_function/overworld/final_density", FINAL_DENSITY);
|
||||||
|
put(files, "noise/hash_a", NOISE);
|
||||||
|
put(files, "noise/hash_b", NOISE);
|
||||||
|
|
||||||
|
DensityJsonBreakdown.Report report = DensityJsonBreakdown.analyze(files, "overworld").report();
|
||||||
|
|
||||||
|
assertEquals(5, report.files());
|
||||||
|
assertEquals(bytes(TEMPERATURE, SELECTOR, CELL_A, CELL_B, FINAL_DENSITY), report.bytes());
|
||||||
|
DensityJsonBreakdown.CategoryBytes cellular = category(report, "cellular");
|
||||||
|
assertEquals(2, cellular.files());
|
||||||
|
assertEquals(1, cellular.distinctShapes());
|
||||||
|
assertEquals(bytes(CELL_B), cellular.structurallyDuplicateBytes());
|
||||||
|
assertEquals(2, category(report, "router").files());
|
||||||
|
assertEquals(2, report.cellularInstances());
|
||||||
|
assertEquals(1, report.cellularShapes().size());
|
||||||
|
assertEquals("biome selector+router", report.cellularShapes().getFirst().owners());
|
||||||
|
assertEquals(bytes(CELL_A), owner(report, "cellular via biome selector"));
|
||||||
|
assertEquals(bytes(CELL_B), owner(report, "cellular via router"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DensityJsonBreakdown.CategoryBytes category(DensityJsonBreakdown.Report report, String name) {
|
||||||
|
for (DensityJsonBreakdown.CategoryBytes category : report.categories()) {
|
||||||
|
if (category.category().equals(name)) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("No category " + name + " in " + report.categories());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long owner(DensityJsonBreakdown.Report report, String name) {
|
||||||
|
for (DensityJsonBreakdown.Bytes owner : report.libraryOwners()) {
|
||||||
|
if (owner.name().equals(name)) {
|
||||||
|
return owner.bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("No owner " + name + " in " + report.libraryOwners());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long bytes(String... bodies) {
|
||||||
|
long total = 0;
|
||||||
|
for (String body : bodies) {
|
||||||
|
total += body.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void put(SortedMap<String, byte[]> files, String path, String body) {
|
||||||
|
files.put("data/x/worldgen/" + path + ".json", body.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-19
@@ -1,37 +1,19 @@
|
|||||||
package art.arcane.iris.probe;
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
import io.papermc.paper.registry.PaperRegistryAccess;
|
|
||||||
import org.junit.rules.TestRule;
|
import org.junit.rules.TestRule;
|
||||||
import org.junit.runner.Description;
|
import org.junit.runner.Description;
|
||||||
import org.junit.runners.model.Statement;
|
import org.junit.runners.model.Statement;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public final class PaperRegistryIsolation implements TestRule {
|
public final class PaperRegistryIsolation implements TestRule {
|
||||||
@Override
|
@Override
|
||||||
public Statement apply(Statement base, Description description) {
|
public Statement apply(Statement base, Description description) {
|
||||||
return new Statement() {
|
return new Statement() {
|
||||||
@Override
|
@Override
|
||||||
public void evaluate() throws Throwable {
|
public void evaluate() throws Throwable {
|
||||||
Map<Object, Object> registries = registries();
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
Map<Object, Object> previous = new HashMap<>(registries);
|
|
||||||
registries.clear();
|
|
||||||
try {
|
|
||||||
base.evaluate();
|
base.evaluate();
|
||||||
} finally {
|
|
||||||
registries.clear();
|
|
||||||
registries.putAll(previous);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static Map<Object, Object> registries() throws ReflectiveOperationException {
|
|
||||||
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
|
|
||||||
field.setAccessible(true);
|
|
||||||
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.DensityFunctionEmitter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.FunctionTable;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ValueRange;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.VanillaBounds;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.Interval;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaBoundsParity263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final String NAMESPACE = "iris_export";
|
||||||
|
private static final List<NoiseDef> NOISES = List.of(new NoiseDef("parity/base", -3, List.of(1.0, 0.5, 0.25)),
|
||||||
|
new NoiseDef("parity/sparse", 0, List.of(0.0, 1.5, 0.0, 0.7)));
|
||||||
|
private static final Field BASE = new Field.Noise(NAMESPACE + ":parity/base", 0.25, 0.0, null, null, null);
|
||||||
|
private static final Field SPARSE = new Field.Noise(NAMESPACE + ":parity/sparse", 0.5, 0.25, null, null, null);
|
||||||
|
private static final Field UNIT = new Field.Clamp(BASE, 0.0, 1.0);
|
||||||
|
private static final Field BAND = new Field.Clamp(SPARSE, 0.3, 0.9);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void boundsMatchMinecraftForEveryEmittedNode() throws Exception {
|
||||||
|
List<Field> fields = fields();
|
||||||
|
VanillaBounds bounds = VanillaBounds.of(NAMESPACE, NOISES);
|
||||||
|
DensityFunctionEmitter emitter = DensityFunctionEmitter.forTarget(ExportCapabilities.forTarget(DataVersion.V26_3), NAMESPACE, bounds);
|
||||||
|
FunctionTable table = new FunctionTable(NAMESPACE);
|
||||||
|
List<String> references = new ArrayList<>(fields.size());
|
||||||
|
for (int index = 0; index < fields.size(); index++) {
|
||||||
|
JsonElement emitted = emitter.emit(fields.get(index), table);
|
||||||
|
boolean reference = emitted.isJsonPrimitive() && emitted.getAsJsonPrimitive().isString();
|
||||||
|
references.add(reference ? emitted.getAsString() : table.register("parity/field_" + index, emitted));
|
||||||
|
}
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, JsonElement> function : table.files().entrySet()) {
|
||||||
|
definitions.put("data/" + NAMESPACE + "/worldgen/density_function/" + function.getKey() + ".json", function.getValue().toString());
|
||||||
|
}
|
||||||
|
for (NoiseDef noise : NOISES) {
|
||||||
|
definitions.put("data/" + NAMESPACE + "/worldgen/noise/" + noise.id() + ".json", noiseJson(noise).toString());
|
||||||
|
}
|
||||||
|
try (HeadlessNativeRegistries registries = HeadlessNativeRegistries.load(definitions)) {
|
||||||
|
for (int index = 0; index < fields.size(); index++) {
|
||||||
|
Interval vanilla = registries.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(references.get(index)))).value().range();
|
||||||
|
Optional<ValueRange> modelled = bounds.range(fields.get(index));
|
||||||
|
assertTrue("field " + index + " " + fields.get(index) + " has no modelled range, Minecraft " + vanilla, modelled.isPresent());
|
||||||
|
assertEquals("field " + index + " " + fields.get(index), new ValueRange(vanilla.min(), vanilla.max()), modelled.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Field> fields() {
|
||||||
|
Field scaled = new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.3), BAND);
|
||||||
|
Field curve = new Field.Curve(new Field.Binary(Field.BinaryOp.MUL, BASE, new Field.Const(1.7)), List.of(
|
||||||
|
new Field.Knot(-1.0, new Field.Const(0.2), 0.75),
|
||||||
|
new Field.Knot(0.1, new Field.Curve(SPARSE, List.of(new Field.Knot(-0.5, new Field.Const(-1.0), 0.0),
|
||||||
|
new Field.Knot(0.5, new Field.Const(1.3), 2.0))), 0.0),
|
||||||
|
new Field.Knot(0.8, new Field.Const(0.6), -1.5)));
|
||||||
|
List<Field> fields = new ArrayList<>();
|
||||||
|
fields.add(BASE);
|
||||||
|
fields.add(SPARSE);
|
||||||
|
fields.add(new Field.Const(0.1));
|
||||||
|
fields.add(new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -64.0, 320.0, 1.5, -0.7));
|
||||||
|
fields.add(new Field.Clamp(BASE, -0.3, 0.8));
|
||||||
|
for (Field.BinaryOp op : Field.BinaryOp.values()) {
|
||||||
|
fields.add(new Field.Binary(op, BASE, new Field.Binary(Field.BinaryOp.ADD, BAND, new Field.Const(0.6))));
|
||||||
|
}
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.POW, UNIT, new Field.Const(2.5)));
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.MAX, scaled, new Field.Const(0.0)));
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.MIN, new Field.Const(2.0), scaled));
|
||||||
|
fields.add(new Field.Binary(Field.BinaryOp.MIN, new Field.Const(0.2), scaled));
|
||||||
|
for (Field.UnaryOp op : Field.UnaryOp.values()) {
|
||||||
|
fields.add(new Field.Unary(op, new Field.Binary(Field.BinaryOp.SUB, SPARSE, new Field.Const(0.4))));
|
||||||
|
}
|
||||||
|
fields.add(new Field.Lerp(UNIT, BASE, new Field.Binary(Field.BinaryOp.MUL, SPARSE, new Field.Const(3.0))));
|
||||||
|
for (Field.RoundMode mode : Field.RoundMode.values()) {
|
||||||
|
fields.add(new Field.Round(mode, new Field.Binary(Field.BinaryOp.MUL, BASE, new Field.Const(10.3)), new Field.Const(2.5)));
|
||||||
|
}
|
||||||
|
fields.add(curve);
|
||||||
|
fields.add(new Field.Curve(UNIT, List.of(new Field.Knot(0.0, new Field.Const(0.0), -3.0), new Field.Knot(1.0, new Field.Const(1.0), -3.0))));
|
||||||
|
fields.add(new Field.Partition(BASE, List.of(-0.2, 0.4), List.of(new Field.Const(-3.0), new Field.Const(0.5), new Field.Const(7.25))));
|
||||||
|
fields.add(new Field.Mux(new Field.Binary(Field.BinaryOp.MUL, UNIT, new Field.Const(2.0)), List.of(new Field.Const(4.0), BAND, scaled)));
|
||||||
|
fields.add(new Field.Gate(BASE, new Field.Binary(Field.BinaryOp.ADD, BAND, new Field.Const(40.0))));
|
||||||
|
fields.add(new Field.RangeIf(BASE, -0.1, 0.2, new Field.Const(-2.0), BAND));
|
||||||
|
fields.add(new Field.Grid(new Field.Binary(Field.BinaryOp.ADD, BASE, SPARSE), 4, 8));
|
||||||
|
fields.add(new Field.Shared("parity/shared", new Field.Binary(Field.BinaryOp.SUB, BAND, BASE)));
|
||||||
|
fields.add(new Field.Slice(new Field.Binary(Field.BinaryOp.MUL, SPARSE, new Field.Const(-2.0)), Axis.Y, 64));
|
||||||
|
fields.add(new Field.TopSurface(new Field.Binary(Field.BinaryOp.SUB, BASE, new Field.Const(0.1)), 200, -48, 4));
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject noiseJson(NoiseDef noise) {
|
||||||
|
JsonArray modifiers = new JsonArray(noise.amplitudes().size());
|
||||||
|
for (double amplitude : noise.amplitudes()) {
|
||||||
|
modifiers.add(amplitude);
|
||||||
|
}
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("base_octave", noise.firstOctave());
|
||||||
|
json.addProperty("octave_count", noise.amplitudes().size());
|
||||||
|
json.add("amplitude_modifiers", modifiers);
|
||||||
|
json.addProperty("base_amplitude", 1.0);
|
||||||
|
json.addProperty("normalize", false);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
+242
@@ -0,0 +1,242 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.VanillaNoiseModel;
|
||||||
|
import net.minecraft.core.HolderGetter;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.PositionalRandomFactory;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.synth.Noise;
|
||||||
|
import net.minecraft.world.level.levelgen.synth.NormalNoise;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaNoiseModelParity263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final int POINTS = 4096;
|
||||||
|
private static final double TOLERANCE = 1.0E-5D;
|
||||||
|
private static final double RUNTIME_TOLERANCE = 1.0E-4D;
|
||||||
|
private static final List<Long> WORLD_SEEDS = List.of(0L, 8_675_309L, -4_611_686_018_427_387_904L);
|
||||||
|
private static final List<ParityNoise> NOISES = List.of(
|
||||||
|
ParityNoise.mapped("iris_export:parity/single", -7, List.of(1.0D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/fbm", -9, List.of(1.0D, 1.0D, 0.5D, 0.25D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/sparse", -10, List.of(1.5D, 0.0D, 1.0D, 0.0D, 0.0D, 0.0D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/leading_zero", -6, List.of(0.0D, 0.0D, 1.0D, 2.0D)),
|
||||||
|
ParityNoise.mapped("iris_export:parity/positive", 2, List.of(1.0D, 0.5D)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/enabled",
|
||||||
|
"{\"base_octave\":-10,\"octave_count\":6,\"amplitude_modifiers\":[1.5,0.0,1.0,0.0,0.0,0.0],"
|
||||||
|
+ "\"base_amplitude\":1.2453007926713473}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-10, 6, List.of(1.5D, 0.0D, 1.0D, 0.0D, 0.0D, 0.0D),
|
||||||
|
1.2453007926713473D, VanillaNoiseModel.Normalization.ENABLED)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/legacy",
|
||||||
|
"{\"base_octave\":-9,\"octave_count\":3,\"amplitude_modifiers\":[1.0,0.0,1.0],\"normalize\":\"legacy\"}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-9, 3, List.of(1.0D, 0.0D, 1.0D), 1.0D,
|
||||||
|
VanillaNoiseModel.Normalization.LEGACY)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/defaults", "{\"base_octave\":-5}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-5, 1, List.of(), 1.0D, VanillaNoiseModel.Normalization.ENABLED)),
|
||||||
|
ParityNoise.profiled("iris_export:parity/scaled",
|
||||||
|
"{\"base_octave\":-6,\"octave_count\":2,\"normalize\":false,\"base_amplitude\":0.5}",
|
||||||
|
new VanillaNoiseModel.OctaveProfile(-6, 2, List.of(), 0.5D, VanillaNoiseModel.Normalization.DISABLED)));
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries registries;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadNoiseDefinitions() throws IOException {
|
||||||
|
Map<String, String> definitions = new LinkedHashMap<>();
|
||||||
|
for (ParityNoise noise : NOISES) {
|
||||||
|
definitions.put(resourcePath(noise.id()), noise.json());
|
||||||
|
}
|
||||||
|
registries = HeadlessNativeRegistries.load(definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeRegistries() throws IOException {
|
||||||
|
if (registries != null) {
|
||||||
|
registries.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modelMatchesNoiseBuiltFromLoadedDefinitions() {
|
||||||
|
List<String> report = new ArrayList<>();
|
||||||
|
for (long seed : WORLD_SEEDS) {
|
||||||
|
PositionalRandomFactory world = new VanillaPrecisionRandom(new XoroshiroRandomSource(seed)).forkPositional();
|
||||||
|
for (ParityNoise noise : NOISES) {
|
||||||
|
NormalNoise definition = registries.registries().lookupOrThrow(Registries.NOISE).getOrThrow(key(noise.id())).value();
|
||||||
|
Noise real = definition.create(world.fromHashOf(Identifier.parse(noise.id())));
|
||||||
|
report.add(noise.id() + "@" + seed + "=" + assertParity(noise, seed, real, TOLERANCE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("26.3 vanilla-precision parity " + report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modelMatchesRuntimeRandomStateNoise() {
|
||||||
|
List<String> report = new ArrayList<>();
|
||||||
|
for (long seed : WORLD_SEEDS) {
|
||||||
|
RandomState state = randomState(seed);
|
||||||
|
for (ParityNoise noise : NOISES) {
|
||||||
|
Noise real = state.getOrCreateNoise(key(noise.id()));
|
||||||
|
report.add(noise.id() + "@" + seed + "=" + assertParity(noise, seed, real, RUNTIME_TOLERANCE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("26.3 runtime parity " + report);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double assertParity(ParityNoise noise, long seed, Noise real, double tolerance) {
|
||||||
|
VanillaNoiseModel model = noise.model(seed);
|
||||||
|
SplittableRandom random = new SplittableRandom(seed ^ noise.id().hashCode());
|
||||||
|
double worst = 0.0D;
|
||||||
|
double peak = 0.0D;
|
||||||
|
for (int index = 0; index < POINTS; index++) {
|
||||||
|
double range = random.nextInt(4) == 0 ? 30_000_000.0D : 100_000.0D;
|
||||||
|
double x = random.nextDouble(-range, range);
|
||||||
|
double y = random.nextDouble(-2_048.0D, 2_048.0D);
|
||||||
|
double z = random.nextDouble(-range, range);
|
||||||
|
double expected = real.get(x, y, z);
|
||||||
|
worst = Math.max(worst, Math.abs(expected - model.sample(x, y, z)));
|
||||||
|
peak = Math.max(peak, Math.abs(expected));
|
||||||
|
}
|
||||||
|
assertTrue(noise.id() + " seed " + seed + " differs by " + worst, worst <= tolerance);
|
||||||
|
assertTrue(noise.id() + " seed " + seed + " is flat", peak > 0.05D);
|
||||||
|
return worst;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RandomState randomState(long seed) {
|
||||||
|
HolderGetter<NormalNoise> noises = registries.registries().lookupOrThrow(Registries.NOISE);
|
||||||
|
NoiseGeneratorSettings settings = registries.registries().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(NoiseGeneratorSettings.OVERWORLD).value();
|
||||||
|
return RandomState.create(noises, seed, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceKey<NormalNoise> key(String id) {
|
||||||
|
return ResourceKey.create(Registries.NOISE, Identifier.parse(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String resourcePath(String id) {
|
||||||
|
Identifier identifier = Identifier.parse(id);
|
||||||
|
return "data/" + identifier.getNamespace() + "/worldgen/noise/" + identifier.getPath() + ".json";
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParityNoise(String id, String json, int firstOctave, List<Double> amplitudes,
|
||||||
|
VanillaNoiseModel.OctaveProfile profile) {
|
||||||
|
private static ParityNoise mapped(String id, int firstOctave, List<Double> amplitudes) {
|
||||||
|
String modifiers = amplitudes.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
String json = "{\"base_octave\":" + firstOctave + ",\"octave_count\":" + amplitudes.size()
|
||||||
|
+ ",\"amplitude_modifiers\":[" + modifiers + "],\"base_amplitude\":1.0,\"normalize\":false}";
|
||||||
|
return new ParityNoise(id, json, firstOctave, amplitudes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParityNoise profiled(String id, String json, VanillaNoiseModel.OctaveProfile profile) {
|
||||||
|
return new ParityNoise(id, json, 0, List.of(), profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private VanillaNoiseModel model(long seed) {
|
||||||
|
if (profile == null) {
|
||||||
|
return VanillaNoiseModel.create(id, firstOctave, amplitudes, seed, DataVersion.V26_3);
|
||||||
|
}
|
||||||
|
return VanillaNoiseModel.create(id, profile, seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class VanillaPrecisionRandom implements RandomSource {
|
||||||
|
private static final double DOUBLE_UNIT = 1.1102230246251565E-16D;
|
||||||
|
|
||||||
|
private final RandomSource source;
|
||||||
|
|
||||||
|
private VanillaPrecisionRandom(RandomSource source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource fork() {
|
||||||
|
return new VanillaPrecisionRandom(source.fork());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PositionalRandomFactory forkPositional() {
|
||||||
|
return new VanillaPrecisionPositional(source.forkPositional());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSeed(long seed) {
|
||||||
|
source.setSeed(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int nextInt() {
|
||||||
|
return source.nextInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return source.nextInt(bound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long nextLong() {
|
||||||
|
return source.nextLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean nextBoolean() {
|
||||||
|
return source.nextBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float nextFloat() {
|
||||||
|
return source.nextFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double nextDouble() {
|
||||||
|
return (source.nextLong() >>> 11) * DOUBLE_UNIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double nextGaussian() {
|
||||||
|
return source.nextGaussian();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record VanillaPrecisionPositional(PositionalRandomFactory factory) implements PositionalRandomFactory {
|
||||||
|
@Override
|
||||||
|
public RandomSource at(int x, int y, int z) {
|
||||||
|
return new VanillaPrecisionRandom(factory.at(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource fromHashOf(String name) {
|
||||||
|
return new VanillaPrecisionRandom(factory.fromHashOf(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomSource fromSeed(long seed) {
|
||||||
|
return new VanillaPrecisionRandom(factory.fromSeed(seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parityConfigString(StringBuilder builder) {
|
||||||
|
factory.parityConfigString(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+700
@@ -0,0 +1,700 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.AtomicDatapackPublisher;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportFormat;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.GenerationCost;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.surface.MaterialProgramCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.terrain.FieldMacros;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.terrain.HeightCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.NestingSplitter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.GenerationBudget;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.tags.WorldPresetTags;
|
||||||
|
import net.minecraft.util.Interval;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DfRewriteRule;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.op.BinaryFunction;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.OreVeinRule;
|
||||||
|
import net.minecraft.world.level.levelgen.presets.WorldPreset;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.zip.GZIPInputStream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExport263CodecTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static final TemporaryFolder overworldFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
|
||||||
|
private static final String HEIGHT_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION + "/height";
|
||||||
|
private static final double SURFACE_TOLERANCE = 2.0;
|
||||||
|
private static final String BIOME_PREFIX = "data/" + ExportIrFixtures.NAMESPACE + "/worldgen/biome/";
|
||||||
|
private static final int[] SAMPLE_Y = {-64, 0, 64, 200};
|
||||||
|
private static final int LATTICE = 16;
|
||||||
|
private static final int LATTICE_STEP = 61;
|
||||||
|
private static final int HEIGHT_POINTS = 8;
|
||||||
|
private static final int HEIGHT_STEP = 97;
|
||||||
|
private static final String PROBE_PACK = "iris.exportProbePack";
|
||||||
|
private static final String OVERWORLD = "overworld";
|
||||||
|
private static final String OVERWORLD_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + OVERWORLD;
|
||||||
|
private static final String OVERWORLD_BIOME_PREFIX = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/worldgen/biome/";
|
||||||
|
private static final String LOG_PREFIX = "[worldgen-export]";
|
||||||
|
private static final int OVERWORLD_LATTICE = 4;
|
||||||
|
private static final int OVERWORLD_STEP = 509;
|
||||||
|
private static final int MAX_NESTING = 255;
|
||||||
|
private static final String CAVE_FIELD = "cave/field";
|
||||||
|
private static final String WALL_FALLBACK = "uses the nearest kept palette (minecraft:";
|
||||||
|
private static final double WALL_SHELL_BAND_LIMIT = 0.25;
|
||||||
|
private static final String DENSITY_FUNCTION_RESOURCE = "worldgen/density_function";
|
||||||
|
private static final double MEGABYTE = 1024.0 * 1024.0;
|
||||||
|
private static final int OVERWORLD_LAVA_Y = -248;
|
||||||
|
private static final int LAVA_LINE = -54;
|
||||||
|
private static final int DEEP_CHUNKS = 2;
|
||||||
|
private static final int DEEP_STRIDE = 4;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final float OPEN_MARGIN = 1.0e-3f;
|
||||||
|
private static final int LAVA_SEARCH_RADIUS = 1024;
|
||||||
|
private static final int LAVA_SEARCH_STEP = 16;
|
||||||
|
private static final int LAVA_CELLS = 4;
|
||||||
|
private static final int AGREEMENT_POINTS = 16;
|
||||||
|
private static final double AGREEMENT_TOLERANCE = 1.0;
|
||||||
|
private static final int AGREEMENT_OUTLIERS = 2;
|
||||||
|
private static final String SUBTERRAIN_STRUCTURES = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/worldgen/structure/subterrain/";
|
||||||
|
private static final String SUBTERRAIN_TEMPLATES = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/structure/subterrain/";
|
||||||
|
private static final int ROOM_GRID = 24;
|
||||||
|
private static final int ROOM_STEP_CHUNKS = 8;
|
||||||
|
private static final int ROOM_ATTEMPTS = 4;
|
||||||
|
private static final int ROOM_REACH = 128;
|
||||||
|
private static final Map<WorldgenExportMode, SortedMap<String, byte[]>> FILES = new EnumMap<>(WorldgenExportMode.class);
|
||||||
|
private static final Map<WorldgenExportMode, WorldgenExportSampler> SAMPLERS = new EnumMap<>(WorldgenExportMode.class);
|
||||||
|
private static SortedMap<String, byte[]> exportedOverworld;
|
||||||
|
private static WorldgenExportResult exportedOverworldResult;
|
||||||
|
private static ExportIr exportedOverworldIr;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadExports() throws IOException {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
SortedMap<String, byte[]> files = WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ExportIrFixtures.flatNoise(DataVersion.V26_3, mode));
|
||||||
|
FILES.put(mode, files);
|
||||||
|
SAMPLERS.put(mode, WorldgenExportSampler.open(files, mode, DIMENSION_ID, SEED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeExports() throws IOException {
|
||||||
|
for (WorldgenExportSampler sampler : SAMPLERS.values()) {
|
||||||
|
sampler.close();
|
||||||
|
}
|
||||||
|
SAMPLERS.clear();
|
||||||
|
FILES.clear();
|
||||||
|
exportedOverworld = null;
|
||||||
|
exportedOverworldResult = null;
|
||||||
|
exportedOverworldIr = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fixtureLoadsInEveryMode() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
assertEquals(ExportIrFixtures.MIN_Y, sampler.minY());
|
||||||
|
assertEquals(ExportIrFixtures.HEIGHT, sampler.height());
|
||||||
|
assertEquals(ExportIrFixtures.SEA_LEVEL, sampler.seaLevel());
|
||||||
|
assertFalse(sampler.densityFunctionIds().isEmpty());
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
assertTrue(mode + " " + id, sampler.registries().lookupOrThrow(Registries.DENSITY_FUNCTION).containsKey(Identifier.parse(id)));
|
||||||
|
}
|
||||||
|
assertTrue(sampler.registries().lookupOrThrow(Registries.NOISE_SETTINGS).containsKey(Identifier.parse(DIMENSION_ID)));
|
||||||
|
assertTrue(sampler.registries().lookupOrThrow(Registries.DIMENSION_TYPE).containsKey(Identifier.parse(DIMENSION_ID)));
|
||||||
|
assertTrue(sampler.registries().lookupOrThrow(Registries.MATERIAL_RULE).containsKey(Identifier.parse(DIMENSION_ID)));
|
||||||
|
boolean preset = sampler.registries().lookupOrThrow(Registries.WORLD_PRESET).containsKey(Identifier.parse(DIMENSION_ID));
|
||||||
|
boolean dimension = sampler.registries().lookupOrThrow(Registries.LEVEL_STEM).containsKey(Identifier.parse(DIMENSION_ID));
|
||||||
|
boolean overworld = sampler.registries().lookupOrThrow(Registries.LEVEL_STEM).containsKey(LevelStem.OVERWORLD.identifier());
|
||||||
|
assertEquals(mode.toString(), mode == WorldgenExportMode.PRESET, preset);
|
||||||
|
assertEquals(mode.toString(), mode == WorldgenExportMode.DIMENSION, dimension);
|
||||||
|
assertEquals(mode.toString(), mode == WorldgenExportMode.OVERWORLD, overworld);
|
||||||
|
}
|
||||||
|
WorldgenExportSampler presetSampler = SAMPLERS.get(WorldgenExportMode.PRESET);
|
||||||
|
Holder<WorldPreset> preset = presetSampler.registries().lookupOrThrow(Registries.WORLD_PRESET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.WORLD_PRESET, Identifier.parse(DIMENSION_ID)));
|
||||||
|
assertTrue(preset.value().createWorldDimensions().get(LevelStem.NETHER).isPresent());
|
||||||
|
assertTrue(preset.value().createWorldDimensions().get(LevelStem.END).isPresent());
|
||||||
|
HolderSet.Named<WorldPreset> normal = presetSampler.registries().lookupOrThrow(Registries.WORLD_PRESET).getOrThrow(WorldPresetTags.NORMAL);
|
||||||
|
assertTrue(normal.contains(preset));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyDensityFunctionEvaluatesFinite() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int z = (zIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
float value = sampler.density(id, x, y, z);
|
||||||
|
assertTrue(mode + " " + id + " at " + x + "," + y + "," + z + " = " + value, Float.isFinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void baseHeightWithinDimension() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
Set<Integer> heights = new HashSet<>();
|
||||||
|
int land = 0;
|
||||||
|
for (int xIndex = 0; xIndex < HEIGHT_POINTS; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < HEIGHT_POINTS; zIndex++) {
|
||||||
|
int x = (xIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int z = (zIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int height = sampler.baseHeight(x, z);
|
||||||
|
assertTrue(mode + " height " + height + " at " + x + "," + z,
|
||||||
|
height >= sampler.minY() && height <= sampler.minY() + sampler.height());
|
||||||
|
float surface = sampler.density(HEIGHT_ID, x, 0, z);
|
||||||
|
int floor = sampler.floorHeight(x, z);
|
||||||
|
assertTrue(mode + " height " + height + " below floor " + floor, height >= floor);
|
||||||
|
if (surface > sampler.seaLevel() + 1) {
|
||||||
|
assertTrue(mode + " floor " + floor + " against surface " + surface, Math.abs(floor - surface) <= SURFACE_TOLERANCE);
|
||||||
|
land++;
|
||||||
|
}
|
||||||
|
heights.add(height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(mode + " surface never varies", heights.size() > 1);
|
||||||
|
assertTrue(mode + " has no land column above sea level", land > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolverReturnsOnlyExportedBiomes() {
|
||||||
|
for (WorldgenExportMode mode : WorldgenExportMode.values()) {
|
||||||
|
WorldgenExportSampler sampler = SAMPLERS.get(mode);
|
||||||
|
Set<String> exported = exportedBiomes(FILES.get(mode), BIOME_PREFIX, ExportIrFixtures.NAMESPACE);
|
||||||
|
assertFalse(exported.isEmpty());
|
||||||
|
assertTrue(mode + " " + sampler.possibleBiomes(), exported.containsAll(sampler.possibleBiomes()));
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
String biome = sampler.biome((xIndex - LATTICE / 2) * LATTICE_STEP, y, (zIndex - LATTICE / 2) * LATTICE_STEP);
|
||||||
|
assertTrue(mode + " " + biome, exported.contains(biome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldPackLoads() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
assertGenerationBudget(exportedOverworldResult);
|
||||||
|
assertWallPalettesFallBack(exportedOverworldResult);
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().endsWith(".json")) {
|
||||||
|
int depth = NestingSplitter.depth(JsonParser.parseString(new String(file.getValue(), StandardCharsets.UTF_8)));
|
||||||
|
assertTrue(file.getKey() + " nests " + depth, depth <= MAX_NESTING);
|
||||||
|
}
|
||||||
|
if (file.getKey().endsWith(".nbt")) {
|
||||||
|
assertCompleteGzip(file.getKey(), file.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
Set<String> roots = new TreeSet<>();
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
if (id.startsWith(OVERWORLD_ID + "/")) {
|
||||||
|
roots.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(roots.toString(), roots.contains(OVERWORLD_ID + "/final_density"));
|
||||||
|
for (String id : roots) {
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < OVERWORLD_LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < OVERWORLD_LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - OVERWORLD_LATTICE / 2) * OVERWORLD_STEP;
|
||||||
|
int z = (zIndex - OVERWORLD_LATTICE / 2) * OVERWORLD_STEP;
|
||||||
|
float value = sampler.density(id, x, y, z);
|
||||||
|
assertTrue(id + " at " + x + "," + y + "," + z + " = " + value, Float.isFinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNaturalHeightMatchesTheEvaluator(exportedOverworldIr, sampler);
|
||||||
|
Set<String> exported = exportedBiomes(files, OVERWORLD_BIOME_PREFIX, WorldgenExportRequest.DEFAULT_NAMESPACE);
|
||||||
|
assertFalse(exported.isEmpty());
|
||||||
|
assertTrue(sampler.possibleBiomes().toString(), exported.containsAll(sampler.possibleBiomes()));
|
||||||
|
for (int xIndex = 0; xIndex < HEIGHT_POINTS; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < HEIGHT_POINTS; zIndex++) {
|
||||||
|
int x = (xIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int z = (zIndex - HEIGHT_POINTS / 2) * HEIGHT_STEP;
|
||||||
|
int height = sampler.baseHeight(x, z);
|
||||||
|
assertTrue("height " + height + " at " + x + "," + z, height >= sampler.minY() && height <= sampler.minY() + sampler.height());
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
String biome = sampler.biome(x, y, z);
|
||||||
|
assertTrue(biome + " at " + x + "," + y + "," + z, exported.contains(biome));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldHasNoDisjointMinMax() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
MinMaxAudit audit = new MinMaxAudit();
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
audit.rewrite(sampler.registries().lookupOrThrow(Registries.DENSITY_FUNCTION)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DENSITY_FUNCTION, Identifier.parse(id))).value());
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " min/max nodes " + audit.checked() + ", disjoint " + audit.disjoint().size());
|
||||||
|
assertTrue(audit.checked() > 0);
|
||||||
|
assertTrue(audit.disjoint().size() + " disjoint min/max nodes: " + audit.disjoint(), audit.disjoint().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldDeepCavesOpenWithoutLava() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
String field = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + CAVE_FIELD;
|
||||||
|
long open = 0L;
|
||||||
|
long lava = 0L;
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
assertTrue(field, sampler.densityFunctionIds().contains(field));
|
||||||
|
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||||
|
for (int chunkX = -DEEP_CHUNKS; chunkX < DEEP_CHUNKS; chunkX++) {
|
||||||
|
for (int chunkZ = -DEEP_CHUNKS; chunkZ < DEEP_CHUNKS; chunkZ++) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(chunkX, chunkZ);
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z++) {
|
||||||
|
for (int y = OVERWORLD_LAVA_Y + 1; y < LAVA_LINE; y++) {
|
||||||
|
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||||
|
assertFalse(state + " at chunk " + chunkX + "," + chunkZ + " " + x + "," + y + "," + z, state.is(Blocks.LAVA));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x += DEEP_STRIDE) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z += DEEP_STRIDE) {
|
||||||
|
int blockX = (chunkX << 4) + x;
|
||||||
|
int blockZ = (chunkZ << 4) + z;
|
||||||
|
for (int y = OVERWORLD_LAVA_Y + 1; y < LAVA_LINE; y++) {
|
||||||
|
if (sampler.density(field, blockX, y, blockZ) > -OPEN_MARGIN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockState state = chunk.getBlockState(position.set(x, y, z));
|
||||||
|
assertTrue(state + " at " + blockX + "," + y + "," + blockZ, state.isAir());
|
||||||
|
open++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BlockPos cell : openLavaBandCells(sampler, field)) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(cell.getX() >> 4, cell.getZ() >> 4);
|
||||||
|
BlockState state = chunk.getBlockState(position.set(cell.getX() & (CHUNK_WIDTH - 1), cell.getY(), cell.getZ() & (CHUNK_WIDTH - 1)));
|
||||||
|
assertTrue(state + " at " + cell, state.is(Blocks.LAVA));
|
||||||
|
lava++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " deep cave cells open " + open + ", lava band cells " + lava);
|
||||||
|
assertTrue("no deep cave cell opened", open > 0L);
|
||||||
|
assertTrue("no lava band cell opened", lava > 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldMaterialPassComputesTheCaveFieldOnce() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
CompiledCaches caches = CompiledCaches.open(sampler.registries(), SEED);
|
||||||
|
int field = caches.rootId(caches.function(WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + CAVE_FIELD));
|
||||||
|
List<OreVeinRule> veins = caches.oreVeins();
|
||||||
|
DensityFunction opening = veins.getFirst().density();
|
||||||
|
assertTrue(caches.ids(opening).contains(field));
|
||||||
|
assertTrue(caches.ids(caches.finalDensity()).contains(field));
|
||||||
|
Set<Integer> caveCaches = caches.ids(caches.function(WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + CAVE_FIELD));
|
||||||
|
int shells = 0;
|
||||||
|
for (OreVeinRule vein : veins) {
|
||||||
|
if (!vein.oreBlock().isAir()) {
|
||||||
|
assertTrue("wall shell " + shells + " " + vein.oreBlock(), caches.ids(vein.density()).contains(field));
|
||||||
|
assertTrue("wall shell " + shells + " " + vein.oreBlock(), caveCaches.containsAll(caches.ids(vein.density())));
|
||||||
|
shells++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JsonObject shell = JsonParser.parseString(new String(files.get("data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/" + DENSITY_FUNCTION_RESOURCE
|
||||||
|
+ "/" + MaterialProgramCompiler.WALL_SHELL + ".json"), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||||
|
double band = shell.get("max_exclusive").getAsDouble();
|
||||||
|
System.out.println(LOG_PREFIX + " material pass: " + veins.size() + " ore vein rules, " + shells + " wall shells read cave field cache " + field
|
||||||
|
+ " within band " + band);
|
||||||
|
assertTrue(String.valueOf(shells), shells > 0);
|
||||||
|
assertTrue(String.valueOf(band), band > 0.0 && band < WALL_SHELL_BAND_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldSubterrainRoomsPlace() throws Exception {
|
||||||
|
SortedMap<String, byte[]> files = overworldFiles();
|
||||||
|
SortedMap<String, JsonObject> structures = new TreeMap<>();
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().startsWith(SUBTERRAIN_STRUCTURES) && file.getKey().endsWith(".json")) {
|
||||||
|
String path = file.getKey().substring(SUBTERRAIN_STRUCTURES.length(), file.getKey().length() - ".json".length());
|
||||||
|
structures.put(WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/" + path,
|
||||||
|
JsonParser.parseString(new String(file.getValue(), StandardCharsets.UTF_8)).getAsJsonObject());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long templateBytes = 0L;
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
if (file.getKey().startsWith(SUBTERRAIN_TEMPLATES)) {
|
||||||
|
templateBytes += file.getValue().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " subterrain structures " + structures.size() + ", structure sets "
|
||||||
|
+ count(files, "/worldgen/structure_set/subterrain/", ".json") + ", templates " + count(files, "/structure/subterrain/", ".nbt")
|
||||||
|
+ ", template bytes " + templateBytes);
|
||||||
|
assertFalse(structures.isEmpty());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(files, WorldgenExportMode.OVERWORLD, OVERWORLD_ID, SEED)) {
|
||||||
|
for (Map.Entry<String, JsonObject> entry : structures.entrySet()) {
|
||||||
|
Structure structure = sampler.registries().lookupOrThrow(Registries.STRUCTURE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE, Identifier.parse(entry.getKey()))).value();
|
||||||
|
JsonObject height = entry.getValue().getAsJsonObject("start_height");
|
||||||
|
int y = (height.getAsJsonObject("min_inclusive").get("absolute").getAsInt()
|
||||||
|
+ height.getAsJsonObject("max_inclusive").get("absolute").getAsInt()) / 2;
|
||||||
|
List<ChunkPos> matches = new ArrayList<>();
|
||||||
|
for (int xIndex = 0; xIndex < ROOM_GRID; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < ROOM_GRID; zIndex++) {
|
||||||
|
ChunkPos chunk = new ChunkPos((xIndex - ROOM_GRID / 2) * ROOM_STEP_CHUNKS, (zIndex - ROOM_GRID / 2) * ROOM_STEP_CHUNKS);
|
||||||
|
String biome = sampler.biome(chunk.getMinBlockX(), y, chunk.getMinBlockZ());
|
||||||
|
Holder<Biome> holder = sampler.registries().lookupOrThrow(Registries.BIOME)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse(biome)));
|
||||||
|
if (structure.biomes().contains(holder)) {
|
||||||
|
matches.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(String.format(Locale.ROOT, "%s subterrain %s biome match %d/%d at y %d", LOG_PREFIX, entry.getKey(),
|
||||||
|
matches.size(), ROOM_GRID * ROOM_GRID, y));
|
||||||
|
assertFalse(entry.getKey() + " finds no biome at y " + y, matches.isEmpty());
|
||||||
|
StructureStart start = null;
|
||||||
|
for (ChunkPos chunk : matches.subList(0, Math.min(ROOM_ATTEMPTS, matches.size()))) {
|
||||||
|
StructureStart candidate = sampler.structureStart(entry.getKey(), chunk.x(), chunk.z());
|
||||||
|
if (candidate.isValid()) {
|
||||||
|
start = candidate;
|
||||||
|
BoundingBox box = start.getBoundingBox();
|
||||||
|
assertTrue(entry.getKey() + " " + box, chunk.getMinBlockX() - box.minX() <= ROOM_REACH && box.maxX() - chunk.getMinBlockX() <= ROOM_REACH);
|
||||||
|
assertTrue(entry.getKey() + " " + box, chunk.getMinBlockZ() - box.minZ() <= ROOM_REACH && box.maxZ() - chunk.getMinBlockZ() <= ROOM_REACH);
|
||||||
|
assertTrue(entry.getKey() + " " + box, box.minY() >= sampler.minY() && box.maxY() < sampler.minY() + sampler.height());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(entry.getKey() + " never starts", start != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BlockPos> openLavaBandCells(WorldgenExportSampler sampler, String field) {
|
||||||
|
List<BlockPos> cells = new ArrayList<>(LAVA_CELLS);
|
||||||
|
for (int x = -LAVA_SEARCH_RADIUS; x < LAVA_SEARCH_RADIUS; x += LAVA_SEARCH_STEP) {
|
||||||
|
for (int z = -LAVA_SEARCH_RADIUS; z < LAVA_SEARCH_RADIUS; z += LAVA_SEARCH_STEP) {
|
||||||
|
for (int y = sampler.minY(); y <= OVERWORLD_LAVA_Y; y++) {
|
||||||
|
if (sampler.density(field, x, y, z) > -OPEN_MARGIN) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cells.add(new BlockPos(x, y, z));
|
||||||
|
if (cells.size() == LAVA_CELLS) {
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized SortedMap<String, byte[]> overworldFiles() throws Exception {
|
||||||
|
String probePack = System.getProperty(PROBE_PACK);
|
||||||
|
Assume.assumeTrue("Set -P" + PROBE_PACK + " to export a real pack", probePack != null && !probePack.isBlank());
|
||||||
|
if (exportedOverworld != null) {
|
||||||
|
return exportedOverworld;
|
||||||
|
}
|
||||||
|
Path root = overworldFolder.newFolder("exports").toPath();
|
||||||
|
SortedMap<String, byte[]> untrimmed = new TreeMap<>();
|
||||||
|
AtomicReference<ExportIr> compiled = new AtomicReference<>();
|
||||||
|
WorldgenExportResult result;
|
||||||
|
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(
|
||||||
|
new File(probePack), OVERWORLD, LOG_PREFIX, new HeadlessNativeRuntime(), overworldFolder.newFolder("workspace").toPath()))) {
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(workspace.pack().toPath())
|
||||||
|
.dimension(OVERWORLD)
|
||||||
|
.exportRoot(root)
|
||||||
|
.objects(true)
|
||||||
|
.build();
|
||||||
|
result = new WorldgenDatapackExporter(target -> ir -> {
|
||||||
|
compiled.set(ir);
|
||||||
|
SortedMap<String, byte[]> written = WorldgenJsonWriter.forTarget(target).write(ir);
|
||||||
|
untrimmed.putAll(written);
|
||||||
|
return written;
|
||||||
|
}, PackCompiler.create()).export(request, WorldgenExportProgress.NONE);
|
||||||
|
}
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
SortedMap<String, byte[]> files = zipEntries(result.output());
|
||||||
|
Path untrimmedZip = root.resolve("untrimmed.zip");
|
||||||
|
new AtomicDatapackPublisher().publish(untrimmed, untrimmedZip, DatapackExportFormat.ZIP, false, ".untrimmed-");
|
||||||
|
report(result, untrimmed, Files.size(untrimmedZip), files);
|
||||||
|
exportedOverworld = files;
|
||||||
|
exportedOverworldResult = result;
|
||||||
|
exportedOverworldIr = compiled.get();
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertGenerationBudget(WorldgenExportResult result) {
|
||||||
|
GenerationCost cost = result.cost();
|
||||||
|
Set<String> flagged = new TreeSet<>();
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
if (diagnostic.limit() == ExportLimit.GENERATION_COST) {
|
||||||
|
flagged.add(diagnostic.resource());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (GenerationCost.Root root : GenerationCost.Root.values()) {
|
||||||
|
GenerationCost.RootCost rootCost = cost.root(root).orElseThrow();
|
||||||
|
System.out.println(String.format(Locale.ROOT, "%s cost %s worst case %.3e expanded %.3e distinct %d", LOG_PREFIX, root.id(),
|
||||||
|
rootCost.worstCaseNodes(), rootCost.expandedNodes(), rootCost.distinctNodes()));
|
||||||
|
}
|
||||||
|
for (GenerationCost.Root root : GenerationCost.Root.values()) {
|
||||||
|
if (!root.climate()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double limit = GenerationBudget.worstCaseLimit(root).orElseThrow();
|
||||||
|
double nodes = cost.root(root).orElseThrow().worstCaseNodes();
|
||||||
|
String resource = "noise_settings/" + OVERWORLD + "/" + root.id();
|
||||||
|
assertTrue(root.id() + " evaluates " + nodes + " nodes, budget " + limit, nodes <= limit);
|
||||||
|
assertFalse(resource + " " + flagged, flagged.contains(resource));
|
||||||
|
}
|
||||||
|
long finalDensity = cost.root(GenerationCost.Root.FINAL_DENSITY).orElseThrow().distinctNodes();
|
||||||
|
String finalResource = "noise_settings/" + OVERWORLD + "/" + GenerationCost.Root.FINAL_DENSITY.id();
|
||||||
|
System.out.println(String.format(Locale.ROOT, "%s final_density distinct %d of %d, density function JSON %.1f of %.1f MB", LOG_PREFIX, finalDensity,
|
||||||
|
GenerationBudget.FINAL_DENSITY_DISTINCT_NODES, cost.densityFunctionBytes() / MEGABYTE, GenerationBudget.DENSITY_FUNCTION_JSON_BYTES / MEGABYTE));
|
||||||
|
assertEquals(finalResource + " " + flagged, finalDensity > GenerationBudget.FINAL_DENSITY_DISTINCT_NODES, flagged.contains(finalResource));
|
||||||
|
assertTrue(cost.densityFunctionBytes() + " bytes", cost.densityFunctionBytes() > 0L);
|
||||||
|
assertTrue(cost.densityFunctionBytes() + " bytes", cost.densityFunctionBytes() <= GenerationBudget.DENSITY_FUNCTION_JSON_BYTES);
|
||||||
|
assertFalse(flagged.toString(), flagged.contains(DENSITY_FUNCTION_RESOURCE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertWallPalettesFallBack(WorldgenExportResult result) {
|
||||||
|
int fallbacks = 0;
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
if (diagnostic.limit() == ExportLimit.WALL_PALETTE && diagnostic.severity() == ExportDiagnostic.Severity.WARNING) {
|
||||||
|
assertTrue(diagnostic.resource() + " " + diagnostic.message(), diagnostic.message().contains(WALL_FALLBACK));
|
||||||
|
fallbacks++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " wall palettes over the ore vein budget " + fallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertNaturalHeightMatchesTheEvaluator(ExportIr ir, WorldgenExportSampler sampler) {
|
||||||
|
Field.Shared height = shared(ir.router().chunkSurfaceLevel(), HeightCompiler.HEIGHT);
|
||||||
|
Map<String, NoiseDef> noises = new HashMap<>();
|
||||||
|
for (NoiseDef noise : ir.noises()) {
|
||||||
|
noises.put(ir.namespace() + ":" + noise.id(), noise);
|
||||||
|
}
|
||||||
|
String heightId = ir.namespace() + ":" + HeightCompiler.HEIGHT;
|
||||||
|
int outliers = 0;
|
||||||
|
StringBuilder detail = new StringBuilder();
|
||||||
|
for (int xIndex = 0; xIndex < AGREEMENT_POINTS; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < AGREEMENT_POINTS; zIndex++) {
|
||||||
|
int x = (xIndex - AGREEMENT_POINTS / 2) * OVERWORLD_STEP;
|
||||||
|
int z = (zIndex - AGREEMENT_POINTS / 2) * OVERWORLD_STEP;
|
||||||
|
double evaluated = FieldEvaluator.sample(height.expr(), noises, SEED, DataVersion.V26_3, x, 0, z);
|
||||||
|
double exported = sampler.density(heightId, x, 0, z);
|
||||||
|
if (Math.abs(evaluated - exported) > AGREEMENT_TOLERANCE) {
|
||||||
|
outliers++;
|
||||||
|
detail.append(' ').append(x).append(',').append(z).append('=').append(evaluated).append('/').append(exported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(outliers + " columns differ:" + detail, outliers <= AGREEMENT_OUTLIERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field.Shared shared(Field root, String name) {
|
||||||
|
Set<Field> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
Deque<Field> pending = new ArrayDeque<>();
|
||||||
|
pending.push(root);
|
||||||
|
while (!pending.isEmpty()) {
|
||||||
|
Field field = pending.pop();
|
||||||
|
if (!visited.add(field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (field instanceof Field.Shared candidate && candidate.name().equals(name)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
for (Field child : FieldMacros.children(field)) {
|
||||||
|
pending.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("Router has no shared field " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertCompleteGzip(String path, byte[] bytes) throws IOException {
|
||||||
|
try (InputStream input = new GZIPInputStream(new ByteArrayInputStream(bytes))) {
|
||||||
|
input.readAllBytes();
|
||||||
|
assertEquals(path, -1, input.read());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void report(WorldgenExportResult result, SortedMap<String, byte[]> untrimmed, long untrimmedZipBytes,
|
||||||
|
SortedMap<String, byte[]> files) throws IOException {
|
||||||
|
SortedMap<ExportLimit, Integer> limits = new TreeMap<>();
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
limits.merge(diagnostic.limit(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
System.out.println(LOG_PREFIX + " untrimmed zip bytes " + untrimmedZipBytes + ", files " + untrimmed.size()
|
||||||
|
+ ", trimmed zip bytes " + Files.size(result.output()) + ", files " + files.size() + ", trim " + result.trim());
|
||||||
|
System.out.println(LOG_PREFIX + " diagnostics " + result.diagnostics().size() + " " + limits);
|
||||||
|
reportCounts("untrimmed", untrimmed);
|
||||||
|
reportCounts("trimmed", files);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportCounts(String label, SortedMap<String, byte[]> files) {
|
||||||
|
System.out.println(LOG_PREFIX + " " + label + " templates " + count(files, "/structure/", ".nbt")
|
||||||
|
+ ", structures " + count(files, "/worldgen/structure/", ".json")
|
||||||
|
+ ", structure sets " + count(files, "/worldgen/structure_set/", ".json")
|
||||||
|
+ ", placed features " + count(files, "/worldgen/placed_feature/", ".json")
|
||||||
|
+ ", features " + count(files, "/worldgen/feature/", ".json")
|
||||||
|
+ ", density functions " + count(files, "/worldgen/density_function/", ".json")
|
||||||
|
+ ", noises " + count(files, "/worldgen/noise/", ".json")
|
||||||
|
+ ", biomes " + count(files, "/worldgen/biome/", ".json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int count(SortedMap<String, byte[]> files, String folder, String suffix) {
|
||||||
|
int count = 0;
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
if (path.contains(folder) && path.endsWith(suffix)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> zipEntries(Path zip) throws IOException {
|
||||||
|
SortedMap<String, byte[]> files = new TreeMap<>();
|
||||||
|
try (ZipFile archive = new ZipFile(zip.toFile())) {
|
||||||
|
Enumeration<? extends ZipEntry> entries = archive.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
files.put(entry.getName(), archive.getInputStream(entry).readAllBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> exportedBiomes(SortedMap<String, byte[]> files, String prefix, String namespace) {
|
||||||
|
Set<String> biomes = new TreeSet<>();
|
||||||
|
for (String path : files.keySet()) {
|
||||||
|
if (path.startsWith(prefix)) {
|
||||||
|
biomes.add(namespace + ":" + path.substring(prefix.length(), path.length() - ".json".length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MinMaxAudit implements DfRewriteRule {
|
||||||
|
private final Set<DensityFunction> visited = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||||
|
private final List<String> disjoint = new ArrayList<>();
|
||||||
|
private int checked;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DensityFunction rewrite(DensityFunction function) {
|
||||||
|
if (!visited.add(function)) {
|
||||||
|
return function;
|
||||||
|
}
|
||||||
|
if (function instanceof BinaryFunction binary && (binary.type() == BinaryFunction.Type.MIN || binary.type() == BinaryFunction.Type.MAX)) {
|
||||||
|
checked++;
|
||||||
|
Interval left = binary.left().range();
|
||||||
|
Interval right = binary.right().range();
|
||||||
|
if (left.max() < right.min() || right.max() < left.min()) {
|
||||||
|
disjoint.add(binary.type() + " " + left + " " + right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function.rewriteChildren(this);
|
||||||
|
return function;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int checked() {
|
||||||
|
return checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> disjoint() {
|
||||||
|
return disjoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.DensityFunction;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportDeepCaves263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String NAMESPACE = WorldgenExportRequest.DEFAULT_NAMESPACE;
|
||||||
|
private static final String DIMENSION_ID = NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final int MIN_Y = -64;
|
||||||
|
private static final int LAVA_Y = -62;
|
||||||
|
private static final int LAVA_LINE = -54;
|
||||||
|
private static final int CAVE_TOP = -14;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final String CAVE_FIELD = "cave/field";
|
||||||
|
private static final String DIMENSION = """
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 192},
|
||||||
|
"fluidHeight": 63,
|
||||||
|
"caveLavaHeight": 2,
|
||||||
|
"caveProfile": {
|
||||||
|
"enabled": true,
|
||||||
|
"verticalRange": {"min": 0, "max": 50},
|
||||||
|
"verticalEdgeFade": 0,
|
||||||
|
"baseDensityStyle": {"style": "SIMPLEX", "zoom": 0.6},
|
||||||
|
"detailDensityStyle": {"style": "SIMPLEX", "zoom": 0.3},
|
||||||
|
"baseWeight": 1.0,
|
||||||
|
"detailWeight": 0.0,
|
||||||
|
"densityThreshold": {"min": 5.0, "max": 5.0, "style": {"style": "SIMPLEX"}},
|
||||||
|
"thresholdBias": 0.0,
|
||||||
|
"surfaceClearance": 0,
|
||||||
|
"allowSurfaceBreak": false,
|
||||||
|
"allowFluid": false,
|
||||||
|
"allowLava": true,
|
||||||
|
"modules": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deepCavesOpenAsAirAboveTheIrisLavaBand() throws IOException {
|
||||||
|
WorldgenExportResult result = export();
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
JsonObject head = JsonParser.parseString(Files.readString(result.output().resolve("data/" + NAMESPACE + "/worldgen/material_rule/"
|
||||||
|
+ ExportTestPacks.DIMENSION + ".json"), StandardCharsets.UTF_8)).getAsJsonObject().getAsJsonArray("sequence").get(0).getAsJsonObject();
|
||||||
|
assertEquals("minecraft:ore_vein", head.get("type").getAsString());
|
||||||
|
String opening = head.get("density").getAsString();
|
||||||
|
assertTrue(opening, Files.isRegularFile(densityFunction(result.output(), opening)));
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(0, 0);
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z++) {
|
||||||
|
for (int y = MIN_Y; y <= CAVE_TOP; y++) {
|
||||||
|
BlockState state = chunk.getBlockState(new BlockPos(x, y, z));
|
||||||
|
String where = state + " at " + x + "," + y + "," + z;
|
||||||
|
if (y <= LAVA_Y) {
|
||||||
|
assertTrue(where, state.is(Blocks.LAVA));
|
||||||
|
} else {
|
||||||
|
assertTrue(where, state.isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(chunk.getBlockState(new BlockPos(0, LAVA_LINE - 1, 0)).isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deepCaveRuleSharesTheCaveFieldCacheWithTheFinalDensity() throws IOException {
|
||||||
|
WorldgenExportResult result = export();
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
CompiledCaches caches = CompiledCaches.open(sampler.registries(), SEED);
|
||||||
|
DensityFunction opening = caches.oreVeins().getFirst().density();
|
||||||
|
int field = caches.rootId(caches.function(NAMESPACE + ":" + CAVE_FIELD));
|
||||||
|
Set<Integer> finalDensity = caches.ids(caches.finalDensity());
|
||||||
|
|
||||||
|
assertTrue(caches.ids(opening).toString(), caches.ids(opening).contains(field));
|
||||||
|
assertTrue(finalDensity.toString(), finalDensity.contains(field));
|
||||||
|
assertTrue(finalDensity.toString(), finalDensity.contains(caches.rootId(opening)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportResult export() throws IOException {
|
||||||
|
File pack = ExportTestPacks.withDimension(temporaryFolder.newFolder("pack"), DIMENSION);
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
return WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path densityFunction(Path datapack, String id) {
|
||||||
|
int separator = id.indexOf(':');
|
||||||
|
return datapack.resolve("data/" + id.substring(0, separator) + "/worldgen/density_function/" + id.substring(separator + 1) + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportPipeline263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final int[] SAMPLE_Y = {-64, 0, 64, 200};
|
||||||
|
private static final int LATTICE = 8;
|
||||||
|
private static final int LATTICE_STEP = 61;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedPackLoadsAndSamples() throws IOException {
|
||||||
|
File pack = ExportTestPacks.basic(temporaryFolder.newFolder("pack"));
|
||||||
|
Path root = temporaryFolder.newFolder("exports").toPath();
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(root)
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorldgenExportResult result = WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
assertFalse(sampler.densityFunctionIds().isEmpty());
|
||||||
|
for (String id : sampler.densityFunctionIds()) {
|
||||||
|
for (int y : SAMPLE_Y) {
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int z = (zIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
float value = sampler.density(id, x, y, z);
|
||||||
|
assertTrue(id + " at " + x + "," + y + "," + z + " = " + value, Float.isFinite(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int xIndex = 0; xIndex < LATTICE; xIndex++) {
|
||||||
|
for (int zIndex = 0; zIndex < LATTICE; zIndex++) {
|
||||||
|
int x = (xIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int z = (zIndex - LATTICE / 2) * LATTICE_STEP;
|
||||||
|
int height = sampler.baseHeight(x, z);
|
||||||
|
assertTrue("height " + height + " at " + x + "," + z, height >= sampler.minY() && height <= sampler.minY() + sampler.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportSamplerThreadsTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final String FINAL_DENSITY_ID = DIMENSION_ID + "/final_density";
|
||||||
|
private static final int THREADS = 8;
|
||||||
|
private static final int POINTS = 2048;
|
||||||
|
private static final int ROW = 64;
|
||||||
|
private static final int STEP = 37;
|
||||||
|
private static final int ROUNDS = 3;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentSamplingMatchesSequentialSampling() throws Exception {
|
||||||
|
File pack = ExportTestPacks.basic(temporaryFolder.newFolder("pack"));
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
WorldgenExportResult result = WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
assertTrue(sampler.densityFunctionIds().toString(), sampler.densityFunctionIds().contains(FINAL_DENSITY_ID));
|
||||||
|
Sample expected = sweep(sampler, 0, 1).call();
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||||
|
try {
|
||||||
|
List<Future<Sample>> futures = new ArrayList<>(THREADS);
|
||||||
|
for (int thread = 0; thread < THREADS; thread++) {
|
||||||
|
futures.add(executor.submit(sweep(sampler, thread * (POINTS / THREADS), ROUNDS)));
|
||||||
|
}
|
||||||
|
for (Future<Sample> future : futures) {
|
||||||
|
Sample sample = future.get();
|
||||||
|
assertArrayEquals(expected.density(), sample.density(), 0.0F);
|
||||||
|
assertEquals(List.of(expected.biome()), List.of(sample.biome()));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Callable<Sample> sweep(WorldgenExportSampler sampler, int offset, int rounds) {
|
||||||
|
return () -> {
|
||||||
|
float[] density = new float[POINTS];
|
||||||
|
String[] biome = new String[POINTS];
|
||||||
|
for (int round = 0; round < rounds; round++) {
|
||||||
|
for (int step = 0; step < POINTS; step++) {
|
||||||
|
int index = (offset + step) % POINTS;
|
||||||
|
int x = (index % ROW - ROW / 2) * STEP;
|
||||||
|
int y = (index * 7) % 256 - 64;
|
||||||
|
int z = (index / ROW - POINTS / ROW / 2) * STEP;
|
||||||
|
density[index] = sampler.density(FINAL_DENSITY_ID, x, y, z);
|
||||||
|
biome[index] = sampler.biome(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Sample(density, biome);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Sample(float[] density, String[] biome) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.testsupport.CompatTestPlatform;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportSeaLevel263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final int FLUID_HEIGHT = 40;
|
||||||
|
private static final int SEA_FLOOR = 28;
|
||||||
|
private static final int CHUNK_WIDTH = 16;
|
||||||
|
private static final String DIMENSION = """
|
||||||
|
{
|
||||||
|
"regions": ["region"],
|
||||||
|
"dimensionHeight": {"min": -64, "max": 192},
|
||||||
|
"fluidHeight": 40,
|
||||||
|
"landChance": 0,
|
||||||
|
"carvingEnabled": false
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void waterFillsThroughTheIrisFluidHeight() throws IOException {
|
||||||
|
WorldgenExportResult result = export();
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
ProtoChunk chunk = sampler.terrainChunk(0, 0);
|
||||||
|
for (int x = 0; x < CHUNK_WIDTH; x++) {
|
||||||
|
for (int z = 0; z < CHUNK_WIDTH; z++) {
|
||||||
|
assertSolid(chunk.getBlockState(new BlockPos(x, SEA_FLOOR, z)), x, z);
|
||||||
|
for (int y = SEA_FLOOR + 1; y <= FLUID_HEIGHT; y++) {
|
||||||
|
BlockState state = chunk.getBlockState(new BlockPos(x, y, z));
|
||||||
|
assertTrue(state + " at " + x + "," + y + "," + z, state.is(Blocks.WATER));
|
||||||
|
}
|
||||||
|
BlockState above = chunk.getBlockState(new BlockPos(x, FLUID_HEIGHT + 1, z));
|
||||||
|
assertTrue(above + " above the fluid height at " + x + "," + z, above.isAir());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(FLUID_HEIGHT + 1, sampler.seaLevel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertSolid(BlockState state, int x, int z) {
|
||||||
|
assertTrue(state + " sea floor at " + x + "," + z, !state.isAir() && state.getFluidState().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportResult export() throws IOException {
|
||||||
|
File pack = ExportTestPacks.withDimension(temporaryFolder.newFolder("pack"), DIMENSION);
|
||||||
|
CompatTestPlatform.write(pack, "generators/flat.json", "{\"composite\":[]}");
|
||||||
|
CompatTestPlatform.write(pack, "biomes/ocean.json", """
|
||||||
|
{"name":"Ocean","generators":[{"generator":"flat","min":-12,"max":-12}]}
|
||||||
|
""");
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
return WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportTestPacks;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenDatapackExporter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.NbtAccounter;
|
||||||
|
import net.minecraft.nbt.NbtIo;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.Rotation;
|
||||||
|
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.pools.ListPoolElement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenExportSubterrain263Test {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = WorldgenExportRequest.DEFAULT_NAMESPACE + ":" + ExportTestPacks.DIMENSION;
|
||||||
|
private static final String POOLS = WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/pools";
|
||||||
|
private static final String TUBES = WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/tubes";
|
||||||
|
private static final String ANCHOR = WorldgenExportRequest.DEFAULT_NAMESPACE + ":subterrain/anchor";
|
||||||
|
private static final String TEMPLATES = "data/" + WorldgenExportRequest.DEFAULT_NAMESPACE + "/structure/subterrain/";
|
||||||
|
private static final int CHUNK_X = 3;
|
||||||
|
private static final int CHUNK_Z = -2;
|
||||||
|
private static final int POOLS_CENTER_Y = -49;
|
||||||
|
private static final int POOLS_BELOW_CENTER = 10;
|
||||||
|
private static final int POOLS_EXTENT = 10;
|
||||||
|
private static final int MAX_REACH = 128;
|
||||||
|
private static final String DIMENSION = """
|
||||||
|
{"regions": ["region"], "subterrainFeatures": [
|
||||||
|
{"id": "pools", "family": "CENOTE", "worldYRange": {"min": -60, "max": -40}, "spacing": 64, "probability": 1.0,
|
||||||
|
"radius": 8, "height": 12, "fluid": "WATER", "fluidDepth": 2, "shapeWarp": 0.5, "pillarSpacing": 0,
|
||||||
|
"formationFraction": 0.0, "solid": "minecraft:stone"},
|
||||||
|
{"id": "tubes", "family": "LAVA_TUBE", "allowedRegions": ["region"], "worldYRange": {"min": -60, "max": 40},
|
||||||
|
"spacing": 320, "probability": 0.5, "length": 300, "radius": 6, "height": 8, "fluidDepth": 2, "chimneyHeight": 0,
|
||||||
|
"shapeWarp": 0.0, "pillarSpacing": 0, "formationFraction": 0.0, "solid": "minecraft:stone"}]}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ExportTestPacks.BoundPlatform platform = new ExportTestPacks.BoundPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void subterrainRoomsLoadAndPlaceThroughRealCodecs() throws IOException {
|
||||||
|
File pack = ExportTestPacks.withDimension(temporaryFolder.newFolder("pack"), DIMENSION);
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack.toPath())
|
||||||
|
.dimension(ExportTestPacks.DIMENSION)
|
||||||
|
.exportRoot(temporaryFolder.newFolder("exports").toPath())
|
||||||
|
.format("directory")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorldgenExportResult result = WorldgenDatapackExporter.create().export(request, WorldgenExportProgress.NONE);
|
||||||
|
|
||||||
|
assertTrue(result.message() + " " + result.diagnostics(), result.isSuccess());
|
||||||
|
try (WorldgenExportSampler sampler = WorldgenExportSampler.open(result.output(), WorldgenExportMode.OVERWORLD, DIMENSION_ID, SEED)) {
|
||||||
|
assertRegistered(sampler, POOLS, 4, 2);
|
||||||
|
assertRegistered(sampler, TUBES, 20, 10);
|
||||||
|
int cornerX = CHUNK_X << 4;
|
||||||
|
int cornerZ = CHUNK_Z << 4;
|
||||||
|
|
||||||
|
BoundingBox pools = placedBox(sampler, POOLS);
|
||||||
|
assertEquals(cornerX - POOLS_EXTENT, pools.minX());
|
||||||
|
assertEquals(cornerX + POOLS_EXTENT, pools.maxX());
|
||||||
|
assertEquals(cornerZ - POOLS_EXTENT, pools.minZ());
|
||||||
|
assertEquals(cornerZ + POOLS_EXTENT, pools.maxZ());
|
||||||
|
assertEquals(POOLS_CENTER_Y - POOLS_BELOW_CENTER, pools.minY());
|
||||||
|
|
||||||
|
BoundingBox tubes = placedBox(sampler, TUBES);
|
||||||
|
assertEquals(2 * MAX_REACH + 1, Math.max(tubes.getXSpan(), tubes.getZSpan()));
|
||||||
|
assertTrue(tubes.toString(), tubes.minX() >= cornerX - MAX_REACH && tubes.maxX() <= cornerX + MAX_REACH);
|
||||||
|
assertTrue(tubes.toString(), tubes.minZ() >= cornerZ - MAX_REACH && tubes.maxZ() <= cornerZ + MAX_REACH);
|
||||||
|
assertTrue(tubes.toString(), tubes.minY() >= -60 && tubes.maxY() <= 40);
|
||||||
|
}
|
||||||
|
assertTemplatesResolve(result.output());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRegistered(WorldgenExportSampler sampler, String id, int spacing, int separation) {
|
||||||
|
Identifier identifier = Identifier.parse(id);
|
||||||
|
Structure structure = sampler.registries().lookupOrThrow(Registries.STRUCTURE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE, identifier)).value();
|
||||||
|
assertTrue(id, structure instanceof JigsawStructure);
|
||||||
|
assertEquals(id, GenerationStep.Decoration.UNDERGROUND_STRUCTURES, structure.step());
|
||||||
|
StructureSet set = sampler.registries().lookupOrThrow(Registries.STRUCTURE_SET)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, identifier)).value();
|
||||||
|
RandomSpreadStructurePlacement placement = (RandomSpreadStructurePlacement) set.placement();
|
||||||
|
assertEquals(id, spacing, placement.spacing());
|
||||||
|
assertEquals(id, separation, placement.separation());
|
||||||
|
assertTrue(id, sampler.registries().lookupOrThrow(Registries.TEMPLATE_POOL).containsKey(identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoundingBox placedBox(WorldgenExportSampler sampler, String id) throws IOException {
|
||||||
|
StructureStart start = sampler.structureStart(id, CHUNK_X, CHUNK_Z);
|
||||||
|
assertTrue(id, start.isValid());
|
||||||
|
assertEquals(id, 1, start.getPieces().size());
|
||||||
|
PoolElementStructurePiece piece = (PoolElementStructurePiece) start.getPieces().getFirst();
|
||||||
|
assertTrue(id + " " + piece.getElement(), piece.getElement() instanceof ListPoolElement);
|
||||||
|
return start.getBoundingBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertTemplatesResolve(Path datapack) throws IOException {
|
||||||
|
List<Path> templates;
|
||||||
|
try (Stream<Path> paths = Files.walk(datapack.resolve(TEMPLATES))) {
|
||||||
|
templates = paths.filter(path -> path.toString().endsWith(".nbt")).sorted().toList();
|
||||||
|
}
|
||||||
|
assertEquals(12, templates.size());
|
||||||
|
boolean lava = false;
|
||||||
|
for (Path path : templates) {
|
||||||
|
CompoundTag tag;
|
||||||
|
try (InputStream input = Files.newInputStream(path)) {
|
||||||
|
tag = NbtIo.readCompressed(input, NbtAccounter.unlimitedHeap());
|
||||||
|
}
|
||||||
|
StructureTemplate template = new StructureTemplate();
|
||||||
|
template.load(BuiltInRegistries.BLOCK, tag);
|
||||||
|
StructurePlaceSettings settings = new StructurePlaceSettings();
|
||||||
|
assertTrue(path.toString(), template.filterBlocks(BlockPos.ZERO, settings, Blocks.AIR).isEmpty());
|
||||||
|
if (path.getFileName().toString().endsWith("_core.nbt")) {
|
||||||
|
assertFalse(path.toString(), template.filterBlocks(BlockPos.ZERO, settings, Blocks.CAVE_AIR).isEmpty());
|
||||||
|
List<StructureTemplate.JigsawBlockInfo> jigsaws = template.getJigsaws(BlockPos.ZERO, Rotation.NONE);
|
||||||
|
assertEquals(path.toString(), 1, jigsaws.size());
|
||||||
|
assertEquals(ANCHOR, jigsaws.getFirst().name().toString());
|
||||||
|
lava |= !template.filterBlocks(BlockPos.ZERO, settings, Blocks.LAVA).isEmpty();
|
||||||
|
} else {
|
||||||
|
assertFalse(path.toString(), template.filterBlocks(BlockPos.ZERO, settings, Blocks.STONE).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("no lava tube template holds lava", lava);
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package art.arcane.iris.probe;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class WorldgenStageBenchTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final int SIDE = 2;
|
||||||
|
private static final WorldgenStageBench.CallBudget BUDGET = new WorldgenStageBench.CallBudget(32, 8, 0L);
|
||||||
|
|
||||||
|
private static HeadlessNativeRegistries loaded;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void loadFixture() throws IOException {
|
||||||
|
HeadlessNativeBootstrap.initialize();
|
||||||
|
SortedMap<String, byte[]> files = WorldgenJsonWriter.forTarget(DataVersion.V26_3)
|
||||||
|
.write(ExportIrFixtures.flatNoise(DataVersion.V26_3, WorldgenExportMode.PRESET));
|
||||||
|
Path directory = Files.createTempDirectory("iris-stage-bench-");
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
Path target = directory.resolve(file.getKey());
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.write(target, file.getValue());
|
||||||
|
}
|
||||||
|
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||||
|
loaded = HeadlessNativeRegistries.load(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void closeFixture() throws IOException {
|
||||||
|
if (loaded != null) {
|
||||||
|
loaded.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedFixtureRunsEveryStage() {
|
||||||
|
assertStages(bench("export", ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaOverworldRunsEveryStage() {
|
||||||
|
assertStages(bench("vanilla", DatapackCostProbe.VANILLA_PRESET));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertStages(WorldgenStageBench bench) {
|
||||||
|
WorldgenStageBench.ChunkSamples samples = new WorldgenStageBench.ChunkSamples();
|
||||||
|
bench.chunks(0, 0, SIDE, samples);
|
||||||
|
bench.chunks(SIDE + 4, 0, SIDE, samples);
|
||||||
|
WorldgenStageBench.ChunkTimings timings = samples.timings();
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.chunks());
|
||||||
|
assertEquals(2 * (SIDE + 2) * (SIDE + 2), timings.biomes().wall().count());
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.noise().cpu().count());
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.surface().wall().count());
|
||||||
|
assertEquals(2 * SIDE * SIDE, timings.carverBiome().wall().count());
|
||||||
|
assertEquals(0L, timings.biomeFallbacks());
|
||||||
|
assertTrue(Double.isFinite(timings.totalMean()));
|
||||||
|
assertTrue(Double.isFinite(timings.totalCpuMean()));
|
||||||
|
assertCalls(samples(bench, Call.UNCACHED_BIOME));
|
||||||
|
assertCalls(samples(bench, Call.BASE_HEIGHT));
|
||||||
|
assertCalls(samples(bench, Call.BASE_COLUMN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertCalls(WorldgenStageBench.CallTiming timing) {
|
||||||
|
assertEquals(2 * BUDGET.minimumCalls(), timing.calls());
|
||||||
|
assertEquals(2 * BUDGET.minimumCalls(), timing.wall().count());
|
||||||
|
assertTrue(Double.isFinite(timing.cpuMean()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenStageBench.CallTiming samples(WorldgenStageBench bench, Call call) {
|
||||||
|
WorldgenStageBench.CallSamples samples = new WorldgenStageBench.CallSamples();
|
||||||
|
for (int round = 0; round < 2; round++) {
|
||||||
|
switch (call) {
|
||||||
|
case UNCACHED_BIOME -> bench.uncachedBiome(BUDGET, samples);
|
||||||
|
case BASE_HEIGHT -> bench.baseHeight(BUDGET, Heightmap.Types.WORLD_SURFACE_WG, samples);
|
||||||
|
case BASE_COLUMN -> bench.baseColumn(BUDGET, samples);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return samples.timing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenStageBench bench(String label, String preset) {
|
||||||
|
return new WorldgenStageBench(new WorldgenStageBench.Options(label,
|
||||||
|
DatapackCostProbe.presetOverworld(loaded.registries(), preset), loaded.registries(), SEED));
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Call {
|
||||||
|
UNCACHED_BIOME,
|
||||||
|
BASE_HEIGHT,
|
||||||
|
BASE_COLUMN
|
||||||
|
}
|
||||||
|
}
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
|
||||||
|
import art.arcane.iris.probe.PaperRegistryIsolation;
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaNoiseColumnSourceTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
|
||||||
|
private static final String BIOME_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.BIOME;
|
||||||
|
private static final String LOAD_KEY = "Flat Plains";
|
||||||
|
private static final int LAND_HEIGHT = 80;
|
||||||
|
private static final int SEA_FLOOR_HEIGHT = 40;
|
||||||
|
private static final int MAX_Y = ExportIrFixtures.MIN_Y + ExportIrFixtures.HEIGHT;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void landColumnsReportTheTopSolidBlockAndStayDry() throws IOException {
|
||||||
|
try (VanillaNoiseColumnSource source = new VanillaNoiseColumnSource(open(LAND_HEIGHT), Map.of(BIOME_ID, LOAD_KEY))) {
|
||||||
|
assertEquals(ExportIrFixtures.MIN_Y, source.minY());
|
||||||
|
assertEquals(MAX_Y, source.maxY());
|
||||||
|
for (int x : new int[]{-37, 0, 113}) {
|
||||||
|
FidelityColumn column = source.column(x, -x);
|
||||||
|
assertEquals(new FidelityColumn(LOAD_KEY, LAND_HEIGHT - 1, false), column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void seaColumnsReportTheFloorUnderWater() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = open(SEA_FLOOR_HEIGHT);
|
||||||
|
try (VanillaNoiseColumnSource source = new VanillaNoiseColumnSource(sampler, Map.of(BIOME_ID, LOAD_KEY))) {
|
||||||
|
FidelityStats stats = new FidelityStats(source.minY(), source.maxY());
|
||||||
|
for (int x : new int[]{-37, 0, 113}) {
|
||||||
|
FidelityColumn column = source.column(x, x);
|
||||||
|
assertEquals(new FidelityColumn(LOAD_KEY, sampler.floorHeight(x, x) - 1, true), column);
|
||||||
|
assertTrue(column.terrainY() + " under sea level", column.terrainY() < ExportIrFixtures.SEA_LEVEL - 1);
|
||||||
|
assertEquals(ExportIrFixtures.SEA_LEVEL, sampler.baseHeight(x, x));
|
||||||
|
stats.accept(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(0.0D, stats.landFraction(), 0.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedBiomeWithoutALoadKeyIsRejected() throws IOException {
|
||||||
|
try (VanillaNoiseColumnSource source = new VanillaNoiseColumnSource(open(LAND_HEIGHT), Map.of())) {
|
||||||
|
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> source.column(0, 0));
|
||||||
|
|
||||||
|
assertTrue(failure.getMessage(), failure.getMessage().contains(BIOME_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportSampler open(int height) throws IOException {
|
||||||
|
return WorldgenExportSampler.open(flatExport(height), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> flatExport(int height) {
|
||||||
|
Field zero = new Field.Const(0.0);
|
||||||
|
Field level = new Field.Const(height);
|
||||||
|
Field y = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -4064.0, 4062.0, -4064.0, 4062.0);
|
||||||
|
Field finalDensity = new Field.Grid(new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125),
|
||||||
|
new Field.Binary(Field.BinaryOp.SUB, level, y)), 4, 8);
|
||||||
|
RouterDef router = new RouterDef(zero, zero, zero, zero, zero, zero, new Field.Grid(level, 16, 1), finalDensity, AquiferDef.dry(level));
|
||||||
|
DimensionDef dimension = new DimensionDef(ExportIrFixtures.DIMENSION, ExportIrFixtures.MIN_Y, ExportIrFixtures.HEIGHT,
|
||||||
|
ExportIrFixtures.HEIGHT, ExportIrFixtures.FLUID_HEIGHT, "minecraft:stone", "minecraft:water", ExportIrFixtures.dimensionType());
|
||||||
|
ClimateLayout climate = new ClimateLayout(List.of(new ClimateLayout.ClimateEntry(BIOME_ID, ClimateLayout.ClimateAxis.TEMPERATURE, -2.0, 2.0)));
|
||||||
|
ExportIr ir = new ExportIr(ExportIrFixtures.NAMESPACE, DataVersion.V26_3, WorldgenExportMode.PRESET, dimension, List.of(), router, climate,
|
||||||
|
List.of(new BiomeDef(ExportIrFixtures.BIOME, ExportIrFixtures.biome())), MaterialProgram.rock("minecraft:stone"),
|
||||||
|
List.of(), List.of(), List.of());
|
||||||
|
return WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ir);
|
||||||
|
}
|
||||||
|
}
|
||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
package art.arcane.iris.probe.fidelity;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
|
||||||
|
import art.arcane.iris.probe.PaperRegistryIsolation;
|
||||||
|
import art.arcane.iris.probe.WorldgenExportSampler;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class VanillaNoiseFidelitySourceTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
|
||||||
|
|
||||||
|
private static final long SEED = 1337L;
|
||||||
|
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
|
||||||
|
private static final String BIOME_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.BIOME;
|
||||||
|
private static final String LOAD_KEY = "Flat Plains";
|
||||||
|
private static final int LAND_HEIGHT = 80;
|
||||||
|
private static final int MAX_Y = ExportIrFixtures.MIN_Y + ExportIrFixtures.HEIGHT;
|
||||||
|
private static final String WALL_SHELL = ExportIrFixtures.NAMESPACE + ":cave/wall_shell";
|
||||||
|
private static final int WALL_MIN_Y = 10;
|
||||||
|
private static final int WALL_MAX_Y = 20;
|
||||||
|
private static final int CARVED_HEIGHT = 120;
|
||||||
|
private static final int VOID_MIN_Y = 96;
|
||||||
|
private static final int CAVE_MIN_Y = 64;
|
||||||
|
private static final Field BLOCK_Y = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -4064.0, 4062.0, -4064.0, 4062.0);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flatExportReportsTheConstantSurface() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = open(LAND_HEIGHT);
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
assertEquals(ExportIrFixtures.MIN_Y, source.minY());
|
||||||
|
assertEquals(MAX_Y, source.maxY());
|
||||||
|
for (int x : new int[]{-37, 0, 113}) {
|
||||||
|
assertEquals(LAND_HEIGHT, sampler.baseHeight(x, -x));
|
||||||
|
}
|
||||||
|
for (int[] chunkPosition : new int[][]{{0, 0}, {-3, 5}}) {
|
||||||
|
FidelityChunk chunk = source.sample(chunkPosition[0], chunkPosition[1]);
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
assertEquals(LAND_HEIGHT - 1, chunk.surfaceY(x, z));
|
||||||
|
assertEquals(LOAD_KEY, chunk.surfaceBiome(x, z));
|
||||||
|
assertTrue(chunk.open(x, LAND_HEIGHT, z));
|
||||||
|
assertNull(chunk.block(x, LAND_HEIGHT - 1, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int y = ExportIrFixtures.MIN_Y; y < LAND_HEIGHT; y++) {
|
||||||
|
assertFalse("y " + y, chunk.open(7, y, 9));
|
||||||
|
assertFalse("y " + y, chunk.fluid(7, y, 9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
|
||||||
|
|
||||||
|
assertEquals(144L, stats.heightColumns());
|
||||||
|
assertEquals(LAND_HEIGHT - 1, stats.heightQuantile(0.1D), 0.0D);
|
||||||
|
assertEquals(LAND_HEIGHT - 1, stats.heightQuantile(0.9D), 0.0D);
|
||||||
|
assertEquals(1.0D, stats.landFraction(), 0.0D);
|
||||||
|
assertEquals(Map.of(LOAD_KEY, 144L), stats.biomeColumns());
|
||||||
|
assertEquals(0L, stats.blockColumns());
|
||||||
|
assertEquals(0L, stats.wallSolidVoxels());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wallShellDensityMarksWallVoxels() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = WorldgenExportSampler.open(flatExport(LAND_HEIGHT, wallShell()), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
FidelityChunk chunk = source.sample(2, -1);
|
||||||
|
|
||||||
|
assertTrue(chunk.wallsKnown());
|
||||||
|
assertTrue(chunk.wall(7, WALL_MIN_Y, 9));
|
||||||
|
assertTrue(chunk.wall(7, WALL_MAX_Y - 1, 9));
|
||||||
|
assertFalse(chunk.wall(7, WALL_MIN_Y - 1, 9));
|
||||||
|
assertFalse(chunk.wall(7, WALL_MAX_Y, 9));
|
||||||
|
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
|
||||||
|
|
||||||
|
long interiorColumns = 9L * 9L;
|
||||||
|
assertEquals(interiorColumns * (LAND_HEIGHT - 1 - ExportIrFixtures.MIN_Y), stats.wallSolidVoxels());
|
||||||
|
assertEquals(interiorColumns * (WALL_MAX_Y - WALL_MIN_Y), stats.wallVoxels());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openSpaceOutsideTheTerrainOperandIsATerrainVoid() throws IOException {
|
||||||
|
WorldgenExportSampler sampler = WorldgenExportSampler.open(carvedExport(), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
FidelityChunk chunk = source.sample(1, -2);
|
||||||
|
|
||||||
|
assertEquals(CARVED_HEIGHT - 1, chunk.surfaceY(7, 9));
|
||||||
|
assertTrue(chunk.open(7, VOID_MIN_Y, 9));
|
||||||
|
assertFalse(chunk.terrainMass(7, VOID_MIN_Y, 9));
|
||||||
|
assertTrue(chunk.open(7, CAVE_MIN_Y + 3, 9));
|
||||||
|
assertTrue(chunk.terrainMass(7, CAVE_MIN_Y + 3, 9));
|
||||||
|
assertFalse(chunk.open(7, VOID_MIN_Y - 4, 9));
|
||||||
|
assertTrue(chunk.terrainMass(7, VOID_MIN_Y - 4, 9));
|
||||||
|
|
||||||
|
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
|
||||||
|
|
||||||
|
int voidBand = (VOID_MIN_Y - ExportIrFixtures.MIN_Y) / 16;
|
||||||
|
int caveBand = (CAVE_MIN_Y - ExportIrFixtures.MIN_Y) / 16;
|
||||||
|
assertEquals(1.0D, stats.voidShare(voidBand), 0.0D);
|
||||||
|
assertEquals(0.0D, stats.caveShare(voidBand), 0.0D);
|
||||||
|
assertEquals(1.0D, stats.caveShare(caveBand), 0.0D);
|
||||||
|
assertEquals(0.0D, stats.voidShare(caveBand), 0.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportWithoutAWallShellReportsNoWalls() throws IOException {
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(open(LAND_HEIGHT), Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
|
||||||
|
assertFalse(source.sample(0, 0).wallsKnown());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportedBiomeWithoutALoadKeyIsRejected() throws IOException {
|
||||||
|
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(open(LAND_HEIGHT), Map.of(), WALL_SHELL)) {
|
||||||
|
FidelityChunk chunk = source.sample(0, 0);
|
||||||
|
|
||||||
|
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> chunk.surfaceBiome(0, 0));
|
||||||
|
|
||||||
|
assertTrue(failure.getMessage(), failure.getMessage().contains(BIOME_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportSampler open(int height) throws IOException {
|
||||||
|
return WorldgenExportSampler.open(flatExport(height), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonResource wallShell() {
|
||||||
|
return new JsonResource("data/" + ExportIrFixtures.NAMESPACE + "/worldgen/density_function/cave/wall_shell.json", JsonParser.parseString(
|
||||||
|
"{\"type\":\"minecraft:range_choice\",\"input\":{\"type\":\"minecraft:gradient\",\"axis\":\"y\",\"tiling\":\"clamp_to_edge\","
|
||||||
|
+ "\"from_coordinate\":-4064,\"to_coordinate\":4062,\"from_value\":-4064.0,\"to_value\":4062.0},"
|
||||||
|
+ "\"min_inclusive\":" + WALL_MIN_Y + ".0,\"max_exclusive\":" + WALL_MAX_Y + ".0,\"when_in_range\":1.0,\"when_out_of_range\":0.0}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> flatExport(int height, JsonResource... resources) {
|
||||||
|
Field level = new Field.Const(height);
|
||||||
|
Field finalDensity = new Field.Grid(new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125),
|
||||||
|
new Field.Binary(Field.BinaryOp.SUB, level, BLOCK_Y)), 4, 8);
|
||||||
|
return export(height, finalDensity, resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> carvedExport() {
|
||||||
|
Field ramp = new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125), new Field.Binary(Field.BinaryOp.SUB, new Field.Const(CARVED_HEIGHT), BLOCK_Y));
|
||||||
|
Field terrain = new Field.RangeIf(BLOCK_Y, VOID_MIN_Y, VOID_MIN_Y + 16, new Field.Const(-1.0), ramp);
|
||||||
|
Field caves = new Field.RangeIf(BLOCK_Y, CAVE_MIN_Y, CAVE_MIN_Y + 16, new Field.Const(-1.0), new Field.Const(1.0));
|
||||||
|
return export(CARVED_HEIGHT, new Field.Binary(Field.BinaryOp.MIN, terrain, caves));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, byte[]> export(int height, Field finalDensity, JsonResource... resources) {
|
||||||
|
Field zero = new Field.Const(0.0);
|
||||||
|
Field level = new Field.Const(height);
|
||||||
|
RouterDef router = new RouterDef(zero, zero, zero, zero, zero, zero, new Field.Grid(level, 16, 1), finalDensity, AquiferDef.dry(level));
|
||||||
|
DimensionDef dimension = new DimensionDef(ExportIrFixtures.DIMENSION, ExportIrFixtures.MIN_Y, ExportIrFixtures.HEIGHT,
|
||||||
|
ExportIrFixtures.HEIGHT, ExportIrFixtures.FLUID_HEIGHT, "minecraft:stone", "minecraft:water", ExportIrFixtures.dimensionType());
|
||||||
|
ClimateLayout climate = new ClimateLayout(List.of(new ClimateLayout.ClimateEntry(BIOME_ID, ClimateLayout.ClimateAxis.TEMPERATURE, -2.0, 2.0)));
|
||||||
|
ExportIr ir = new ExportIr(ExportIrFixtures.NAMESPACE, DataVersion.V26_3, WorldgenExportMode.PRESET, dimension, List.of(), router, climate,
|
||||||
|
List.of(new BiomeDef(ExportIrFixtures.BIOME, ExportIrFixtures.biome())), MaterialProgram.rock("minecraft:stone"),
|
||||||
|
List.of(), List.of(resources), List.of());
|
||||||
|
return WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,21 +18,41 @@
|
|||||||
|
|
||||||
package art.arcane.iris.command;
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.localization.BukkitCommandMessages;
|
||||||
import art.arcane.iris.localization.C;
|
import art.arcane.iris.localization.C;
|
||||||
|
import art.arcane.iris.localization.DirectorCommandMessages;
|
||||||
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.DatapackIngestService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportServices;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.director.annotations.Director;
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
import art.arcane.volmlib.util.director.annotations.Param;
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
import art.arcane.volmlib.util.localization.TextKey;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
|
||||||
import art.arcane.iris.localization.BukkitCommandMessages;
|
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
|
||||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
|
||||||
public class CommandDatapack implements DirectorExecutor {
|
public class CommandDatapack implements DirectorExecutor {
|
||||||
|
private static final int RESOURCE_REPORTS = 4;
|
||||||
|
|
||||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
|
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
|
||||||
public void ingest(
|
public void ingest(
|
||||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
|
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
|
||||||
@@ -71,4 +91,128 @@ public class CommandDatapack implements DirectorExecutor {
|
|||||||
) {
|
) {
|
||||||
DatapackIngestService.remove(sender(), id);
|
DatapackIngestService.remove(sender(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Director(description = "Export a dimension as a vanilla worldgen datapack", descriptionKey = "iris.director.commanddatapack.director.export_dimension_vanilla_worldgen_datapack")
|
||||||
|
public void export(
|
||||||
|
@Param(name = "dimension", description = "The dimension to export", descriptionKey = "iris.director.commanddatapack.param.export_dimension", customHandler = DimensionHandler.class)
|
||||||
|
IrisDimension dimension,
|
||||||
|
@Param(name = "version", description = "Target Minecraft version, 26.3 or later (blank uses the latest)", descriptionKey = "iris.director.commanddatapack.param.export_version", defaultValue = "")
|
||||||
|
String version,
|
||||||
|
@Param(name = "mode", description = "Output mode: preset, dimension, or overworld", descriptionKey = "iris.director.commanddatapack.param.export_mode", defaultValue = WorldgenExportRequest.DEFAULT_MODE)
|
||||||
|
String mode,
|
||||||
|
@Param(name = "namespace", description = "Namespace for the exported resources", descriptionKey = "iris.director.commanddatapack.param.export_namespace", defaultValue = WorldgenExportRequest.DEFAULT_NAMESPACE)
|
||||||
|
String namespace,
|
||||||
|
@Param(name = "output", description = "Output name in the datapack exports folder (blank uses the dimension and version)", descriptionKey = "iris.director.commanddatapack.param.export_output", defaultValue = "")
|
||||||
|
String output,
|
||||||
|
@Param(name = "format", description = "zip or directory", descriptionKey = "iris.director.commanddatapack.param.export_format", defaultValue = WorldgenExportRequest.DEFAULT_FORMAT)
|
||||||
|
String format,
|
||||||
|
@Param(name = "objects", description = "Include objects as structure templates", descriptionKey = "iris.director.commanddatapack.param.export_objects", defaultValue = "true")
|
||||||
|
boolean objects,
|
||||||
|
@Param(name = "replace", description = "Replace an existing export with the same name", descriptionKey = "iris.director.commanddatapack.param.export_replace", defaultValue = "false")
|
||||||
|
boolean replace
|
||||||
|
) {
|
||||||
|
VolmitSender sender = sender();
|
||||||
|
WorldgenExportRequest request = WorldgenExportRequest.builder()
|
||||||
|
.pack(dimension.getLoader().getDataFolder().toPath())
|
||||||
|
.dimension(dimension.getLoadKey())
|
||||||
|
.version(version)
|
||||||
|
.mode(mode)
|
||||||
|
.namespace(namespace)
|
||||||
|
.output(output)
|
||||||
|
.format(format)
|
||||||
|
.objects(objects)
|
||||||
|
.replace(replace)
|
||||||
|
.exportRoot(IrisPlatforms.get().dataFolder("exports", "datapacks").toPath())
|
||||||
|
.build();
|
||||||
|
DataVersion target;
|
||||||
|
try {
|
||||||
|
target = validateExport(request);
|
||||||
|
} catch (IllegalArgumentException invalid) {
|
||||||
|
sender.sendMessage(C.RED + IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_INVALID, MessageArgument.untrusted("reason", invalid.getMessage())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WorldgenExportService service = WorldgenExportServices.get();
|
||||||
|
sender.sendMessage(C.GRAY + IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STARTED, MessageArgument.untrusted("dimension", request.dimension()), MessageArgument.untrusted("version", target.getVersion())));
|
||||||
|
UUID audience = LanguageAudience.current();
|
||||||
|
ExportFeedback feedback = new ExportFeedback(sender);
|
||||||
|
J.a(() -> LanguageAudience.run(audience, () -> runExport(service, request, feedback)));
|
||||||
|
}
|
||||||
|
|
||||||
|
static DataVersion validateExport(WorldgenExportRequest request) {
|
||||||
|
DataVersion target = WorldgenExportTargets.resolve(request.version());
|
||||||
|
WorldgenExportMode.parse(request.mode());
|
||||||
|
DatapackExportDestinations.parseFormat(request.format());
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runExport(WorldgenExportService service, WorldgenExportRequest request, ExportFeedback feedback) {
|
||||||
|
WorldgenExportResult result;
|
||||||
|
try {
|
||||||
|
result = service.export(request, feedback);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
Iris.reportError("Datapack export failed for '" + request.dimension() + "'.", failure);
|
||||||
|
feedback.send(C.RED, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED, MessageArgument.untrusted("reason", IrisLanguage.errorDetail(failure))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportExport(feedback, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportExport(ExportFeedback feedback, WorldgenExportResult result) {
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
feedback.send(diagnosticColor(diagnostic.severity()), IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_DIAGNOSTIC, MessageArgument.untrusted("key", diagnosticLabel(diagnostic)), MessageArgument.untrusted("message", diagnostic.message())));
|
||||||
|
}
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
feedback.send(C.GREEN, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_EXPORTED, MessageArgument.untrusted("path", result.output())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TextKey outcome = result.status() == WorldgenExportResult.Status.REJECTED
|
||||||
|
? DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_REJECTED
|
||||||
|
: DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED;
|
||||||
|
feedback.send(C.RED, IrisLanguage.text(outcome, MessageArgument.untrusted("reason", result.message())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static C diagnosticColor(ExportDiagnostic.Severity severity) {
|
||||||
|
return switch (severity) {
|
||||||
|
case ERROR -> C.RED;
|
||||||
|
case WARNING -> C.YELLOW;
|
||||||
|
case INFO -> C.GRAY;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String diagnosticLabel(ExportDiagnostic diagnostic) {
|
||||||
|
return diagnostic.resource().isEmpty()
|
||||||
|
? diagnostic.limit().name()
|
||||||
|
: diagnostic.limit().name() + " " + diagnostic.resource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExportFeedback implements WorldgenExportProgress {
|
||||||
|
private final VolmitSender sender;
|
||||||
|
|
||||||
|
private ExportFeedback(VolmitSender sender) {
|
||||||
|
this.sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
send(C.GRAY, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STAGE, MessageArgument.untrusted("stage", stage)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
int stride = Math.max(1, total / RESOURCE_REPORTS);
|
||||||
|
if (done != total && done % stride != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send(C.GRAY, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_RESOURCES, MessageArgument.untrusted("done", done), MessageArgument.untrusted("total", total)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send(C color, String message) {
|
||||||
|
String line = color + message;
|
||||||
|
if (sender.isPlayer()) {
|
||||||
|
J.runEntity(sender.player(), () -> sender.sendMessage(line));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@ import art.arcane.iris.structure.StructureImporter;
|
|||||||
import art.arcane.iris.structure.VillageImporter;
|
import art.arcane.iris.structure.VillageImporter;
|
||||||
import art.arcane.iris.structure.authoring.StructureKey;
|
import art.arcane.iris.structure.authoring.StructureKey;
|
||||||
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
import art.arcane.iris.structure.authoring.StructureWriteResult;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportFormat;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportSettings;
|
import art.arcane.iris.structure.export.VanillaJigsawExportSettings;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
|
||||||
@@ -420,9 +421,9 @@ public class CommandJigsaw implements DirectorExecutor {
|
|||||||
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
|
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
VanillaJigsawExportFormat exportFormat;
|
DatapackExportFormat exportFormat;
|
||||||
try {
|
try {
|
||||||
exportFormat = CommandJigsawExports.parseFormat(format);
|
exportFormat = DatapackExportDestinations.parseFormat(format);
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
sendError(exception.getMessage());
|
sendError(exception.getMessage());
|
||||||
return;
|
return;
|
||||||
@@ -431,7 +432,7 @@ public class CommandJigsaw implements DirectorExecutor {
|
|||||||
.toPath().toAbsolutePath().normalize();
|
.toPath().toAbsolutePath().normalize();
|
||||||
Path destination;
|
Path destination;
|
||||||
try {
|
try {
|
||||||
destination = CommandJigsawExports.resolveExportDestination(exportRoot, output, exportFormat);
|
destination = DatapackExportDestinations.resolve(exportRoot, output, exportFormat);
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
sendError(exception.getMessage());
|
sendError(exception.getMessage());
|
||||||
return;
|
return;
|
||||||
|
|||||||
+4
-37
@@ -4,7 +4,7 @@ import art.arcane.iris.Iris;
|
|||||||
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
|
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
|
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
|
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
|
||||||
import art.arcane.iris.localization.C;
|
import art.arcane.iris.localization.C;
|
||||||
@@ -13,18 +13,14 @@ import art.arcane.iris.world.task.J;
|
|||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
final class CommandJigsawExports {
|
final class CommandJigsawExports {
|
||||||
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
|
|
||||||
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
|
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
|
||||||
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
private CommandJigsawExports() {
|
private CommandJigsawExports() {
|
||||||
}
|
}
|
||||||
@@ -45,11 +41,11 @@ final class CommandJigsawExports {
|
|||||||
|
|
||||||
static boolean beginExport(UUID playerId, Path destination) {
|
static boolean beginExport(UUID playerId, Path destination) {
|
||||||
Objects.requireNonNull(playerId);
|
Objects.requireNonNull(playerId);
|
||||||
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
|
Objects.requireNonNull(destination);
|
||||||
if (!EXPORTING_PLAYERS.add(playerId)) {
|
if (!EXPORTING_PLAYERS.add(playerId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
|
if (DatapackExportDestinations.claim(destination)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
EXPORTING_PLAYERS.remove(playerId);
|
EXPORTING_PLAYERS.remove(playerId);
|
||||||
@@ -58,7 +54,7 @@ final class CommandJigsawExports {
|
|||||||
|
|
||||||
static void finishExport(UUID playerId, Path destination) {
|
static void finishExport(UUID playerId, Path destination) {
|
||||||
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
|
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
|
||||||
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
|
DatapackExportDestinations.release(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void runExport(ExportOperation operation) {
|
static void runExport(ExportOperation operation) {
|
||||||
@@ -102,35 +98,6 @@ final class CommandJigsawExports {
|
|||||||
+ result.status() + ": " + result.output());
|
+ result.status() + ": " + result.output());
|
||||||
}
|
}
|
||||||
|
|
||||||
static VanillaJigsawExportFormat parseFormat(String value) {
|
|
||||||
return switch (value.trim().toLowerCase(Locale.ROOT)) {
|
|
||||||
case "zip" -> VanillaJigsawExportFormat.ZIP;
|
|
||||||
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
|
|
||||||
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static Path resolveExportDestination(
|
|
||||||
Path exportRoot,
|
|
||||||
String output,
|
|
||||||
VanillaJigsawExportFormat format
|
|
||||||
) {
|
|
||||||
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
|
|
||||||
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
|
|
||||||
String name = requested.trim();
|
|
||||||
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
|
|
||||||
}
|
|
||||||
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
|
|
||||||
? name + ".zip" : name;
|
|
||||||
Path destination = root.resolve(fileName).normalize();
|
|
||||||
if (destination.equals(root) || !root.equals(destination.getParent())) {
|
|
||||||
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
|
|
||||||
}
|
|
||||||
return destination;
|
|
||||||
}
|
|
||||||
|
|
||||||
record ExportOperation(
|
record ExportOperation(
|
||||||
Player player,
|
Player player,
|
||||||
VolmitSender sender,
|
VolmitSender sender,
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package art.arcane.iris.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.command.handlers.DimensionHandler;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
public class CommandDatapackExportContractTest {
|
||||||
|
private static final List<String> NAMES = List.of(
|
||||||
|
"dimension", "version", "mode", "namespace", "output", "format", "objects", "replace");
|
||||||
|
private static final List<String> DEFAULTS = List.of(
|
||||||
|
Param.NO_DEFAULT, "", "overworld", "iris_export", "", "zip", "true", "false");
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportParametersAreNamedInOrder() throws Exception {
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
|
for (Parameter parameter : exportMethod().getParameters()) {
|
||||||
|
names.add(parameter.getAnnotation(Param.class).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(NAMES, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dimensionIsPositionalAndEveryOtherParameterIsKeyedWithItsDefault() throws Exception {
|
||||||
|
Parameter[] parameters = exportMethod().getParameters();
|
||||||
|
List<String> defaults = new ArrayList<>();
|
||||||
|
for (Parameter parameter : parameters) {
|
||||||
|
defaults.add(parameter.getAnnotation(Param.class).defaultValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(DEFAULTS, defaults);
|
||||||
|
assertEquals(DimensionHandler.class, parameters[0].getAnnotation(Param.class).customHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void exportCommandAndParametersUseRegisteredDescriptions() throws Exception {
|
||||||
|
Method export = exportMethod();
|
||||||
|
Director director = export.getAnnotation(Director.class);
|
||||||
|
|
||||||
|
assertNotNull(director);
|
||||||
|
assertEquals(0L, Arrays.stream(director.aliases()).filter((String alias) -> !alias.isBlank()).count());
|
||||||
|
assertNotNull(director.descriptionKey(), IrisLanguage.catalog().key(director.descriptionKey()));
|
||||||
|
for (Parameter parameter : export.getParameters()) {
|
||||||
|
String key = parameter.getAnnotation(Param.class).descriptionKey();
|
||||||
|
assertNotNull(key, IrisLanguage.catalog().key(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validRequestResolvesItsTarget() {
|
||||||
|
assertSame(DataVersion.V26_3, CommandDatapack.validateExport(request().build()));
|
||||||
|
assertSame(DataVersion.V26_3, CommandDatapack.validateExport(request().version("26.3").build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedArgumentsAreRejectedWithSharedMessages() {
|
||||||
|
assertEquals("Unsupported datapack export version '1.21.1'. Supported: 26.3",
|
||||||
|
reason(request().version("1.21.1").build()));
|
||||||
|
assertEquals("Unsupported datapack export version '26.2'. Supported: 26.3",
|
||||||
|
reason(request().version("26.2").build()));
|
||||||
|
assertEquals("Unknown export mode 'nether'. Expected: preset, dimension, overworld",
|
||||||
|
reason(request().mode("nether").build()));
|
||||||
|
assertEquals("Export format must be directory or zip.", reason(request().format("rar").build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportRequest.Builder request() {
|
||||||
|
Path root = temporaryFolder.getRoot().toPath();
|
||||||
|
return WorldgenExportRequest.builder()
|
||||||
|
.pack(root.resolve("packs").resolve("overworld"))
|
||||||
|
.dimension("overworld")
|
||||||
|
.exportRoot(root.resolve("exports"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String reason(WorldgenExportRequest request) {
|
||||||
|
return assertThrows(IllegalArgumentException.class, () -> CommandDatapack.validateExport(request)).getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method exportMethod() throws NoSuchMethodException {
|
||||||
|
return CommandDatapack.class.getDeclaredMethod("export", IrisDimension.class, String.class, String.class,
|
||||||
|
String.class, String.class, String.class, boolean.class, boolean.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
-19
@@ -17,7 +17,6 @@ import art.arcane.iris.structure.authoring.StructureOwnershipManifest;
|
|||||||
import art.arcane.iris.structure.authoring.StructureSource;
|
import art.arcane.iris.structure.authoring.StructureSource;
|
||||||
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
|
||||||
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
|
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
|
||||||
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
|
|
||||||
import art.arcane.iris.world.IrisCreator;
|
import art.arcane.iris.world.IrisCreator;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
import art.arcane.iris.pack.value.IrisDirection;
|
import art.arcane.iris.pack.value.IrisDirection;
|
||||||
@@ -413,24 +412,6 @@ public class CommandJigsawContractTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void exportOutputIsOneSafeChildArtifact() {
|
|
||||||
Path root = Path.of("build", "jigsaw-exports").toAbsolutePath().normalize();
|
|
||||||
|
|
||||||
assertEquals(root.resolve("village.zip"), CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "village", VanillaJigsawExportFormat.ZIP));
|
|
||||||
assertEquals(root.resolve("village"), CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "village", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, ".", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "../all-exports", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
|
|
||||||
root, "nested/export", VanillaJigsawExportFormat.DIRECTORY));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void exportLeaseRejectsDuplicatePlayerAndDestination() {
|
public void exportLeaseRejectsDuplicatePlayerAndDestination() {
|
||||||
UUID firstPlayer = UUID.randomUUID();
|
UUID firstPlayer = UUID.randomUUID();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntitySpawns;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBiomeRegistry;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBiomeRegistry;
|
||||||
import art.arcane.iris.BuildConstants;
|
import art.arcane.iris.BuildConstants;
|
||||||
|
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandExecutor;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandExecutor;
|
||||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
@@ -35,6 +36,7 @@ import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
|||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.iris.spi.PlatformScheduler;
|
import art.arcane.iris.spi.PlatformScheduler;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -56,6 +58,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
private final ModdedScheduler scheduler;
|
private final ModdedScheduler scheduler;
|
||||||
private final ModdedStructureHooks structureHooks;
|
private final ModdedStructureHooks structureHooks;
|
||||||
private final ModdedBiomeWriter biomeWriter;
|
private final ModdedBiomeWriter biomeWriter;
|
||||||
|
private final ClasspathVanillaDataSource vanillaData;
|
||||||
|
|
||||||
public ModdedPlatform(NativeModdedLoader loader) {
|
public ModdedPlatform(NativeModdedLoader loader) {
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
@@ -65,6 +68,7 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
this.scheduler = new ModdedScheduler();
|
this.scheduler = new ModdedScheduler();
|
||||||
this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer);
|
this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer);
|
||||||
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
|
||||||
|
this.vanillaData = new ClasspathVanillaDataSource(() -> overworldClass(ModdedEngineBootstrap.currentServer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void errorSink(Consumer<Throwable> sink) {
|
public static void errorSink(Consumer<Throwable> sink) {
|
||||||
@@ -110,6 +114,11 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
return biomeWriter;
|
return biomeWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VanillaDataSource vanillaData() {
|
||||||
|
return vanillaData;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public File dataFolder() {
|
public File dataFolder() {
|
||||||
File folder = loader.configDir().resolve("iris").toFile();
|
File folder = loader.configDir().resolve("iris").toFile();
|
||||||
@@ -365,4 +374,15 @@ public final class ModdedPlatform implements IrisPlatform {
|
|||||||
"modded-generation-registry-v1");
|
"modded-generation-registry-v1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Class<?> overworldClass(NativeModdedServer server) {
|
||||||
|
if (server == null) {
|
||||||
|
throw new IllegalStateException("Vanilla data needs a running Minecraft server.");
|
||||||
|
}
|
||||||
|
NativeWorld overworld = server.overworld();
|
||||||
|
if (overworld == null) {
|
||||||
|
throw new IllegalStateException("Vanilla data needs the Minecraft server overworld to be loaded.");
|
||||||
|
}
|
||||||
|
return overworld.nativeHandle().getClass();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+2
-1
@@ -189,7 +189,8 @@ final class ModdedCommandHelp {
|
|||||||
Entry.command("install", "", ModdedHelpMessages.COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS),
|
Entry.command("install", "", ModdedHelpMessages.COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS),
|
||||||
Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, "ls"),
|
Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, "ls"),
|
||||||
Entry.command("ingest", "", ModdedHelpMessages.COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, "pull"),
|
Entry.command("ingest", "", ModdedHelpMessages.COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, "pull"),
|
||||||
Entry.command("remove", "<id>", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm")
|
Entry.command("remove", "<id>", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm"),
|
||||||
|
Entry.command("export", "<dimension> [version=] [mode=overworld] [namespace=iris_export] [output=] [format=zip] [objects=true] [replace=false]", ModdedHelpMessages.COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK)
|
||||||
));
|
));
|
||||||
SECTIONS.put("datapacks", SECTIONS.get("datapack"));
|
SECTIONS.put("datapacks", SECTIONS.get("datapack"));
|
||||||
SECTIONS.put("dp", SECTIONS.get("datapack"));
|
SECTIONS.put("dp", SECTIONS.get("datapack"));
|
||||||
|
|||||||
+122
@@ -18,7 +18,14 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded.command;
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||||
import art.arcane.iris.modded.ModdedIrisLog;
|
import art.arcane.iris.modded.ModdedIrisLog;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportServices;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.pack.datapack.DataVersion;
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||||
@@ -33,6 +40,7 @@ import com.mojang.brigadier.context.CommandContext;
|
|||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
|
||||||
|
|
||||||
@@ -40,6 +48,7 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -47,15 +56,20 @@ import java.util.LinkedHashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import art.arcane.iris.localization.DirectorCommandMessages;
|
||||||
import art.arcane.iris.localization.IrisLanguage;
|
import art.arcane.iris.localization.IrisLanguage;
|
||||||
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
import art.arcane.iris.modded.localization.ModdedCommandMessages;
|
||||||
import art.arcane.iris.localization.RuntimeUiMessages;
|
import art.arcane.iris.localization.RuntimeUiMessages;
|
||||||
|
import art.arcane.volmlib.util.localization.LanguageAudience;
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
|
import art.arcane.volmlib.util.localization.TextKey;
|
||||||
public final class ModdedDatapackCommands {
|
public final class ModdedDatapackCommands {
|
||||||
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
|
||||||
private static final String WORLD_PACK_NAME = "iris";
|
private static final String WORLD_PACK_NAME = "iris";
|
||||||
|
private static final int RESOURCE_REPORTS = 4;
|
||||||
|
|
||||||
private ModdedDatapackCommands() {
|
private ModdedDatapackCommands() {
|
||||||
}
|
}
|
||||||
@@ -79,6 +93,11 @@ public final class ModdedDatapackCommands {
|
|||||||
root.then(message("ingest", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
root.then(message("ingest", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
||||||
root.then(message("pull", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
root.then(message("pull", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
|
||||||
|
|
||||||
|
root.then(NativeCommandRegistration.literal("export")
|
||||||
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> export(context.getSource(), "")))
|
||||||
|
.then(NativeCommandRegistration.argument("args", StringArgumentType.greedyString())
|
||||||
|
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> export(context.getSource(), StringArgumentType.getString(context, "args"))))));
|
||||||
|
|
||||||
root.then(message("remove", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
root.then(message("remove", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
||||||
root.then(message("rm", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
root.then(message("rm", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
|
||||||
|
|
||||||
@@ -291,4 +310,107 @@ public final class ModdedDatapackCommands {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int export(NativeCommandSource source, String raw) {
|
||||||
|
ModdedDatapackExportArguments arguments;
|
||||||
|
try {
|
||||||
|
arguments = ModdedDatapackExportArguments.parse(raw);
|
||||||
|
} catch (IllegalArgumentException invalid) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_INVALID, MessageArgument.untrusted("reason", invalid.getMessage())));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
File packsRoot = ModdedPackCommands.packsRoot();
|
||||||
|
File pack = PackDirectoryResolver.resolveExisting(packsRoot, arguments.dimension());
|
||||||
|
if (pack == null) {
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_NOT_FOUND_UNDER, MessageArgument.untrusted("name", arguments.dimension()), MessageArgument.untrusted("value", packsRoot.getAbsolutePath())));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
WorldgenExportService service = WorldgenExportServices.get();
|
||||||
|
WorldgenExportRequest request = arguments.toRequest(pack.toPath(), exportRoot());
|
||||||
|
IrisModdedCommands.ok(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STARTED, MessageArgument.untrusted("dimension", request.dimension()), MessageArgument.untrusted("version", arguments.target().getVersion())));
|
||||||
|
ExportFeedback feedback = new ExportFeedback(source);
|
||||||
|
Thread worker = new Thread(() -> runExport(service, request, feedback), "Iris Datapack Export");
|
||||||
|
worker.setDaemon(true);
|
||||||
|
worker.start();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path exportRoot() {
|
||||||
|
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("exports").resolve("datapacks");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runExport(WorldgenExportService service, WorldgenExportRequest request, ExportFeedback feedback) {
|
||||||
|
WorldgenExportResult result;
|
||||||
|
try {
|
||||||
|
result = service.export(request, feedback);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
ModdedIrisLog.error("Iris datapack export failed for " + request.dimension(), failure);
|
||||||
|
feedback.post(() -> IrisModdedCommands.fail(feedback.source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED, MessageArgument.untrusted("reason", IrisLanguage.errorDetail(failure)))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
feedback.post(() -> reportExport(feedback.source, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportExport(NativeCommandSource source, WorldgenExportResult result) {
|
||||||
|
for (ExportDiagnostic diagnostic : result.diagnostics()) {
|
||||||
|
String line = IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_DIAGNOSTIC, MessageArgument.untrusted("key", diagnosticLabel(diagnostic)), MessageArgument.untrusted("message", diagnostic.message()));
|
||||||
|
ModdedCommandFeedback.send(source, NativeCommandText.literal(line).withStyle(diagnosticFormat(diagnostic.severity())));
|
||||||
|
}
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
IrisModdedCommands.ok(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_EXPORTED, MessageArgument.untrusted("path", result.output())));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TextKey outcome = result.status() == WorldgenExportResult.Status.REJECTED
|
||||||
|
? DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_REJECTED
|
||||||
|
: DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED;
|
||||||
|
IrisModdedCommands.fail(source, IrisLanguage.plain(outcome, MessageArgument.untrusted("reason", result.message())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NativeCommandText.Format diagnosticFormat(ExportDiagnostic.Severity severity) {
|
||||||
|
return switch (severity) {
|
||||||
|
case ERROR -> NativeCommandText.Format.RED;
|
||||||
|
case WARNING -> NativeCommandText.Format.YELLOW;
|
||||||
|
case INFO -> NativeCommandText.Format.GRAY;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String diagnosticLabel(ExportDiagnostic diagnostic) {
|
||||||
|
return diagnostic.resource().isEmpty()
|
||||||
|
? diagnostic.limit().name()
|
||||||
|
: diagnostic.limit().name() + " " + diagnostic.resource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ExportFeedback implements WorldgenExportProgress {
|
||||||
|
private final NativeCommandSource source;
|
||||||
|
private final NativeModdedServer server;
|
||||||
|
private final UUID audience;
|
||||||
|
|
||||||
|
private ExportFeedback(NativeCommandSource source) {
|
||||||
|
this.source = source;
|
||||||
|
this.server = source.server();
|
||||||
|
this.audience = source.playerId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
post(() -> progress(IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STAGE, MessageArgument.untrusted("stage", stage))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
int stride = Math.max(1, total / RESOURCE_REPORTS);
|
||||||
|
if (done != total && done % stride != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
post(() -> progress(IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_RESOURCES, MessageArgument.untrusted("done", done), MessageArgument.untrusted("total", total))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void post(Runnable task) {
|
||||||
|
server.execute(() -> LanguageAudience.run(audience, task));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void progress(String message) {
|
||||||
|
ModdedCommandFeedback.send(source, NativeCommandText.literal(message).withStyle(NativeCommandText.Format.GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
record ModdedDatapackExportArguments(String dimension, String version, String mode, String namespace, String output,
|
||||||
|
String format, boolean objects, boolean replace) {
|
||||||
|
private static final Pattern DIMENSION = Pattern.compile("[A-Za-z0-9_/.-]+");
|
||||||
|
private static final String VERSION_KEY = "version";
|
||||||
|
private static final String MODE_KEY = "mode";
|
||||||
|
private static final String NAMESPACE_KEY = "namespace";
|
||||||
|
private static final String OUTPUT_KEY = "output";
|
||||||
|
private static final String FORMAT_KEY = "format";
|
||||||
|
private static final String OBJECTS_KEY = "objects";
|
||||||
|
private static final String REPLACE_KEY = "replace";
|
||||||
|
private static final Set<String> OPTIONS = Set.of(VERSION_KEY, MODE_KEY, NAMESPACE_KEY, OUTPUT_KEY, FORMAT_KEY,
|
||||||
|
OBJECTS_KEY, REPLACE_KEY);
|
||||||
|
|
||||||
|
ModdedDatapackExportArguments {
|
||||||
|
Objects.requireNonNull(dimension, "dimension");
|
||||||
|
Objects.requireNonNull(version, "version");
|
||||||
|
Objects.requireNonNull(mode, "mode");
|
||||||
|
Objects.requireNonNull(namespace, "namespace");
|
||||||
|
Objects.requireNonNull(output, "output");
|
||||||
|
Objects.requireNonNull(format, "format");
|
||||||
|
}
|
||||||
|
|
||||||
|
static ModdedDatapackExportArguments parse(String raw) {
|
||||||
|
String trimmed = raw == null ? "" : raw.trim();
|
||||||
|
String[] tokens = trimmed.isEmpty() ? new String[0] : trimmed.split("\\s+");
|
||||||
|
String dimension = null;
|
||||||
|
Map<String, String> options = new HashMap<>(OPTIONS.size());
|
||||||
|
for (String token : tokens) {
|
||||||
|
int equals = token.indexOf('=');
|
||||||
|
if (equals < 0) {
|
||||||
|
if (dimension != null) {
|
||||||
|
throw new IllegalArgumentException("unexpected argument '" + token + "'");
|
||||||
|
}
|
||||||
|
dimension = token;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String key = token.substring(0, equals);
|
||||||
|
if (!OPTIONS.contains(key)) {
|
||||||
|
throw new IllegalArgumentException("unknown option '" + key + "'");
|
||||||
|
}
|
||||||
|
if (options.putIfAbsent(key, token.substring(equals + 1)) != null) {
|
||||||
|
throw new IllegalArgumentException("option '" + key + "' given twice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalArgumentException("missing dimension");
|
||||||
|
}
|
||||||
|
if (!DIMENSION.matcher(dimension).matches() || dimension.contains("..")) {
|
||||||
|
throw new IllegalArgumentException("invalid dimension key '" + dimension + "'");
|
||||||
|
}
|
||||||
|
ModdedDatapackExportArguments arguments = new ModdedDatapackExportArguments(
|
||||||
|
dimension,
|
||||||
|
options.getOrDefault(VERSION_KEY, ""),
|
||||||
|
options.getOrDefault(MODE_KEY, WorldgenExportRequest.DEFAULT_MODE),
|
||||||
|
options.getOrDefault(NAMESPACE_KEY, WorldgenExportRequest.DEFAULT_NAMESPACE),
|
||||||
|
options.getOrDefault(OUTPUT_KEY, ""),
|
||||||
|
options.getOrDefault(FORMAT_KEY, WorldgenExportRequest.DEFAULT_FORMAT),
|
||||||
|
flag(OBJECTS_KEY, options.get(OBJECTS_KEY), true),
|
||||||
|
flag(REPLACE_KEY, options.get(REPLACE_KEY), false));
|
||||||
|
arguments.target();
|
||||||
|
WorldgenExportMode.parse(arguments.mode());
|
||||||
|
DatapackExportDestinations.parseFormat(arguments.format());
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataVersion target() {
|
||||||
|
return WorldgenExportTargets.resolve(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldgenExportRequest toRequest(Path pack, Path exportRoot) {
|
||||||
|
return WorldgenExportRequest.builder()
|
||||||
|
.pack(pack)
|
||||||
|
.dimension(dimension)
|
||||||
|
.version(version)
|
||||||
|
.mode(mode)
|
||||||
|
.namespace(namespace)
|
||||||
|
.output(output)
|
||||||
|
.format(format)
|
||||||
|
.objects(objects)
|
||||||
|
.replace(replace)
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean flag(String key, String raw, boolean fallback) {
|
||||||
|
if (raw == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return switch (raw) {
|
||||||
|
case "true" -> true;
|
||||||
|
case "false" -> false;
|
||||||
|
default -> throw new IllegalArgumentException(key + " must be true or false");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -330,6 +330,10 @@ public final class ModdedHelpMessages {
|
|||||||
"iris.modded.help.entry.command.remove",
|
"iris.modded.help.entry.command.remove",
|
||||||
"Explain datapack removal workflow"
|
"Explain datapack removal workflow"
|
||||||
);
|
);
|
||||||
|
public static final TextKey COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK = TextKey.of(
|
||||||
|
"iris.modded.help.entry.command.export_datapack",
|
||||||
|
"Export a dimension as a vanilla worldgen datapack"
|
||||||
|
);
|
||||||
public static final TextKey COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON = TextKey.of(
|
public static final TextKey COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON = TextKey.of(
|
||||||
"iris.modded.help.entry.command.list_structures",
|
"iris.modded.help.entry.command.list_structures",
|
||||||
"Regenerate structure-index.json"
|
"Regenerate structure-index.json"
|
||||||
@@ -505,6 +509,7 @@ public final class ModdedHelpMessages {
|
|||||||
COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS,
|
COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS,
|
||||||
COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW,
|
COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW,
|
||||||
COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW,
|
COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW,
|
||||||
|
COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK,
|
||||||
COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON,
|
COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON,
|
||||||
COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS,
|
COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS,
|
||||||
COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION,
|
COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION,
|
||||||
|
|||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||||
|
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.server.Bootstrap;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class ModdedVanillaDataTest {
|
||||||
|
private static final String PLAINS = "data/minecraft/worldgen/biome/plains.json";
|
||||||
|
private static final String BIOME_TAGS = "data/minecraft/tags/worldgen/biome";
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void bootstrapRegistries() {
|
||||||
|
SharedConstants.tryDetectVersion();
|
||||||
|
Bootstrap.bootStrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void overworldLevelClassReadsTheGameVanillaData() {
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
NativeWorld overworld = mock(NativeWorld.class);
|
||||||
|
when(overworld.nativeHandle()).thenReturn(mock(ServerLevel.class));
|
||||||
|
when(server.overworld()).thenReturn(overworld);
|
||||||
|
Class<?> anchor = ModdedPlatform.overworldClass(server);
|
||||||
|
assertSame(ServerLevel.class, anchor);
|
||||||
|
ClasspathVanillaDataSource source = new ClasspathVanillaDataSource(() -> anchor);
|
||||||
|
String plains = new String(source.read(PLAINS).orElseThrow(), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(plains.contains("\"temperature\""));
|
||||||
|
assertTrue(source.list(BIOME_TAGS).contains(BIOME_TAGS + "/is_overworld.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void vanillaDataNeedsALoadedOverworld() {
|
||||||
|
NativeModdedServer server = mock(NativeModdedServer.class);
|
||||||
|
assertThrows(IllegalStateException.class, () -> ModdedPlatform.overworldClass(null));
|
||||||
|
assertThrows(IllegalStateException.class, () -> ModdedPlatform.overworldClass(server));
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -63,6 +63,9 @@ public class IrisModdedCommandParityTest {
|
|||||||
CommandNode<CommandSourceStack> studio = child(iris, "studio");
|
CommandNode<CommandSourceStack> studio = child(iris, "studio");
|
||||||
child(studio, "package");
|
child(studio, "package");
|
||||||
child(studio, "pkg");
|
child(studio, "pkg");
|
||||||
|
CommandNode<CommandSourceStack> datapackExport = child(child(iris, "datapack"), "export");
|
||||||
|
assertNotNull(datapackExport.getCommand());
|
||||||
|
assertNotNull(child(datapackExport, "args").getCommand());
|
||||||
|
|
||||||
CommandNode<CommandSourceStack> download = child(iris, "download");
|
CommandNode<CommandSourceStack> download = child(iris, "download");
|
||||||
CommandNode<CommandSourceStack> source = child(download, "source");
|
CommandNode<CommandSourceStack> source = child(download, "source");
|
||||||
@@ -211,6 +214,8 @@ public class IrisModdedCommandParityTest {
|
|||||||
assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris teleport <dimension> [player] (tp) - ")));
|
assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris teleport <dimension> [player] (tp) - ")));
|
||||||
assertTrue(ModdedCommandHelp.consoleLines("pregen").stream().anyMatch((String line) ->
|
assertTrue(ModdedCommandHelp.consoleLines("pregen").stream().anyMatch((String line) ->
|
||||||
line.startsWith("/iris pregen start <radius> [dimension] [at] [x] [z] [gui] [sync] [nocache] - ")));
|
line.startsWith("/iris pregen start <radius> [dimension] [at] [x] [z] [gui] [sync] [nocache] - ")));
|
||||||
|
assertTrue(ModdedCommandHelp.consoleLines("datapack").stream().anyMatch((String line) ->
|
||||||
|
line.startsWith("/iris datapack export <dimension> [version=] [mode=overworld] [namespace=iris_export] [output=] [format=zip] [objects=true] [replace=false] - ")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CommandNode<CommandSourceStack> child(
|
private static CommandNode<CommandSourceStack> child(
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedDatapackExportArgumentsTest {
|
||||||
|
@Test
|
||||||
|
public void unknownKeyFails() {
|
||||||
|
assertEquals("unknown option 'seed'", reason("overworld seed=1"));
|
||||||
|
assertEquals("unknown option 'Version'", reason("overworld Version=26.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void duplicateKeyFails() {
|
||||||
|
assertEquals("option 'replace' given twice", reason("overworld replace=true replace=false"));
|
||||||
|
assertEquals("option 'version' given twice", reason("overworld version= version=26.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void firstBareTokenIsDimension() {
|
||||||
|
ModdedDatapackExportArguments arguments = ModdedDatapackExportArguments.parse(
|
||||||
|
" version=26.3 overworld mode=dimension namespace=worlds output=export_one format=directory objects=false replace=true ");
|
||||||
|
|
||||||
|
assertEquals("overworld", arguments.dimension());
|
||||||
|
assertEquals("26.3", arguments.version());
|
||||||
|
assertEquals("dimension", arguments.mode());
|
||||||
|
assertEquals("worlds", arguments.namespace());
|
||||||
|
assertEquals("export_one", arguments.output());
|
||||||
|
assertEquals("directory", arguments.format());
|
||||||
|
assertFalse(arguments.objects());
|
||||||
|
assertTrue(arguments.replace());
|
||||||
|
assertSame(DataVersion.V26_3, arguments.target());
|
||||||
|
assertEquals("unexpected argument 'underworld'", reason("overworld underworld"));
|
||||||
|
assertEquals("missing dimension", reason(" "));
|
||||||
|
assertEquals("missing dimension", reason("version=26.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultsMatchBukkit() {
|
||||||
|
ModdedDatapackExportArguments arguments = ModdedDatapackExportArguments.parse("overworld");
|
||||||
|
|
||||||
|
assertEquals("", arguments.version());
|
||||||
|
assertEquals("overworld", arguments.mode());
|
||||||
|
assertEquals("iris_export", arguments.namespace());
|
||||||
|
assertEquals("", arguments.output());
|
||||||
|
assertEquals("zip", arguments.format());
|
||||||
|
assertTrue(arguments.objects());
|
||||||
|
assertFalse(arguments.replace());
|
||||||
|
assertSame(DataVersion.V26_3, arguments.target());
|
||||||
|
|
||||||
|
Path pack = Path.of("packs", "overworld");
|
||||||
|
Path exportRoot = Path.of("exports", "datapacks");
|
||||||
|
WorldgenExportRequest expected = WorldgenExportRequest.builder()
|
||||||
|
.pack(pack)
|
||||||
|
.dimension("overworld")
|
||||||
|
.exportRoot(exportRoot)
|
||||||
|
.build();
|
||||||
|
assertEquals(expected, arguments.toRequest(pack, exportRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unsupportedValuesFailWithSharedMessages() {
|
||||||
|
assertEquals("Unsupported datapack export version '1.21.1'. Supported: 26.3",
|
||||||
|
reason("overworld version=1.21.1"));
|
||||||
|
assertEquals("Unsupported datapack export version '26.2'. Supported: 26.3",
|
||||||
|
reason("overworld version=26.2"));
|
||||||
|
assertEquals("Unknown export mode 'nether'. Expected: preset, dimension, overworld",
|
||||||
|
reason("overworld mode=nether"));
|
||||||
|
assertEquals("Export format must be directory or zip.", reason("overworld format=rar"));
|
||||||
|
assertEquals("objects must be true or false", reason("overworld objects=yes"));
|
||||||
|
assertEquals("replace must be true or false", reason("overworld replace=1"));
|
||||||
|
assertEquals("invalid dimension key 'a..b'", reason("a..b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String reason(String raw) {
|
||||||
|
return assertThrows(IllegalArgumentException.class, () -> ModdedDatapackExportArguments.parse(raw)).getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -821,6 +821,13 @@ project(':adapters:bukkit:nms:v26_3_R1') {
|
|||||||
}
|
}
|
||||||
sourceSets.test.compileClasspath += sourceSets.offline.output
|
sourceSets.test.compileClasspath += sourceSets.offline.output
|
||||||
sourceSets.test.runtimeClasspath += sourceSets.offline.output
|
sourceSets.test.runtimeClasspath += sourceSets.offline.output
|
||||||
|
tasks.named('test').configure {
|
||||||
|
String exportProbePack = providers.gradleProperty('iris.exportProbePack').orNull
|
||||||
|
if (exportProbePack != null) {
|
||||||
|
systemProperty('iris.exportProbePack', exportProbePack)
|
||||||
|
maxHeapSize = '6g'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
|
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
|
||||||
dependsOn(offlineClasses)
|
dependsOn(offlineClasses)
|
||||||
@@ -848,6 +855,30 @@ project(':adapters:bukkit:nms:v26_3_R1') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('datapackCostProbe', JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Exports an Iris dimension as a 26.3 worldgen datapack and measures its load, chunk and density function cost against vanilla.'
|
||||||
|
dependsOn(offlineClasses)
|
||||||
|
mainClass = 'art.arcane.iris.probe.DatapackCostProbe'
|
||||||
|
classpath = sourceSets.offline.runtimeClasspath
|
||||||
|
maxHeapSize = providers.gradleProperty('probeHeap').getOrElse('24G')
|
||||||
|
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||||
|
doFirst {
|
||||||
|
String dimension = providers.gradleProperty('probeDimension').getOrElse('overworld')
|
||||||
|
String output = providers.gradleProperty('probeOutput')
|
||||||
|
.getOrElse(layout.buildDirectory.file("reports/datapack-cost/${dimension}.json").get().asFile.absolutePath)
|
||||||
|
args([rootProject.file(providers.gradleProperty('probePack').get()).absolutePath, dimension,
|
||||||
|
providers.gradleProperty('probeSeed').getOrElse('1337'),
|
||||||
|
providers.gradleProperty('probeChunkSide').getOrElse('6'),
|
||||||
|
providers.gradleProperty('probeRounds').getOrElse('4'),
|
||||||
|
providers.gradleProperty('probeWarmupSide').getOrElse('6'),
|
||||||
|
providers.gradleProperty('probeCalls').getOrElse('256'),
|
||||||
|
providers.gradleProperty('probeCallSeconds').getOrElse('60'),
|
||||||
|
rootProject.file(output).absolutePath,
|
||||||
|
layout.buildDirectory.dir('tmp/datapack-cost').get().asFile.absolutePath])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register('regionMantleInspection', JavaExec) {
|
tasks.register('regionMantleInspection', JavaExec) {
|
||||||
group = 'verification'
|
group = 'verification'
|
||||||
dependsOn(testClasses)
|
dependsOn(testClasses)
|
||||||
@@ -877,6 +908,32 @@ project(':adapters:bukkit:nms:v26_3_R1') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.register('datapackFidelityProbe', JavaExec) {
|
||||||
|
group = 'verification'
|
||||||
|
description = 'Exports an Iris dimension as a 26.3 worldgen datapack and compares both generators on the same seed.'
|
||||||
|
dependsOn(offlineClasses)
|
||||||
|
mainClass = 'art.arcane.iris.probe.DatapackFidelityProbe'
|
||||||
|
classpath = sourceSets.offline.runtimeClasspath
|
||||||
|
maxHeapSize = '8G'
|
||||||
|
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
|
||||||
|
doFirst {
|
||||||
|
String dimension = providers.gradleProperty('probeDimension').get()
|
||||||
|
String seed = providers.gradleProperty('probeSeed').get()
|
||||||
|
String mode = providers.gradleProperty('probeMode').getOrElse('full')
|
||||||
|
boolean biomes = mode == 'biomes'
|
||||||
|
String output = providers.gradleProperty('probeOutput')
|
||||||
|
.getOrElse(layout.buildDirectory.file("reports/datapack-fidelity/${dimension}-${seed}${biomes ? '-biomes' : ''}.json").get().asFile.absolutePath)
|
||||||
|
args([rootProject.file(providers.gradleProperty('probePack').get()).absolutePath, dimension, seed,
|
||||||
|
biomes ? providers.gradleProperty('probeRadiusChunks').getOrElse('0') : providers.gradleProperty('probeRadiusChunks').get(),
|
||||||
|
biomes ? providers.gradleProperty('probeStride').getOrElse('1') : providers.gradleProperty('probeStride').get(),
|
||||||
|
rootProject.file(output).absolutePath,
|
||||||
|
providers.gradleProperty('probeSurveyBlocks').getOrElse('65536'),
|
||||||
|
providers.gradleProperty('probeSurveyStride').getOrElse('256'),
|
||||||
|
providers.gradleProperty('probeSurveySeeds').getOrElse(biomes ? '7' : '1'),
|
||||||
|
mode])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -193,6 +193,10 @@ tasks.named('test').configure {
|
|||||||
maxHeapSize = '2g'
|
maxHeapSize = '2g'
|
||||||
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
||||||
systemProperty('iris.generation.counters', 'true')
|
systemProperty('iris.generation.counters', 'true')
|
||||||
|
String exportProbePack = providers.gradleProperty('iris.exportProbePack').orNull
|
||||||
|
if (exportProbePack != null) {
|
||||||
|
systemProperty('iris.exportProbePack', exportProbePack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -360,14 +364,9 @@ List<String> unusedVolmLibPackages = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
List<String> unusedVolmLibClasses = [
|
List<String> unusedVolmLibClasses = [
|
||||||
'art/arcane/volmlib/util/nbt/io/ParseException',
|
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTDeserializer',
|
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTParser',
|
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
|
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
|
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
|
||||||
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
|
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
|
||||||
'art/arcane/volmlib/util/nbt/io/StringPointer',
|
|
||||||
'art/arcane/volmlib/util/io/StringDeserializer',
|
|
||||||
'art/arcane/volmlib/util/io/StringSerializer',
|
'art/arcane/volmlib/util/io/StringSerializer',
|
||||||
'art/arcane/volmlib/util/json/EnumType',
|
'art/arcane/volmlib/util/json/EnumType',
|
||||||
'art/arcane/volmlib/util/json/HTTP',
|
'art/arcane/volmlib/util/json/HTTP',
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ art/arcane/iris/studio/StudioSVC.java
|
|||||||
art/arcane/iris/studio/edit/BlockSignal.java
|
art/arcane/iris/studio/edit/BlockSignal.java
|
||||||
art/arcane/iris/studio/edit/DustRevealer.java
|
art/arcane/iris/studio/edit/DustRevealer.java
|
||||||
art/arcane/iris/studio/generation/JigsawStudioGenerator.java
|
art/arcane/iris/studio/generation/JigsawStudioGenerator.java
|
||||||
|
art/arcane/iris/studio/jigsaw/JigsawStudioConnectorTool.java
|
||||||
|
art/arcane/iris/studio/jigsaw/JigsawStudioFloorPainter.java
|
||||||
art/arcane/iris/studio/generation/ObjectStudioGenerator.java
|
art/arcane/iris/studio/generation/ObjectStudioGenerator.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioAutosaveScheduler.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioAutosaveScheduler.java
|
||||||
art/arcane/iris/studio/jigsaw/JigsawStudioCapture.java
|
art/arcane/iris/studio/jigsaw/JigsawStudioCapture.java
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.generation.validation;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
public final class ChunkSpiralScan {
|
||||||
|
private ChunkSpiralScan() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int scan(List<int[]> targets, int threads, ChunkVisitor visitor, Listener listener) throws InterruptedException {
|
||||||
|
Semaphore inFlight = new Semaphore(Math.max(1, threads));
|
||||||
|
CountDownLatch done = new CountDownLatch(targets.size());
|
||||||
|
AtomicInteger failures = new AtomicInteger();
|
||||||
|
|
||||||
|
for (int[] target : targets) {
|
||||||
|
int chunkX = target[0];
|
||||||
|
int chunkZ = target[1];
|
||||||
|
|
||||||
|
inFlight.acquire();
|
||||||
|
MultiBurst.burst.lazy(() -> {
|
||||||
|
boolean ok = false;
|
||||||
|
try {
|
||||||
|
visitor.visit(chunkX, chunkZ);
|
||||||
|
ok = true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failures.incrementAndGet();
|
||||||
|
IrisLogging.reportError(e);
|
||||||
|
listener.chunkFailed(chunkX, chunkZ, e);
|
||||||
|
} finally {
|
||||||
|
listener.chunkDone(chunkX, chunkZ, ok);
|
||||||
|
inFlight.release();
|
||||||
|
done.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
done.await();
|
||||||
|
return failures.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ChunkVisitor {
|
||||||
|
void visit(int chunkX, int chunkZ) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void chunkDone(int chunkX, int chunkZ, boolean ok);
|
||||||
|
|
||||||
|
void chunkFailed(int chunkX, int chunkZ, Throwable error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ import art.arcane.iris.spi.IrisLogging;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
import art.arcane.volmlib.util.math.ChunkSpiral;
|
import art.arcane.volmlib.util.math.ChunkSpiral;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
|
||||||
import art.arcane.volmlib.util.localization.MessageArgument;
|
import art.arcane.volmlib.util.localization.MessageArgument;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
|
||||||
@@ -44,8 +43,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.Semaphore;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public final class GoldenHashEngine {
|
public final class GoldenHashEngine {
|
||||||
@@ -210,38 +207,16 @@ public final class GoldenHashEngine {
|
|||||||
|
|
||||||
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
|
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
|
||||||
Map<Long, String> lines = new ConcurrentHashMap<>();
|
Map<Long, String> lines = new ConcurrentHashMap<>();
|
||||||
Semaphore inFlight = new Semaphore(threads);
|
ChunkSpiralScan.scan(targets, threads, (int chunkX, int chunkZ) -> hash(lines, chunkX, chunkZ), new ScanProgress(targets.size()));
|
||||||
CountDownLatch done = new CountDownLatch(targets.size());
|
return lines;
|
||||||
AtomicInteger completed = new AtomicInteger();
|
}
|
||||||
int total = targets.size();
|
|
||||||
|
|
||||||
for (int[] target : targets) {
|
private void hash(Map<Long, String> lines, int chunkX, int chunkZ) throws Exception {
|
||||||
int chunkX = target[0];
|
|
||||||
int chunkZ = target[1];
|
|
||||||
|
|
||||||
inFlight.acquire();
|
|
||||||
MultiBurst.burst.lazy(() -> {
|
|
||||||
boolean ok = false;
|
|
||||||
try {
|
|
||||||
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
|
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
|
||||||
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
|
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
|
||||||
if (request.deep()) {
|
if (request.deep()) {
|
||||||
writeDeepDump(chunkX, chunkZ, snapshot);
|
writeDeepDump(chunkX, chunkZ, snapshot);
|
||||||
}
|
}
|
||||||
ok = true;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
progress.chunkFailed(chunkX, chunkZ, e);
|
|
||||||
} finally {
|
|
||||||
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
|
|
||||||
inFlight.release();
|
|
||||||
done.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
done.await();
|
|
||||||
return lines;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) {
|
private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) {
|
||||||
@@ -562,4 +537,23 @@ public final class GoldenHashEngine {
|
|||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class ScanProgress implements ChunkSpiralScan.Listener {
|
||||||
|
private final AtomicInteger completed = new AtomicInteger();
|
||||||
|
private final int total;
|
||||||
|
|
||||||
|
private ScanProgress(int total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void chunkDone(int chunkX, int chunkZ, boolean ok) {
|
||||||
|
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void chunkFailed(int chunkX, int chunkZ, Throwable error) {
|
||||||
|
progress.chunkFailed(chunkX, chunkZ, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,74 @@ public final class DirectorCommandMessages {
|
|||||||
"iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list",
|
"iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list",
|
||||||
"The datapack id (folder name) shown by /iris datapack list"
|
"The datapack id (folder name) shown by /iris datapack list"
|
||||||
);
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_DESCRIPTION = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.director.export_dimension_vanilla_worldgen_datapack",
|
||||||
|
"Export a dimension as a vanilla worldgen datapack"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_DIMENSION = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_dimension",
|
||||||
|
"The dimension to export"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_VERSION = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_version",
|
||||||
|
"Target Minecraft version, 26.3 or later (blank uses the latest)"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_MODE = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_mode",
|
||||||
|
"Output mode: preset, dimension, or overworld"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_NAMESPACE = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_namespace",
|
||||||
|
"Namespace for the exported resources"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_OUTPUT = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_output",
|
||||||
|
"Output name in the datapack exports folder (blank uses the dimension and version)"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_FORMAT = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_format",
|
||||||
|
"zip or directory"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_OBJECTS = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_objects",
|
||||||
|
"Include objects as structure templates"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_REPLACE = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.param.export_replace",
|
||||||
|
"Replace an existing export with the same name"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_STARTED = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.started",
|
||||||
|
"Exporting {dimension} as a datapack for Minecraft {version}..."
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_STAGE = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.stage",
|
||||||
|
"Datapack export: {stage}"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_RESOURCES = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.resources",
|
||||||
|
"Datapack export: {done}/{total} resources"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_DIAGNOSTIC = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.diagnostic",
|
||||||
|
"{key}: {message}"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_EXPORTED = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.exported",
|
||||||
|
"Datapack exported: {path}"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_REJECTED = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.rejected",
|
||||||
|
"Datapack export rejected: {reason}"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_FAILED = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.failed",
|
||||||
|
"Datapack export failed: {reason}"
|
||||||
|
);
|
||||||
|
public static final TextKey COMMAND_DATAPACK_EXPORT_INVALID = TextKey.of(
|
||||||
|
"iris.director.commanddatapack.export.invalid",
|
||||||
|
"Invalid datapack export arguments: {reason}"
|
||||||
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER = TextKey.of(
|
||||||
"iris.director.commanddeveloper.director.iris_world_manager",
|
"iris.director.commanddeveloper.director.iris_world_manager",
|
||||||
"Iris World Manager"
|
"Iris World Manager"
|
||||||
@@ -888,6 +956,23 @@ public final class DirectorCommandMessages {
|
|||||||
COMMAND_DATAPACK_DIRECTOR_LIST_CONFIGURED_DATAPACK_IMPORTS_THEIR_INSTALLED_VERSIONS,
|
COMMAND_DATAPACK_DIRECTOR_LIST_CONFIGURED_DATAPACK_IMPORTS_THEIR_INSTALLED_VERSIONS,
|
||||||
COMMAND_DATAPACK_DIRECTOR_REMOVE_INSTALLED_DATAPACK_BY_ID_ALSO_DELETE_ITS_URL_FROM_DATAPACKIMPORTS_KEEP,
|
COMMAND_DATAPACK_DIRECTOR_REMOVE_INSTALLED_DATAPACK_BY_ID_ALSO_DELETE_ITS_URL_FROM_DATAPACKIMPORTS_KEEP,
|
||||||
COMMAND_DATAPACK_PARAM_DATAPACK_ID_FOLDER_NAME_SHOWN_BY_IRIS_DATAPACK_LIST,
|
COMMAND_DATAPACK_PARAM_DATAPACK_ID_FOLDER_NAME_SHOWN_BY_IRIS_DATAPACK_LIST,
|
||||||
|
COMMAND_DATAPACK_EXPORT_DESCRIPTION,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_DIMENSION,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_VERSION,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_MODE,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_NAMESPACE,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_OUTPUT,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_FORMAT,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_OBJECTS,
|
||||||
|
COMMAND_DATAPACK_EXPORT_PARAM_REPLACE,
|
||||||
|
COMMAND_DATAPACK_EXPORT_STARTED,
|
||||||
|
COMMAND_DATAPACK_EXPORT_STAGE,
|
||||||
|
COMMAND_DATAPACK_EXPORT_RESOURCES,
|
||||||
|
COMMAND_DATAPACK_EXPORT_DIAGNOSTIC,
|
||||||
|
COMMAND_DATAPACK_EXPORT_EXPORTED,
|
||||||
|
COMMAND_DATAPACK_EXPORT_REJECTED,
|
||||||
|
COMMAND_DATAPACK_EXPORT_FAILED,
|
||||||
|
COMMAND_DATAPACK_EXPORT_INVALID,
|
||||||
COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER,
|
COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER,
|
||||||
COMMAND_DEVELOPER_DIRECTOR_GET_LOADED_TECTONICPLATES_COUNT,
|
COMMAND_DEVELOPER_DIRECTOR_GET_LOADED_TECTONICPLATES_COUNT,
|
||||||
COMMAND_DEVELOPER_DIRECTOR_HASH_GENERATED_BLOCK_OUTPUT_FIXED_AREA_DETERMINISM_IDENTITY_TESTING,
|
COMMAND_DEVELOPER_DIRECTOR_HASH_GENERATED_BLOCK_OUTPUT_FIXED_AREA_DETERMINISM_IDENTITY_TESTING,
|
||||||
|
|||||||
@@ -13,20 +13,24 @@ import art.arcane.iris.pack.datapack.v263.DataFixerV263;
|
|||||||
//https://minecraft.wiki/w/Pack_format
|
//https://minecraft.wiki/w/Pack_format
|
||||||
@Getter
|
@Getter
|
||||||
public enum DataVersion {
|
public enum DataVersion {
|
||||||
UNSUPPORTED("0.0.0", 0, () -> null),
|
UNSUPPORTED("0.0.0", 0, 0, 0, () -> null),
|
||||||
V1_21_1("1.21.1", 48, DataFixerV1206::new),
|
V1_21_1("1.21.1", 48, 0, 3955, DataFixerV1206::new),
|
||||||
V26_1_2("26.1.2", 101, DataFixerV1217::new),
|
V26_1_2("26.1.2", 101, 1, 4790, DataFixerV1217::new),
|
||||||
V26_2("26.2", 107, DataFixerV1217::new),
|
V26_2("26.2", 107, 1, 4903, DataFixerV1217::new),
|
||||||
V26_3("26.3", 121, DataFixerV263::new);
|
V26_3("26.3", 121, 0, 5023, DataFixerV263::new);
|
||||||
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
|
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private final Supplier<IDataFixer> constructor;
|
private final Supplier<IDataFixer> constructor;
|
||||||
private final String version;
|
private final String version;
|
||||||
private final int packFormat;
|
private final int packFormat;
|
||||||
|
private final int packFormatMinor;
|
||||||
|
private final int worldDataVersion;
|
||||||
|
|
||||||
DataVersion(String version, int packFormat, Supplier<IDataFixer> constructor) {
|
DataVersion(String version, int packFormat, int packFormatMinor, int worldDataVersion, Supplier<IDataFixer> constructor) {
|
||||||
this.constructor = constructor;
|
this.constructor = constructor;
|
||||||
this.packFormat = packFormat;
|
this.packFormat = packFormat;
|
||||||
|
this.packFormatMinor = packFormatMinor;
|
||||||
|
this.worldDataVersion = worldDataVersion;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package art.arcane.iris.pack.datapack;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DatapackIngestService.Entry;
|
||||||
|
import art.arcane.iris.pack.datapack.ModrinthResolver.ResolvedDatapack;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.volmlib.util.io.IO;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class DatapackExportArchives {
|
||||||
|
private static final String DIRECT_VERSION_PREFIX = "direct-";
|
||||||
|
|
||||||
|
private DatapackExportArchives() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path resolve(String source, DataVersion target) throws IOException {
|
||||||
|
String normalized = DatapackImportSources.normalizeConfiguredSource(Objects.requireNonNull(source, "datapack import source"));
|
||||||
|
URI uri = DatapackStartupValidation.parseSourceUri(normalized);
|
||||||
|
if (uri == null) {
|
||||||
|
throw new IOException("Empty datapack import source");
|
||||||
|
}
|
||||||
|
if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||||
|
return DatapackArchive.requireLocalDatapackPath(uri);
|
||||||
|
}
|
||||||
|
IrisPlatform platform = IrisPlatforms.get();
|
||||||
|
File root = platform.dataFolder("datapacks");
|
||||||
|
File cacheDir = new File(root, "cache");
|
||||||
|
boolean running = DataVersion.forMinecraftVersion(platform.minecraftVersion()) == target;
|
||||||
|
DatapackTransactions.TRANSACTION_LOCK.lock();
|
||||||
|
try {
|
||||||
|
Entry entry = manifestEntry(root, normalized);
|
||||||
|
Path cached = ingestCache(cacheDir, entry, running);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
return download(cacheDir, normalized, entry, target);
|
||||||
|
} finally {
|
||||||
|
DatapackTransactions.TRANSACTION_LOCK.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Entry manifestEntry(File root, String source) {
|
||||||
|
try {
|
||||||
|
return DatapackManifestStore.readCommittedManifest(root).find(source);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
IrisLogging.reportError("The datapack manifest under " + root.getPath() + " could not be read; '" + source
|
||||||
|
+ "' is resolved again for the export", exception);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path ingestCache(File cacheDir, Entry entry, boolean running) throws IOException {
|
||||||
|
if (entry == null || entry.id == null || entry.sha1 == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean direct = entry.versionId != null && entry.versionId.startsWith(DIRECT_VERSION_PREFIX);
|
||||||
|
if (!direct && !running) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
File archive = new File(cacheDir, entry.id + "-" + DatapackSupport.safeFile(entry.versionId) + ".zip");
|
||||||
|
return matches(archive, entry.sha1) ? archive.toPath() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path download(File cacheDir, String source, Entry entry, DataVersion target) throws IOException {
|
||||||
|
ResolvedDatapack resolved = ModrinthResolver.resolve(source, target.getVersion());
|
||||||
|
String id = entry == null || entry.id == null ? DatapackArchive.deriveId(resolved) : entry.id;
|
||||||
|
File archive = new File(cacheDir, id + "-" + DatapackSupport.safeFile(resolved.getVersionId()) + ".zip");
|
||||||
|
String expected = resolved.getSha1();
|
||||||
|
if (expected != null && !expected.isBlank() && matches(archive, expected)) {
|
||||||
|
return archive.toPath();
|
||||||
|
}
|
||||||
|
DatapackSupport.ensureScratchDirectory(cacheDir, "datapack download cache");
|
||||||
|
DatapackArchive.download(resolved.getDownloadUrl(), archive, null, null);
|
||||||
|
if (expected != null && !expected.isBlank()) {
|
||||||
|
String checksum = DatapackArchive.sha1(archive);
|
||||||
|
if (!expected.equalsIgnoreCase(checksum)) {
|
||||||
|
IO.delete(archive);
|
||||||
|
throw new IOException("Checksum mismatch for " + id + " (expected " + expected + ", got " + checksum + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return archive.toPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean matches(File archive, String sha1) throws IOException {
|
||||||
|
Path path = archive.toPath();
|
||||||
|
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sha1.equalsIgnoreCase(DatapackArchive.sha1(archive));
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-16
@@ -1,4 +1,4 @@
|
|||||||
package art.arcane.iris.structure.export;
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
@@ -14,25 +14,26 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
final class AtomicDatapackPublisher {
|
public final class AtomicDatapackPublisher {
|
||||||
Publication publish(
|
public Publication publish(
|
||||||
Map<String, byte[]> resources,
|
Map<String, byte[]> files,
|
||||||
Path output,
|
Path destination,
|
||||||
VanillaJigsawExportFormat format,
|
DatapackExportFormat format,
|
||||||
boolean replaceExisting
|
boolean replace,
|
||||||
|
String stagingPrefix
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
Path parent = output.getParent();
|
Path parent = destination.getParent();
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
throw new IOException("Export output has no parent directory: " + output);
|
throw new IOException("Export output has no parent directory: " + destination);
|
||||||
}
|
}
|
||||||
Files.createDirectories(parent);
|
Files.createDirectories(parent);
|
||||||
Path staging = Files.createTempDirectory(parent, ".iris-jigsaw-export-");
|
Path staging = Files.createTempDirectory(parent, stagingPrefix);
|
||||||
List<String> cleanupWarnings = new ArrayList<>();
|
List<String> cleanupWarnings = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
Path artifact = format == VanillaJigsawExportFormat.DIRECTORY
|
Path artifact = format == DatapackExportFormat.DIRECTORY
|
||||||
? createDirectoryArtifact(staging, resources)
|
? createDirectoryArtifact(staging, files)
|
||||||
: createZipArtifact(staging, resources);
|
: createZipArtifact(staging, files);
|
||||||
replaceAtomically(artifact, output, replaceExisting, cleanupWarnings);
|
replaceAtomically(artifact, destination, replace, cleanupWarnings);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
deleteTree(staging);
|
deleteTree(staging);
|
||||||
@@ -152,8 +153,8 @@ final class AtomicDatapackPublisher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record Publication(List<String> cleanupWarnings) {
|
public record Publication(List<String> cleanupWarnings) {
|
||||||
Publication {
|
public Publication {
|
||||||
cleanupWarnings = List.copyOf(cleanupWarnings);
|
cleanupWarnings = List.copyOf(cleanupWarnings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
public record ContentTrim(int filesBefore, long bytesBefore, int filesAfter, long bytesAfter) {
|
||||||
|
public static final ContentTrim NONE = new ContentTrim(0, 0L, 0, 0L);
|
||||||
|
|
||||||
|
public ContentTrim {
|
||||||
|
if (filesBefore < 0 || bytesBefore < 0L || filesAfter < 0 || bytesAfter < 0L) {
|
||||||
|
throw new IllegalArgumentException("Content trim counts cannot be negative");
|
||||||
|
}
|
||||||
|
if (filesAfter > filesBefore || bytesAfter > bytesBefore) {
|
||||||
|
throw new IllegalArgumentException("A content trim cannot add files or bytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int filesRemoved() {
|
||||||
|
return filesBefore - filesAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long bytesRemoved() {
|
||||||
|
return bytesBefore - bytesAfter;
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class DatapackExportDestinations {
|
||||||
|
private static final Pattern OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
|
||||||
|
|
||||||
|
private static final Set<Path> CLAIMED = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private DatapackExportDestinations() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DatapackExportFormat parseFormat(String raw) {
|
||||||
|
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
|
||||||
|
case "zip" -> DatapackExportFormat.ZIP;
|
||||||
|
case "directory", "dir", "folder" -> DatapackExportFormat.DIRECTORY;
|
||||||
|
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path resolve(Path exportRoot, String output, DatapackExportFormat format) {
|
||||||
|
Path root = Objects.requireNonNull(exportRoot, "Datapack export root").toAbsolutePath().normalize();
|
||||||
|
String requested = Objects.requireNonNull(output, "Datapack export output name");
|
||||||
|
String name = requested.trim();
|
||||||
|
if (!name.equals(requested) || !OUTPUT_NAME.matcher(name).matches()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
|
||||||
|
}
|
||||||
|
String fileName = format == DatapackExportFormat.ZIP && !name.endsWith(".zip") ? name + ".zip" : name;
|
||||||
|
Path destination = root.resolve(fileName).normalize();
|
||||||
|
if (destination.equals(root) || !root.equals(destination.getParent())) {
|
||||||
|
throw new IllegalArgumentException("Export output must be one artifact inside the exports folder.");
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean claim(Path destination) {
|
||||||
|
return CLAIMED.add(normalized(destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void release(Path destination) {
|
||||||
|
CLAIMED.remove(normalized(destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path normalized(Path destination) {
|
||||||
|
return Objects.requireNonNull(destination, "Datapack export destination").toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
public enum DatapackExportFormat {
|
||||||
|
ZIP,
|
||||||
|
DIRECTORY
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public record GenerationCost(Map<GenerationCost.Root, GenerationCost.RootCost> roots, long densityFunctionBytes) {
|
||||||
|
public static final GenerationCost NONE = new GenerationCost(Map.of(), 0L);
|
||||||
|
private static final String DATA_PREFIX = "data/";
|
||||||
|
private static final String DENSITY_FUNCTION_FOLDER = "/worldgen/density_function/";
|
||||||
|
private static final String JSON_SUFFIX = ".json";
|
||||||
|
|
||||||
|
public GenerationCost {
|
||||||
|
EnumMap<Root, RootCost> copy = new EnumMap<>(Root.class);
|
||||||
|
copy.putAll(roots);
|
||||||
|
roots = Collections.unmodifiableMap(copy);
|
||||||
|
if (densityFunctionBytes < 0L) {
|
||||||
|
throw new IllegalArgumentException("Density function JSON size cannot be negative, got " + densityFunctionBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long densityFunctionBytes(Map<String, byte[]> files) {
|
||||||
|
long bytes = 0L;
|
||||||
|
for (Map.Entry<String, byte[]> file : files.entrySet()) {
|
||||||
|
String path = file.getKey();
|
||||||
|
if (path.startsWith(DATA_PREFIX) && path.contains(DENSITY_FUNCTION_FOLDER) && path.endsWith(JSON_SUFFIX)) {
|
||||||
|
bytes += file.getValue().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<RootCost> root(Root root) {
|
||||||
|
return Optional.ofNullable(roots.get(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenerationCost withDensityFunctionBytes(long bytes) {
|
||||||
|
return new GenerationCost(roots, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Root {
|
||||||
|
TEMPERATURE("temperature", true),
|
||||||
|
VEGETATION("vegetation", true),
|
||||||
|
CONTINENTS("continents", true),
|
||||||
|
EROSION("erosion", true),
|
||||||
|
DEPTH("depth", true),
|
||||||
|
RIDGES("ridges", true),
|
||||||
|
CHUNK_SURFACE_LEVEL("chunk_surface_level", false),
|
||||||
|
FINAL_DENSITY("final_density", false),
|
||||||
|
AQUIFER_BARRIER("aquifer.barrier", false),
|
||||||
|
AQUIFER_FLOODEDNESS("aquifer.fluid_level_floodedness", false),
|
||||||
|
AQUIFER_SPREAD("aquifer.fluid_level_spread", false),
|
||||||
|
AQUIFER_LAVA("aquifer.lava", false),
|
||||||
|
AQUIFER_EXCLUSION("aquifer.exclusion", false),
|
||||||
|
AQUIFER_SURFACE_LEVEL("aquifer.surface_level", false);
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final boolean climate;
|
||||||
|
|
||||||
|
Root(String id, boolean climate) {
|
||||||
|
this.id = id;
|
||||||
|
this.climate = climate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean climate() {
|
||||||
|
return climate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record RootCost(double worstCaseNodes, double expandedNodes, long distinctNodes) {
|
||||||
|
public RootCost {
|
||||||
|
if (!(worstCaseNodes >= 0.0) || !(expandedNodes >= 0.0) || distinctNodes < 0L) {
|
||||||
|
throw new IllegalArgumentException("Generation cost counts cannot be negative, got " + worstCaseNodes + " worst case, "
|
||||||
|
+ expandedNodes + " expanded and " + distinctNodes + " distinct");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.TileData;
|
||||||
|
import art.arcane.iris.generation.geometry.IrisBlockVector;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.structure.object.IrisObject;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.math.Vector3i;
|
||||||
|
import art.arcane.volmlib.util.nbt.io.NBTUtil;
|
||||||
|
import art.arcane.volmlib.util.nbt.io.NamedTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.IntTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.ListTag;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
public final class StructureTemplateEncoder {
|
||||||
|
private static final Comparator<TemplateBlock> BLOCK_ORDER = Comparator
|
||||||
|
.comparingInt(TemplateBlock::y)
|
||||||
|
.thenComparingInt(TemplateBlock::x)
|
||||||
|
.thenComparingInt(TemplateBlock::z);
|
||||||
|
|
||||||
|
public static TemplateVolume volume(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) {
|
||||||
|
List<TemplateBlock> blocks = new ArrayList<>();
|
||||||
|
Vector3i center = object.getCenter();
|
||||||
|
int droppedTiles = 0;
|
||||||
|
String firstDropped = null;
|
||||||
|
int outside = 0;
|
||||||
|
String firstOutside = null;
|
||||||
|
Set<String> customBlocks = new TreeSet<>();
|
||||||
|
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : object.getBlocks()) {
|
||||||
|
IrisBlockVector signed = entry.getKey();
|
||||||
|
int x = signed.getBlockX() + center.getX();
|
||||||
|
int y = signed.getBlockY() + center.getY();
|
||||||
|
int z = signed.getBlockZ() + center.getZ();
|
||||||
|
if (x < 0 || y < 0 || z < 0 || x >= object.getW() || y >= object.getH() || z >= object.getD()) {
|
||||||
|
outside++;
|
||||||
|
if (firstOutside == null) {
|
||||||
|
firstOutside = x + "," + y + "," + z;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VanillaBlockState state;
|
||||||
|
try {
|
||||||
|
state = VanillaBlockState.parse(entry.getValue().key());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
customBlocks.add(entry.getValue().key());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TileData tile = object.getStates().get(signed);
|
||||||
|
CompoundTag nbt = null;
|
||||||
|
if (tile != null) {
|
||||||
|
Optional<CompoundTag> converted = TileEntityNbt.toNbt(tile.getMaterialKey(), tile.getProperties());
|
||||||
|
if (converted.isPresent()) {
|
||||||
|
nbt = converted.get();
|
||||||
|
} else {
|
||||||
|
droppedTiles++;
|
||||||
|
if (firstDropped == null) {
|
||||||
|
firstDropped = state.name() + " at " + x + "," + y + "," + z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blocks.add(new TemplateBlock(x, y, z, state, nbt));
|
||||||
|
}
|
||||||
|
if (!customBlocks.isEmpty()) {
|
||||||
|
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.CUSTOM_BLOCK, resource,
|
||||||
|
"Blocks outside Minecraft are left out of the template: " + String.join(", ", customBlocks)));
|
||||||
|
}
|
||||||
|
if (outside > 0) {
|
||||||
|
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.OBJECT_BOUNDS, resource,
|
||||||
|
outside + " blocks outside the " + object.getW() + "x" + object.getH() + "x" + object.getD()
|
||||||
|
+ " object size are left out of the template, first at " + firstOutside));
|
||||||
|
}
|
||||||
|
if (droppedTiles > 0) {
|
||||||
|
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.TILE_ENTITY, resource,
|
||||||
|
droppedTiles + " tile entities have no vanilla block entity form and export as plain blocks, first "
|
||||||
|
+ firstDropped));
|
||||||
|
}
|
||||||
|
return new TemplateVolume(object.getW(), object.getH(), object.getD(), blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encode(IrisObject object, List<TemplateBlock> extraBlocks, DataVersion target) throws IOException {
|
||||||
|
List<ExportDiagnostic> lossy = new ArrayList<>();
|
||||||
|
TemplateVolume volume = volume(object, object.getLoadKey(), lossy);
|
||||||
|
if (!lossy.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Object '" + object.getLoadKey() + "' cannot be encoded: " + lossy.getFirst().message());
|
||||||
|
}
|
||||||
|
Map<BlockPosition, TemplateBlock> blocks = new LinkedHashMap<>();
|
||||||
|
for (TemplateBlock block : volume.blocks()) {
|
||||||
|
blocks.put(new BlockPosition(block.x(), block.y(), block.z()), block);
|
||||||
|
}
|
||||||
|
for (TemplateBlock extra : extraBlocks) {
|
||||||
|
blocks.put(new BlockPosition(extra.x(), extra.y(), extra.z()), extra);
|
||||||
|
}
|
||||||
|
return encode(new TemplateVolume(volume.sizeX(), volume.sizeY(), volume.sizeZ(), new ArrayList<>(blocks.values())), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] encode(TemplateVolume volume, DataVersion target) throws IOException {
|
||||||
|
MCABlockStateCodecSupport.Format format = paletteFormat(target);
|
||||||
|
List<TemplateBlock> orderedBlocks = new ArrayList<>(volume.blocks());
|
||||||
|
orderedBlocks.sort(BLOCK_ORDER);
|
||||||
|
for (int index = 1; index < orderedBlocks.size(); index++) {
|
||||||
|
if (BLOCK_ORDER.compare(orderedBlocks.get(index - 1), orderedBlocks.get(index)) == 0) {
|
||||||
|
TemplateBlock duplicate = orderedBlocks.get(index);
|
||||||
|
throw new IllegalArgumentException("Template has two blocks at " + duplicate.x() + "," + duplicate.y() + "," + duplicate.z());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Integer> paletteIndexes = new LinkedHashMap<>();
|
||||||
|
List<VanillaBlockState> paletteStates = new ArrayList<>();
|
||||||
|
for (TemplateBlock block : orderedBlocks) {
|
||||||
|
String key = block.state().canonical();
|
||||||
|
if (!paletteIndexes.containsKey(key)) {
|
||||||
|
paletteIndexes.put(key, paletteStates.size());
|
||||||
|
paletteStates.add(block.state());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompoundTag root = new CompoundTag();
|
||||||
|
root.put("size", intList(volume.sizeX(), volume.sizeY(), volume.sizeZ()));
|
||||||
|
root.put("palette", palette(paletteStates, format));
|
||||||
|
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
|
||||||
|
root.put("entities", new ListTag<>(CompoundTag.class));
|
||||||
|
root.putInt("DataVersion", target.getWorldDataVersion());
|
||||||
|
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
try (GZIPOutputStream gzip = new GZIPOutputStream(output)) {
|
||||||
|
NBTUtil.write(new NamedTag("", root), gzip, false);
|
||||||
|
}
|
||||||
|
return output.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MCABlockStateCodecSupport.Format paletteFormat(DataVersion target) {
|
||||||
|
return switch (target) {
|
||||||
|
case V26_3 -> MCABlockStateCodecSupport.Format.LOWERCASE;
|
||||||
|
case V1_21_1, V26_1_2, V26_2 -> MCABlockStateCodecSupport.Format.CAPITALIZED;
|
||||||
|
case UNSUPPORTED -> throw new IllegalArgumentException("Structure templates need a supported target version");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListTag<CompoundTag> palette(List<VanillaBlockState> states, MCABlockStateCodecSupport.Format format) {
|
||||||
|
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
|
||||||
|
for (VanillaBlockState state : states) {
|
||||||
|
palette.add(state.toNbt(format));
|
||||||
|
}
|
||||||
|
return palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListTag<CompoundTag> blocks(List<TemplateBlock> entries, Map<String, Integer> paletteIndexes) {
|
||||||
|
ListTag<CompoundTag> blocks = new ListTag<>(CompoundTag.class);
|
||||||
|
for (TemplateBlock entry : entries) {
|
||||||
|
CompoundTag block = new CompoundTag();
|
||||||
|
block.put("pos", intList(entry.x(), entry.y(), entry.z()));
|
||||||
|
block.putInt("state", paletteIndexes.get(entry.state().canonical()));
|
||||||
|
if (entry.nbt() != null) {
|
||||||
|
block.put("nbt", entry.nbt());
|
||||||
|
}
|
||||||
|
blocks.add(block);
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListTag<IntTag> intList(int... values) {
|
||||||
|
ListTag<IntTag> list = new ListTag<>(IntTag.class);
|
||||||
|
for (int value : values) {
|
||||||
|
list.add(new IntTag(value));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record BlockPosition(int x, int y, int z) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record TemplateBlock(int x, int y, int z, VanillaBlockState state, CompoundTag nbt) {
|
||||||
|
public TemplateBlock {
|
||||||
|
Objects.requireNonNull(state, "Template block state");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record TemplateVolume(int sizeX, int sizeY, int sizeZ, List<TemplateBlock> blocks) {
|
||||||
|
public TemplateVolume {
|
||||||
|
if (sizeX < 0 || sizeY < 0 || sizeZ < 0) {
|
||||||
|
throw new IllegalArgumentException("Template size must not be negative, got " + sizeX + "x" + sizeY + "x" + sizeZ);
|
||||||
|
}
|
||||||
|
blocks = List.copyOf(blocks);
|
||||||
|
for (TemplateBlock block : blocks) {
|
||||||
|
if (block.x() < 0 || block.y() < 0 || block.z() < 0 || block.x() >= sizeX || block.y() >= sizeY || block.z() >= sizeZ) {
|
||||||
|
throw new IllegalArgumentException("Template block at " + block.x() + "," + block.y() + "," + block.z()
|
||||||
|
+ " lies outside the " + sizeX + "x" + sizeY + "x" + sizeZ + " template");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int footprint() {
|
||||||
|
return Math.max(sizeX, sizeZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.nbt.io.SNBTDeserializer;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.ByteTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.DoubleTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.EndTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.FloatTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.IntArrayTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.IntTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.ListTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.LongTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.ShortTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.StringTag;
|
||||||
|
import art.arcane.volmlib.util.nbt.tag.Tag;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public final class TileEntityNbt {
|
||||||
|
private static final String SNBT_PROPERTY = "nbt";
|
||||||
|
private static final String ID = "id";
|
||||||
|
private static final String PROFILE = "profile";
|
||||||
|
private static final String UUID = "UUID";
|
||||||
|
private static final int MAX_DEPTH = 64;
|
||||||
|
private static final Set<String> POSITION_KEYS = Set.of("x", "y", "z");
|
||||||
|
private static final Map<String, String> BLOCK_ENTITIES = Map.ofEntries(
|
||||||
|
Map.entry("minecraft:chest", "minecraft:chest"),
|
||||||
|
Map.entry("minecraft:trapped_chest", "minecraft:trapped_chest"),
|
||||||
|
Map.entry("minecraft:ender_chest", "minecraft:ender_chest"),
|
||||||
|
Map.entry("minecraft:barrel", "minecraft:barrel"),
|
||||||
|
Map.entry("minecraft:furnace", "minecraft:furnace"),
|
||||||
|
Map.entry("minecraft:smoker", "minecraft:smoker"),
|
||||||
|
Map.entry("minecraft:blast_furnace", "minecraft:blast_furnace"),
|
||||||
|
Map.entry("minecraft:dispenser", "minecraft:dispenser"),
|
||||||
|
Map.entry("minecraft:dropper", "minecraft:dropper"),
|
||||||
|
Map.entry("minecraft:hopper", "minecraft:hopper"),
|
||||||
|
Map.entry("minecraft:brewing_stand", "minecraft:brewing_stand"),
|
||||||
|
Map.entry("minecraft:beacon", "minecraft:beacon"),
|
||||||
|
Map.entry("minecraft:lectern", "minecraft:lectern"),
|
||||||
|
Map.entry("minecraft:jukebox", "minecraft:jukebox"),
|
||||||
|
Map.entry("minecraft:enchanting_table", "minecraft:enchanting_table"),
|
||||||
|
Map.entry("minecraft:conduit", "minecraft:conduit"),
|
||||||
|
Map.entry("minecraft:bell", "minecraft:bell"),
|
||||||
|
Map.entry("minecraft:crafter", "minecraft:crafter"),
|
||||||
|
Map.entry("minecraft:daylight_detector", "minecraft:daylight_detector"),
|
||||||
|
Map.entry("minecraft:comparator", "minecraft:comparator"),
|
||||||
|
Map.entry("minecraft:end_gateway", "minecraft:end_gateway"),
|
||||||
|
Map.entry("minecraft:end_portal", "minecraft:end_portal"),
|
||||||
|
Map.entry("minecraft:sculk_sensor", "minecraft:sculk_sensor"),
|
||||||
|
Map.entry("minecraft:calibrated_sculk_sensor", "minecraft:calibrated_sculk_sensor"),
|
||||||
|
Map.entry("minecraft:sculk_catalyst", "minecraft:sculk_catalyst"),
|
||||||
|
Map.entry("minecraft:sculk_shrieker", "minecraft:sculk_shrieker"),
|
||||||
|
Map.entry("minecraft:chiseled_bookshelf", "minecraft:chiseled_bookshelf"),
|
||||||
|
Map.entry("minecraft:decorated_pot", "minecraft:decorated_pot"),
|
||||||
|
Map.entry("minecraft:creaking_heart", "minecraft:creaking_heart"),
|
||||||
|
Map.entry("minecraft:trial_spawner", "minecraft:trial_spawner"),
|
||||||
|
Map.entry("minecraft:vault", "minecraft:vault"),
|
||||||
|
Map.entry("minecraft:beehive", "minecraft:beehive"),
|
||||||
|
Map.entry("minecraft:bee_nest", "minecraft:beehive"),
|
||||||
|
Map.entry("minecraft:spawner", "minecraft:mob_spawner"),
|
||||||
|
Map.entry("minecraft:campfire", "minecraft:campfire"),
|
||||||
|
Map.entry("minecraft:soul_campfire", "minecraft:campfire"),
|
||||||
|
Map.entry("minecraft:command_block", "minecraft:command_block"),
|
||||||
|
Map.entry("minecraft:chain_command_block", "minecraft:command_block"),
|
||||||
|
Map.entry("minecraft:repeating_command_block", "minecraft:command_block"),
|
||||||
|
Map.entry("minecraft:suspicious_sand", "minecraft:brushable_block"),
|
||||||
|
Map.entry("minecraft:suspicious_gravel", "minecraft:brushable_block"));
|
||||||
|
private static final List<Map.Entry<String, String>> SUFFIX_BLOCK_ENTITIES = List.of(
|
||||||
|
Map.entry("_hanging_sign", "minecraft:hanging_sign"),
|
||||||
|
Map.entry("_sign", "minecraft:sign"),
|
||||||
|
Map.entry("_banner", "minecraft:banner"),
|
||||||
|
Map.entry("_head", "minecraft:skull"),
|
||||||
|
Map.entry("_skull", "minecraft:skull"),
|
||||||
|
Map.entry("_bed", "minecraft:bed"),
|
||||||
|
Map.entry("shulker_box", "minecraft:shulker_box"),
|
||||||
|
Map.entry("_shelf", "minecraft:shelf"),
|
||||||
|
Map.entry("copper_golem_statue", "minecraft:copper_golem_statue"));
|
||||||
|
private static final Set<String> BYTE_FIELDS = Set.of(
|
||||||
|
"Slot", "Count", "has_glowing_text", "is_waxed", "auto", "conditionMet", "powered", "TrackOutput",
|
||||||
|
"UpdateLastExecution", "OnGround", "Invulnerable", "Silent", "NoGravity", "Glowing",
|
||||||
|
"PersistenceRequired", "CanPickUpLoot", "LeftHanded", "NoAI", "ExactTeleport", "show_particles",
|
||||||
|
"show_icon", "ambient");
|
||||||
|
private static final Set<String> SHORT_FIELDS = Set.of(
|
||||||
|
"Delay", "MinSpawnDelay", "MaxSpawnDelay", "SpawnCount", "MaxNearbyEntities", "RequiredPlayerRange",
|
||||||
|
"SpawnRange", "BurnTime", "CookTime", "CookTimeTotal", "lit_time_remaining", "lit_total_time",
|
||||||
|
"cooking_time_spent", "cooking_total_time", "Fire", "Air", "HurtTime", "DeathTime");
|
||||||
|
private static final Set<String> LONG_FIELDS = Set.of("LootTableSeed", "Age", "LastExecution", "ticks_since_song_started");
|
||||||
|
private static final Set<String> FLOAT_FIELDS = Set.of("Health", "AbsorptionAmount", "Rotation", "HandDropChances", "ArmorDropChances");
|
||||||
|
|
||||||
|
private TileEntityNbt() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<String> blockEntityId(String blockKey) {
|
||||||
|
String key = normalizeBlockKey(blockKey);
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String exact = BLOCK_ENTITIES.get(key);
|
||||||
|
if (exact != null) {
|
||||||
|
return Optional.of(exact);
|
||||||
|
}
|
||||||
|
if (!key.startsWith("minecraft:")) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> suffix : SUFFIX_BLOCK_ENTITIES) {
|
||||||
|
if (key.endsWith(suffix.getKey())) {
|
||||||
|
return Optional.of(suffix.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<CompoundTag> toNbt(String blockKey, Map<String, Object> properties) {
|
||||||
|
Optional<String> blockEntity = blockEntityId(blockKey);
|
||||||
|
if (blockEntity.isEmpty() || properties == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
CompoundTag nbt = parseSnbt(properties.get(SNBT_PROPERTY));
|
||||||
|
if (nbt == null) {
|
||||||
|
nbt = compound(properties, "", 0);
|
||||||
|
}
|
||||||
|
for (String key : POSITION_KEYS) {
|
||||||
|
nbt.remove(key);
|
||||||
|
}
|
||||||
|
nbt.remove(SNBT_PROPERTY);
|
||||||
|
if (nbt.size() == 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (!nbt.containsKey(ID)) {
|
||||||
|
nbt.putString(ID, blockEntity.get());
|
||||||
|
}
|
||||||
|
return Optional.of(nbt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeBlockKey(String blockKey) {
|
||||||
|
if (blockKey == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String trimmed = blockKey.trim().toLowerCase(Locale.ROOT);
|
||||||
|
int properties = trimmed.indexOf('[');
|
||||||
|
String base = properties < 0 ? trimmed : trimmed.substring(0, properties);
|
||||||
|
if (base.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return base.indexOf(':') < 0 ? "minecraft:" + base : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag parseSnbt(Object snbt) {
|
||||||
|
if (!(snbt instanceof String text) || text.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Tag<?> parsed = new SNBTDeserializer().fromString(text);
|
||||||
|
return parsed instanceof CompoundTag compound ? compound : null;
|
||||||
|
} catch (IOException | RuntimeException exception) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompoundTag compound(Map<?, ?> values, String parent, int depth) {
|
||||||
|
CompoundTag compound = new CompoundTag();
|
||||||
|
if (depth > MAX_DEPTH) {
|
||||||
|
return compound;
|
||||||
|
}
|
||||||
|
Map<String, Object> sorted = new TreeMap<>();
|
||||||
|
for (Map.Entry<?, ?> entry : values.entrySet()) {
|
||||||
|
if (entry.getKey() != null && entry.getValue() != null) {
|
||||||
|
sorted.put(String.valueOf(entry.getKey()), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (depth == 0) {
|
||||||
|
sorted.remove(SNBT_PROPERTY);
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Object> entry : sorted.entrySet()) {
|
||||||
|
Tag<?> tag = tag(entry.getKey(), parent, entry.getValue(), depth + 1);
|
||||||
|
if (tag != null) {
|
||||||
|
compound.put(entry.getKey(), tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compound;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Tag<?> tag(String key, String parent, Object value, int depth) {
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
return compound(map, key, depth);
|
||||||
|
}
|
||||||
|
if (value instanceof List<?> list) {
|
||||||
|
return list(key, parent, list, depth);
|
||||||
|
}
|
||||||
|
if (value instanceof Boolean bool) {
|
||||||
|
return new ByteTag(bool);
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number(numberKind(key, number), number);
|
||||||
|
}
|
||||||
|
return new StringTag(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Tag<?> list(String key, String parent, List<?> values, int depth) {
|
||||||
|
if (intArray(key, parent) && allNumbers(values)) {
|
||||||
|
int[] ints = new int[values.size()];
|
||||||
|
for (int index = 0; index < ints.length; index++) {
|
||||||
|
ints[index] = ((Number) values.get(index)).intValue();
|
||||||
|
}
|
||||||
|
return new IntArrayTag(ints);
|
||||||
|
}
|
||||||
|
NumberKind listKind = allNumbers(values) ? listNumberKind(key, values) : null;
|
||||||
|
ListTag<?> list = ListTag.createUnchecked(EndTag.class);
|
||||||
|
Class<?> elementType = null;
|
||||||
|
for (Object value : values) {
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Tag<?> element = listKind == null ? tag(key, parent, value, depth + 1) : number(listKind, (Number) value);
|
||||||
|
if (element == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (elementType == null) {
|
||||||
|
elementType = element.getClass();
|
||||||
|
}
|
||||||
|
if (elementType != element.getClass()) {
|
||||||
|
return stringList(values);
|
||||||
|
}
|
||||||
|
list.addUnchecked(element);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ListTag<?> stringList(List<?> values) {
|
||||||
|
ListTag<?> list = ListTag.createUnchecked(EndTag.class);
|
||||||
|
for (Object value : values) {
|
||||||
|
if (value != null) {
|
||||||
|
list.addUnchecked(new StringTag(String.valueOf(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean intArray(String key, String parent) {
|
||||||
|
return UUID.equals(key) || (ID.equals(key) && PROFILE.equals(parent));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean allNumbers(List<?> values) {
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (Object value : values) {
|
||||||
|
if (!(value instanceof Number)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NumberKind listNumberKind(String key, List<?> values) {
|
||||||
|
NumberKind declared = declaredKind(key);
|
||||||
|
if (declared != null) {
|
||||||
|
return declared;
|
||||||
|
}
|
||||||
|
NumberKind widest = NumberKind.INT;
|
||||||
|
for (Object value : values) {
|
||||||
|
NumberKind kind = inferredKind((Number) value);
|
||||||
|
if (kind.ordinal() > widest.ordinal()) {
|
||||||
|
widest = kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NumberKind numberKind(String key, Number number) {
|
||||||
|
NumberKind declared = declaredKind(key);
|
||||||
|
return declared == null ? inferredKind(number) : declared;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NumberKind declaredKind(String key) {
|
||||||
|
if (BYTE_FIELDS.contains(key)) {
|
||||||
|
return NumberKind.BYTE;
|
||||||
|
}
|
||||||
|
if (SHORT_FIELDS.contains(key)) {
|
||||||
|
return NumberKind.SHORT;
|
||||||
|
}
|
||||||
|
if (LONG_FIELDS.contains(key)) {
|
||||||
|
return NumberKind.LONG;
|
||||||
|
}
|
||||||
|
if (FLOAT_FIELDS.contains(key)) {
|
||||||
|
return NumberKind.FLOAT;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NumberKind inferredKind(Number number) {
|
||||||
|
if (number instanceof Double || number instanceof Float) {
|
||||||
|
return NumberKind.DOUBLE;
|
||||||
|
}
|
||||||
|
long value = number.longValue();
|
||||||
|
return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE ? NumberKind.INT : NumberKind.LONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Tag<?> number(NumberKind kind, Number number) {
|
||||||
|
return switch (kind) {
|
||||||
|
case BYTE -> new ByteTag(number.byteValue());
|
||||||
|
case SHORT -> new ShortTag(number.shortValue());
|
||||||
|
case INT -> new IntTag(number.intValue());
|
||||||
|
case LONG -> new LongTag(number.longValue());
|
||||||
|
case FLOAT -> new FloatTag(number.floatValue());
|
||||||
|
case DOUBLE -> new DoubleTag(number.doubleValue());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum NumberKind {
|
||||||
|
BYTE,
|
||||||
|
SHORT,
|
||||||
|
INT,
|
||||||
|
LONG,
|
||||||
|
FLOAT,
|
||||||
|
DOUBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package art.arcane.iris.structure.export;
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||||
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
|
||||||
@@ -9,15 +9,15 @@ import java.util.Map;
|
|||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
record VanillaBlockState(String name, Map<String, String> properties) {
|
public record VanillaBlockState(String name, Map<String, String> properties) {
|
||||||
private static final Pattern PROPERTY_NAME = Pattern.compile("[a-z0-9_.-]+");
|
private static final Pattern PROPERTY_NAME = Pattern.compile("[a-z0-9_.-]+");
|
||||||
private static final Pattern PROPERTY_VALUE = Pattern.compile("[a-z0-9_.-]+");
|
private static final Pattern PROPERTY_VALUE = Pattern.compile("[a-z0-9_.-]+");
|
||||||
|
|
||||||
VanillaBlockState {
|
public VanillaBlockState {
|
||||||
properties = Collections.unmodifiableMap(new LinkedHashMap<>(properties));
|
properties = Collections.unmodifiableMap(new LinkedHashMap<>(properties));
|
||||||
}
|
}
|
||||||
|
|
||||||
static VanillaBlockState parse(String value) {
|
public static VanillaBlockState parse(String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
throw new IllegalArgumentException("Block state must not be null");
|
throw new IllegalArgumentException("Block state must not be null");
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ record VanillaBlockState(String name, Map<String, String> properties) {
|
|||||||
return new VanillaBlockState(name, properties);
|
return new VanillaBlockState(name, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
String canonical() {
|
public String canonical() {
|
||||||
if (properties.isEmpty()) {
|
if (properties.isEmpty()) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ record VanillaBlockState(String name, Map<String, String> properties) {
|
|||||||
return builder.append(']').toString();
|
return builder.append(']').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
|
public CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
|
||||||
CompoundTag state = new CompoundTag();
|
CompoundTag state = new CompoundTag();
|
||||||
state.putString(format.nameKey(), name);
|
state.putString(format.nameKey(), name);
|
||||||
if (!properties.isEmpty()) {
|
if (!properties.isEmpty()) {
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class VanillaResourceIdentifier {
|
||||||
|
private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+");
|
||||||
|
private static final Pattern PATH = Pattern.compile("[a-z0-9/._-]+");
|
||||||
|
private static final Pattern INVALID_PATH_CHARACTER = Pattern.compile("[^a-z0-9._-]");
|
||||||
|
|
||||||
|
private VanillaResourceIdentifier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validNamespace(String value) {
|
||||||
|
return value != null && NAMESPACE.matcher(value).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validPath(String value) {
|
||||||
|
if (value == null
|
||||||
|
|| !PATH.matcher(value).matches()
|
||||||
|
|| value.startsWith("/")
|
||||||
|
|| value.endsWith("/")
|
||||||
|
|| value.contains("//")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String segment : value.split("/")) {
|
||||||
|
if (segment.equals(".") || segment.equals("..")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validIdentifier(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int separator = value.indexOf(':');
|
||||||
|
if (separator <= 0 || separator != value.lastIndexOf(':')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return validNamespace(value.substring(0, separator)) && validPath(value.substring(separator + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String requireNamespace(String value, String role) {
|
||||||
|
if (!validNamespace(value)) {
|
||||||
|
throw invalid(value, role);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String requirePath(String value, String role) {
|
||||||
|
if (!validPath(value)) {
|
||||||
|
throw invalid(value, role);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String requireIdentifier(String value, String role) {
|
||||||
|
if (!validIdentifier(value)) {
|
||||||
|
throw invalid(value, role);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String sanitizePath(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Cannot derive a resource path from '" + value + "'");
|
||||||
|
}
|
||||||
|
String lowered = value.trim().replace('\\', '/').toLowerCase(Locale.ROOT);
|
||||||
|
StringJoiner path = new StringJoiner("/");
|
||||||
|
for (String segment : lowered.split("/")) {
|
||||||
|
if (segment.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String cleaned = INVALID_PATH_CHARACTER.matcher(segment).replaceAll("_");
|
||||||
|
path.add(cleaned.equals(".") || cleaned.equals("..") ? cleaned.replace('.', '_') : cleaned);
|
||||||
|
}
|
||||||
|
String result = path.toString();
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Cannot derive a resource path from '" + value + "'");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String normalizeConnectorIdentifier(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "minecraft:empty";
|
||||||
|
}
|
||||||
|
String normalized = value.trim();
|
||||||
|
if (normalized.indexOf(':') < 0) {
|
||||||
|
normalized = "minecraft:" + normalized;
|
||||||
|
}
|
||||||
|
if (!validIdentifier(normalized)) {
|
||||||
|
throw new IllegalArgumentException("Invalid resource identifier '" + value + "'");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IllegalArgumentException invalid(String value, String role) {
|
||||||
|
return new IllegalArgumentException("Invalid " + role + " '" + value + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
@@ -0,0 +1,170 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
|
import art.arcane.iris.pack.PackValidator;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseStyleCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.UnusedResourcePruner;
|
||||||
|
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.GenerationBudget;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.GenerationCostCounter;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.VanillaBounds;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class WorldgenDatapackExporter implements WorldgenExportService {
|
||||||
|
private static final String STAGING_PREFIX = ".iris-worldgen-export-";
|
||||||
|
private static final Pattern UNSAFE_OUTPUT_CHARACTER = Pattern.compile("[^A-Za-z0-9._-]");
|
||||||
|
|
||||||
|
private final Function<DataVersion, WorldgenJsonWriter> writers;
|
||||||
|
private final PackCompiler compiler;
|
||||||
|
|
||||||
|
public WorldgenDatapackExporter(Function<DataVersion, WorldgenJsonWriter> writers, PackCompiler compiler) {
|
||||||
|
this.writers = Objects.requireNonNull(writers);
|
||||||
|
this.compiler = Objects.requireNonNull(compiler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenDatapackExporter create() {
|
||||||
|
return new WorldgenDatapackExporter(WorldgenJsonWriter::forTarget, PackCompiler.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WorldgenExportResult export(WorldgenExportRequest request, WorldgenExportProgress progress) {
|
||||||
|
Objects.requireNonNull(request);
|
||||||
|
Objects.requireNonNull(progress);
|
||||||
|
ExportPlan plan;
|
||||||
|
try {
|
||||||
|
plan = plan(request);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return WorldgenExportResult.rejected(null, exception.getMessage(), List.of());
|
||||||
|
}
|
||||||
|
Path destination = plan.destination();
|
||||||
|
if (Files.exists(destination) && !request.replace()) {
|
||||||
|
return rejected(destination, "Export output already exists: " + destination + ". Pass replace=true to overwrite it.");
|
||||||
|
}
|
||||||
|
if (!DatapackExportDestinations.claim(destination)) {
|
||||||
|
return rejected(destination, "An export to " + destination + " is already running.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return exportClaimed(request, plan, progress);
|
||||||
|
} finally {
|
||||||
|
DatapackExportDestinations.release(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExportPlan plan(WorldgenExportRequest request) {
|
||||||
|
DataVersion target = WorldgenExportTargets.resolve(request.version());
|
||||||
|
WorldgenExportMode mode = WorldgenExportMode.parse(request.mode());
|
||||||
|
DatapackExportFormat format = DatapackExportDestinations.parseFormat(request.format());
|
||||||
|
String namespace = VanillaResourceIdentifier.requireNamespace(request.namespace(), "export namespace");
|
||||||
|
String output = request.output().isEmpty() ? defaultOutput(request.dimension(), target) : request.output();
|
||||||
|
return new ExportPlan(target, mode, format, namespace, DatapackExportDestinations.resolve(request.exportRoot(), output, format));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportResult exportClaimed(WorldgenExportRequest request, ExportPlan plan, WorldgenExportProgress progress) {
|
||||||
|
Path destination = plan.destination();
|
||||||
|
File pack = request.pack().toFile();
|
||||||
|
try {
|
||||||
|
progress.stage("validate");
|
||||||
|
PackValidationResult validation = PackValidator.validateForPackaging(pack);
|
||||||
|
if (!validation.isLoadable()) {
|
||||||
|
return rejected(destination, "Pack '" + validation.getPackName() + "' failed validation: "
|
||||||
|
+ String.join("; ", validation.getBlockingErrors()));
|
||||||
|
}
|
||||||
|
progress.stage("load");
|
||||||
|
IrisData data = IrisData.openRuntime(pack);
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(request.dimension(), false);
|
||||||
|
if (dimension == null) {
|
||||||
|
return rejected(destination, "Dimension '" + request.dimension() + "' was not found in pack '" + pack.getName() + "'.");
|
||||||
|
}
|
||||||
|
PackCompileContext context = compileContext(data, dimension, plan, request.objects());
|
||||||
|
ExportIr ir = compiler.compile(context, progress);
|
||||||
|
return publish(request, plan, ir, context.biomeCodes().loadKeyByBiomeId(), progress);
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
IrisLogging.reportError("Worldgen datapack publication to " + destination + " failed", exception);
|
||||||
|
return WorldgenExportResult.failed(destination, "Datapack publication failed: " + exception.getMessage(), List.of());
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
IrisLogging.reportError("Worldgen datapack export of dimension '" + request.dimension() + "' failed", exception);
|
||||||
|
return WorldgenExportResult.failed(destination, "Datapack export failed: " + exception.getMessage(), List.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldgenExportResult publish(WorldgenExportRequest request, ExportPlan plan, ExportIr ir, Map<String, String> loadKeyByBiomeId,
|
||||||
|
WorldgenExportProgress progress) throws IOException {
|
||||||
|
progress.stage("write");
|
||||||
|
GenerationCost routerCost = GenerationCostCounter.count(ir.router(), VanillaBounds.of(ir.namespace(), ir.noises()), ir.target());
|
||||||
|
SortedMap<String, byte[]> files = writers.apply(plan.target()).write(ir);
|
||||||
|
progress.resources(files.size(), files.size());
|
||||||
|
progress.stage("prune");
|
||||||
|
UnusedResourcePruner.Pruned pruned = UnusedResourcePruner.prune(files);
|
||||||
|
progress.trimmed(pruned.trim());
|
||||||
|
GenerationCost cost = routerCost.withDensityFunctionBytes(GenerationCost.densityFunctionBytes(pruned.files()));
|
||||||
|
progress.stage("publish");
|
||||||
|
AtomicDatapackPublisher.Publication publication = new AtomicDatapackPublisher().publish(
|
||||||
|
pruned.files(),
|
||||||
|
plan.destination(),
|
||||||
|
plan.format(),
|
||||||
|
request.replace(),
|
||||||
|
STAGING_PREFIX);
|
||||||
|
List<ExportDiagnostic> costDiagnostics = GenerationBudget.check(cost, ir.dimension().name());
|
||||||
|
List<ExportDiagnostic> diagnostics = new ArrayList<>(ir.diagnostics().size() + pruned.diagnostics().size() + costDiagnostics.size());
|
||||||
|
diagnostics.addAll(ir.diagnostics());
|
||||||
|
diagnostics.addAll(costDiagnostics);
|
||||||
|
diagnostics.addAll(pruned.diagnostics());
|
||||||
|
return new WorldgenExportResult(
|
||||||
|
WorldgenExportResult.Status.EXPORTED,
|
||||||
|
plan.destination(),
|
||||||
|
String.join("; ", publication.cleanupWarnings()),
|
||||||
|
diagnostics,
|
||||||
|
loadKeyByBiomeId,
|
||||||
|
pruned.trim(),
|
||||||
|
cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackCompileContext compileContext(IrisData data, IrisDimension dimension, ExportPlan plan, boolean objects) {
|
||||||
|
List<ExportDiagnostic> diagnostics = new ArrayList<>();
|
||||||
|
return PackCompileContext.builder()
|
||||||
|
.data(data)
|
||||||
|
.dimension(dimension)
|
||||||
|
.capabilities(ExportCapabilities.forTarget(plan.target()))
|
||||||
|
.namespace(plan.namespace())
|
||||||
|
.mode(plan.mode())
|
||||||
|
.objects(objects)
|
||||||
|
.diagnostics(diagnostics)
|
||||||
|
.styles(NoiseStyleCompiler.create(diagnostics))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultOutput(String dimension, DataVersion target) {
|
||||||
|
return UNSAFE_OUTPUT_CHARACTER.matcher(dimension.replace('/', '-')).replaceAll("-") + "-" + target.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenExportResult rejected(Path destination, String message) {
|
||||||
|
return WorldgenExportResult.rejected(destination, message, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExportPlan(DataVersion target, WorldgenExportMode mode, DatapackExportFormat format, String namespace, Path destination) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum WorldgenExportMode {
|
||||||
|
PRESET,
|
||||||
|
DIMENSION,
|
||||||
|
OVERWORLD;
|
||||||
|
|
||||||
|
public static WorldgenExportMode parse(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return OVERWORLD;
|
||||||
|
}
|
||||||
|
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
|
||||||
|
case "preset" -> PRESET;
|
||||||
|
case "dimension" -> DIMENSION;
|
||||||
|
case "overworld" -> OVERWORLD;
|
||||||
|
default -> throw new IllegalArgumentException("Unknown export mode '" + raw + "'. Expected: preset, dimension, overworld");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
public interface WorldgenExportProgress {
|
||||||
|
WorldgenExportProgress NONE = new WorldgenExportProgress() {
|
||||||
|
@Override
|
||||||
|
public void stage(String stage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resources(int done, int total) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void stage(String stage);
|
||||||
|
|
||||||
|
void resources(int done, int total);
|
||||||
|
|
||||||
|
default void trimmed(ContentTrim trim) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record WorldgenExportRequest(
|
||||||
|
Path pack,
|
||||||
|
String dimension,
|
||||||
|
String version,
|
||||||
|
String mode,
|
||||||
|
String namespace,
|
||||||
|
String output,
|
||||||
|
String format,
|
||||||
|
boolean objects,
|
||||||
|
boolean replace,
|
||||||
|
Path exportRoot
|
||||||
|
) {
|
||||||
|
public static final String DEFAULT_MODE = "overworld";
|
||||||
|
public static final String DEFAULT_NAMESPACE = "iris_export";
|
||||||
|
public static final String DEFAULT_FORMAT = "zip";
|
||||||
|
|
||||||
|
public WorldgenExportRequest {
|
||||||
|
if (pack == null) {
|
||||||
|
throw new IllegalArgumentException("Worldgen export request requires a pack directory");
|
||||||
|
}
|
||||||
|
if (dimension == null || dimension.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Worldgen export request requires a dimension");
|
||||||
|
}
|
||||||
|
if (exportRoot == null) {
|
||||||
|
throw new IllegalArgumentException("Worldgen export request requires an export root");
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(version);
|
||||||
|
Objects.requireNonNull(mode);
|
||||||
|
Objects.requireNonNull(namespace);
|
||||||
|
Objects.requireNonNull(output);
|
||||||
|
Objects.requireNonNull(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
private Path pack;
|
||||||
|
private String dimension = "";
|
||||||
|
private String version = "";
|
||||||
|
private String mode = DEFAULT_MODE;
|
||||||
|
private String namespace = DEFAULT_NAMESPACE;
|
||||||
|
private String output = "";
|
||||||
|
private String format = DEFAULT_FORMAT;
|
||||||
|
private boolean objects = true;
|
||||||
|
private boolean replace;
|
||||||
|
private Path exportRoot;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder pack(Path value) {
|
||||||
|
pack = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder dimension(String value) {
|
||||||
|
dimension = Objects.requireNonNull(value).trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder version(String value) {
|
||||||
|
version = Objects.requireNonNull(value).trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder mode(String value) {
|
||||||
|
mode = Objects.requireNonNull(value).trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder namespace(String value) {
|
||||||
|
namespace = Objects.requireNonNull(value).trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder output(String value) {
|
||||||
|
output = Objects.requireNonNull(value).trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder format(String value) {
|
||||||
|
format = Objects.requireNonNull(value).trim();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder objects(boolean value) {
|
||||||
|
objects = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder replace(boolean value) {
|
||||||
|
replace = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder exportRoot(Path value) {
|
||||||
|
exportRoot = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldgenExportRequest build() {
|
||||||
|
return new WorldgenExportRequest(pack, dimension, version, mode, namespace, output, format, objects, replace, exportRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public record WorldgenExportResult(Status status, Path output, String message, List<ExportDiagnostic> diagnostics,
|
||||||
|
Map<String, String> loadKeyByBiomeId, ContentTrim trim, GenerationCost cost) {
|
||||||
|
public WorldgenExportResult {
|
||||||
|
Objects.requireNonNull(status);
|
||||||
|
if (status == Status.EXPORTED) {
|
||||||
|
Objects.requireNonNull(output);
|
||||||
|
}
|
||||||
|
message = message == null ? "" : message;
|
||||||
|
diagnostics = List.copyOf(diagnostics);
|
||||||
|
loadKeyByBiomeId = Collections.unmodifiableSortedMap(new TreeMap<>(loadKeyByBiomeId));
|
||||||
|
Objects.requireNonNull(trim);
|
||||||
|
Objects.requireNonNull(cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportResult exported(Path output, List<ExportDiagnostic> diagnostics, Map<String, String> loadKeyByBiomeId,
|
||||||
|
ContentTrim trim, GenerationCost cost) {
|
||||||
|
return new WorldgenExportResult(Status.EXPORTED, output, "", diagnostics, loadKeyByBiomeId, trim, cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportResult rejected(Path output, String message, List<ExportDiagnostic> diagnostics) {
|
||||||
|
return new WorldgenExportResult(Status.REJECTED, output, message, diagnostics, Map.of(), ContentTrim.NONE, GenerationCost.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportResult failed(Path output, String message, List<ExportDiagnostic> diagnostics) {
|
||||||
|
return new WorldgenExportResult(Status.FAILED, output, message, diagnostics, Map.of(), ContentTrim.NONE, GenerationCost.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return status == Status.EXPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
EXPORTED,
|
||||||
|
REJECTED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
public interface WorldgenExportService {
|
||||||
|
WorldgenExportResult export(WorldgenExportRequest request, WorldgenExportProgress progress);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public final class WorldgenExportServices {
|
||||||
|
private static final AtomicReference<WorldgenExportService> SERVICE = new AtomicReference<>();
|
||||||
|
|
||||||
|
private WorldgenExportServices() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void install(WorldgenExportService service) {
|
||||||
|
SERVICE.set(Objects.requireNonNull(service, "Datapack export service"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenExportService get() {
|
||||||
|
WorldgenExportService service = SERVICE.get();
|
||||||
|
if (service != null) {
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
return SERVICE.updateAndGet(current -> current == null ? WorldgenDatapackExporter.create() : current);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
|
public final class WorldgenExportTargets {
|
||||||
|
private static final DataVersion OLDEST = DataVersion.V26_3;
|
||||||
|
public static final List<DataVersion> SUPPORTED = supported();
|
||||||
|
|
||||||
|
private WorldgenExportTargets() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DataVersion resolve(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return SUPPORTED.getLast();
|
||||||
|
}
|
||||||
|
DataVersion version = DataVersion.forMinecraftVersion(raw.trim());
|
||||||
|
if (!SUPPORTED.contains(version)) {
|
||||||
|
throw unsupported(raw);
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException unsupported(String raw) {
|
||||||
|
StringJoiner supported = new StringJoiner(", ");
|
||||||
|
for (DataVersion version : SUPPORTED) {
|
||||||
|
supported.add(version.getVersion());
|
||||||
|
}
|
||||||
|
return new IllegalArgumentException("Unsupported datapack export version '" + raw + "'. Supported: " + supported);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<DataVersion> supported() {
|
||||||
|
List<DataVersion> versions = new ArrayList<>();
|
||||||
|
for (DataVersion version : DataVersion.values()) {
|
||||||
|
if (version.ordinal() >= OLDEST.ordinal()) {
|
||||||
|
versions.add(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(versions);
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.IrisBlockData;
|
||||||
|
import art.arcane.iris.pack.ContentKeyValidator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.validation.ContentGate;
|
||||||
|
import art.arcane.iris.pack.validation.LegacyBlockRenames;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class BlockStateResolver {
|
||||||
|
private final IrisData data;
|
||||||
|
private final PlatformRegistries registries;
|
||||||
|
private final Set<String> knownBlocks;
|
||||||
|
private final List<ExportDiagnostic> diagnostics;
|
||||||
|
private final Map<String, Resolution> resolutions = new HashMap<>();
|
||||||
|
|
||||||
|
public BlockStateResolver(IrisData data, PlatformRegistries registries, List<ExportDiagnostic> diagnostics) {
|
||||||
|
this.data = Objects.requireNonNull(data);
|
||||||
|
this.registries = registries;
|
||||||
|
this.knownBlocks = registries == null ? Set.of() : knownBlocks(registries.blockTypeKeys());
|
||||||
|
this.diagnostics = Objects.requireNonNull(diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockStateResolver forContext(PackCompileContext context) {
|
||||||
|
PlatformRegistries registries = IrisPlatforms.isBound() ? IrisPlatforms.get().registries() : null;
|
||||||
|
return new BlockStateResolver(context.data(), registries, context.diagnostics());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<VanillaBlockState> resolve(IrisBlockData block, String resource) {
|
||||||
|
if (block == null || block.getBlock() == null || block.getBlock().isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return resolve(stateKey(block), resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<VanillaBlockState> resolve(String rawState, String resource) {
|
||||||
|
String normalized = ContentGate.normalizeState(rawState);
|
||||||
|
if (normalized == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Resolution resolution = resolutions.computeIfAbsent(normalized, this::parse);
|
||||||
|
if (resolution.problem() != null) {
|
||||||
|
ExportDiagnostic diagnostic = new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.BLOCK_STATE, resource, resolution.problem());
|
||||||
|
if (!diagnostics.contains(diagnostic)) {
|
||||||
|
diagnostics.add(diagnostic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(resolution.state());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stateKey(IrisBlockData block) {
|
||||||
|
IrisBlockData custom = data.getBlockLoader() == null ? null : data.getBlockLoader().load(block.getBlock(), false);
|
||||||
|
if (custom == null || custom.getBlock() == null || custom.getBlock().equals(block.getBlock())) {
|
||||||
|
return block.stateKey();
|
||||||
|
}
|
||||||
|
IrisBlockData merged = IrisBlockData.from(custom.stateKey());
|
||||||
|
merged.getData().putAll(block.getData());
|
||||||
|
return merged.stateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Resolution parse(String normalized) {
|
||||||
|
String renamed = renamed(normalized);
|
||||||
|
if (!renamed.startsWith("minecraft:")) {
|
||||||
|
return new Resolution(null, "Block '" + renamed + "' is not a Minecraft block and is not exported");
|
||||||
|
}
|
||||||
|
String problem = null;
|
||||||
|
String validated = renamed;
|
||||||
|
List<String> propertyProblems = registries == null ? List.of() : ContentKeyValidator.validateBlockStateProperties(registries, List.of(renamed));
|
||||||
|
if (!propertyProblems.isEmpty()) {
|
||||||
|
validated = ContentGate.baseKey(renamed);
|
||||||
|
problem = String.join("; ", propertyProblems) + "; the block exports in its default state";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new Resolution(VanillaBlockState.parse(validated), problem);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return new Resolution(null, "Block state '" + renamed + "' is not exported: " + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renamed(String normalized) {
|
||||||
|
if (knownBlocks.isEmpty() || knownBlocks.contains(ContentGate.baseKey(normalized))) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
String properties = normalized.substring(ContentGate.baseKey(normalized).length());
|
||||||
|
String current = normalized;
|
||||||
|
for (int hop = 0; hop < LegacyBlockRenames.MAX_HOPS; hop++) {
|
||||||
|
String supplement = LegacyBlockRenames.supplementFor(current);
|
||||||
|
if (supplement == null) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
String next = ContentGate.normalizeState(supplement);
|
||||||
|
if (next == null) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
if (knownBlocks.contains(ContentGate.baseKey(next))) {
|
||||||
|
return ContentGate.baseKey(next) + properties;
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> knownBlocks(List<String> keys) {
|
||||||
|
Set<String> known = new HashSet<>(keys.size());
|
||||||
|
for (String key : keys) {
|
||||||
|
String normalized = ContentGate.normalizeState(key);
|
||||||
|
if (normalized != null) {
|
||||||
|
known.add(ContentGate.baseKey(normalized));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Resolution(VanillaBlockState state, String problem) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
public interface CompileStage {
|
||||||
|
String name();
|
||||||
|
|
||||||
|
void apply(PackCompileContext context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.block.IrisBlockData;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimensionType;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class DimensionStage implements CompileStage {
|
||||||
|
private static final String DEFAULT_ROCK = "minecraft:stone";
|
||||||
|
private static final String DEFAULT_FLUID = "minecraft:water";
|
||||||
|
private static final float FULLBRIGHT_AMBIENT_LIGHT = 1.0f;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "dimension";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(PackCompileContext context) {
|
||||||
|
IrisDimension dimension = context.dimension();
|
||||||
|
int minY = dimension.getMinHeight();
|
||||||
|
DimensionDef aligned = DimensionDef.aligned(
|
||||||
|
VanillaResourceIdentifier.sanitizePath(dimension.getLoadKey()),
|
||||||
|
minY,
|
||||||
|
dimension.getMaxHeight() - minY,
|
||||||
|
dimension.getLogicalHeight(),
|
||||||
|
dimension.getFluidHeight() + minY,
|
||||||
|
paletteBlock(dimension.getRockPalette(), DEFAULT_ROCK, "rock"),
|
||||||
|
paletteBlock(dimension.getFluidPalette(), DEFAULT_FLUID, "fluid"),
|
||||||
|
new JsonObject(),
|
||||||
|
context.diagnostics());
|
||||||
|
IrisDimensionType type = new IrisDimensionType(
|
||||||
|
dimension.getBaseDimension(),
|
||||||
|
typeOptions(dimension),
|
||||||
|
aligned.logicalHeight(),
|
||||||
|
aligned.height(),
|
||||||
|
aligned.minY());
|
||||||
|
JsonObject typeJson = JsonParser.parseString(type.toJson(context.capabilities().target().get())).getAsJsonObject();
|
||||||
|
context.setDimensionDef(new DimensionDef(
|
||||||
|
aligned.name(),
|
||||||
|
aligned.minY(),
|
||||||
|
aligned.height(),
|
||||||
|
aligned.logicalHeight(),
|
||||||
|
aligned.fluidHeight(),
|
||||||
|
aligned.defaultBlock(),
|
||||||
|
aligned.defaultFluid(),
|
||||||
|
typeJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisDimensionTypeOptions typeOptions(IrisDimension dimension) {
|
||||||
|
IrisDimensionTypeOptions options = dimension.getDimensionOptions();
|
||||||
|
return dimension.isFullbright() ? options.copy().ambientLight(FULLBRIGHT_AMBIENT_LIGHT) : options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String paletteBlock(IrisMaterialPalette palette, String fallback, String role) {
|
||||||
|
List<IrisBlockData> entries = palette == null ? null : palette.getPalette();
|
||||||
|
if (entries == null || entries.isEmpty() || entries.getFirst() == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
String raw = entries.getFirst().getBlock();
|
||||||
|
String block = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT);
|
||||||
|
int properties = block.indexOf('[');
|
||||||
|
String id = properties < 0 ? block : block.substring(0, properties);
|
||||||
|
String namespaced = id.indexOf(':') < 0 ? "minecraft:" + id : id;
|
||||||
|
return VanillaResourceIdentifier.requireIdentifier(namespaced, "dimension " + role + " block");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public final class ExportedBiomes {
|
||||||
|
private final SortedMap<String, IrisBiome> byId;
|
||||||
|
private final Map<String, String> loadKeys;
|
||||||
|
private final Map<String, Integer> codeOrder;
|
||||||
|
private final Map<String, IrisRegion> regionByLoadKey;
|
||||||
|
|
||||||
|
private ExportedBiomes(SortedMap<String, IrisBiome> byId, Map<String, String> loadKeys, Map<String, Integer> codeOrder,
|
||||||
|
Map<String, IrisRegion> regionByLoadKey) {
|
||||||
|
this.byId = Collections.unmodifiableSortedMap(byId);
|
||||||
|
this.loadKeys = Map.copyOf(loadKeys);
|
||||||
|
this.codeOrder = Map.copyOf(codeOrder);
|
||||||
|
this.regionByLoadKey = Map.copyOf(regionByLoadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExportedBiomes load(PackCompileContext context) {
|
||||||
|
SortedMap<String, IrisBiome> byId = new TreeMap<>();
|
||||||
|
Map<String, String> loadKeys = new HashMap<>();
|
||||||
|
for (Map.Entry<String, String> entry : context.biomeCodes().loadKeyByBiomeId().entrySet()) {
|
||||||
|
IrisBiome biome = Objects.requireNonNull(context.data().getBiomeLoader().load(entry.getValue(), false),
|
||||||
|
"Biome '" + entry.getValue() + "' could not be loaded");
|
||||||
|
byId.put(entry.getKey(), biome);
|
||||||
|
loadKeys.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return new ExportedBiomes(byId, loadKeys, codeOrder(context.biomeCodes().layout(), byId.keySet()), regions(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedMap<String, IrisBiome> byId() {
|
||||||
|
return byId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedSet<String> ids() {
|
||||||
|
return new TreeSet<>(byId.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedSet<String> idsFor(Collection<String> loadKeys) {
|
||||||
|
SortedSet<String> ids = new TreeSet<>();
|
||||||
|
for (Map.Entry<String, String> entry : this.loadKeys.entrySet()) {
|
||||||
|
if (loadKeys.contains(entry.getValue())) {
|
||||||
|
ids.add(entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String loadKey(String biomeId) {
|
||||||
|
return loadKeys.get(biomeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resource(String biomeId) {
|
||||||
|
return "biomes/" + loadKeys.get(biomeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisRegion region(String biomeId) {
|
||||||
|
return regionByLoadKey.get(loadKeys.get(biomeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> inCodeOrder(Iterable<String> biomeIds) {
|
||||||
|
List<String> ordered = new ArrayList<>();
|
||||||
|
for (String biomeId : biomeIds) {
|
||||||
|
ordered.add(biomeId);
|
||||||
|
}
|
||||||
|
ordered.sort(Comparator.comparingInt((String id) -> codeOrder.getOrDefault(id, Integer.MAX_VALUE)).thenComparing(Comparator.naturalOrder()));
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Integer> codeOrder(ClimateLayout layout, Iterable<String> biomeIds) {
|
||||||
|
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>(layout.entries());
|
||||||
|
entries.sort(Comparator.comparingInt((ClimateLayout.ClimateEntry entry) -> entry.axis().ordinal())
|
||||||
|
.thenComparingDouble(ClimateLayout.ClimateEntry::min)
|
||||||
|
.thenComparing(ClimateLayout.ClimateEntry::biomeId));
|
||||||
|
Map<String, Integer> order = new LinkedHashMap<>();
|
||||||
|
for (ClimateLayout.ClimateEntry entry : entries) {
|
||||||
|
order.putIfAbsent(entry.biomeId(), order.size());
|
||||||
|
}
|
||||||
|
for (String biomeId : biomeIds) {
|
||||||
|
order.putIfAbsent(biomeId, order.size());
|
||||||
|
}
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, IrisRegion> regions(PackCompileContext context) {
|
||||||
|
Map<String, IrisRegion> regions = new HashMap<>();
|
||||||
|
for (IrisRegion region : context.dimension().getAllRegions(() -> context.data())) {
|
||||||
|
if (region == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisBiome biome : region.getAllBiomes(() -> context.data())) {
|
||||||
|
if (biome != null && biome.getLoadKey() != null) {
|
||||||
|
regions.putIfAbsent(biome.getLoadKey(), region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
|
||||||
|
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public final class ImageMapStage implements CompileStage {
|
||||||
|
private static final String DIMENSION_MESSAGE = "Image maps are not exported; the dimension generates from its procedural layers";
|
||||||
|
private static final String STYLE_MESSAGE = "Generator style image maps are not exported; the base noise style is used";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "image-maps";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(PackCompileContext context) {
|
||||||
|
IrisDimension dimension = context.dimension();
|
||||||
|
IrisData data = context.data();
|
||||||
|
Gson gson = data.getGson();
|
||||||
|
if (usesImageMaps(gson.toJsonTree(dimension))) {
|
||||||
|
report(context, "dimensions/" + dimension.getLoadKey(), DIMENSION_MESSAGE);
|
||||||
|
}
|
||||||
|
dimension.setImageMaps(new KList<>());
|
||||||
|
SortedMap<String, IrisRegion> regions = new TreeMap<>();
|
||||||
|
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||||
|
regions.put(region.getLoadKey(), region);
|
||||||
|
}
|
||||||
|
reportStyles(context, gson, "regions/", regions);
|
||||||
|
SortedMap<String, IrisBiome> biomes = new TreeMap<>();
|
||||||
|
SortedSet<String> generatorKeys = new TreeSet<>();
|
||||||
|
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
||||||
|
biomes.put(biome.getLoadKey(), biome);
|
||||||
|
collectGenerators(biome.getGenerators(), generatorKeys);
|
||||||
|
}
|
||||||
|
reportStyles(context, gson, "biomes/", biomes);
|
||||||
|
SortedMap<String, IrisGenerator> generators = new TreeMap<>();
|
||||||
|
for (String key : generatorKeys) {
|
||||||
|
IrisGenerator generator = data.getGeneratorLoader().load(key, false);
|
||||||
|
if (generator != null) {
|
||||||
|
generators.put(key, generator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reportStyles(context, gson, "generators/", generators);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collectGenerators(List<IrisBiomeGeneratorLink> links, SortedSet<String> generatorKeys) {
|
||||||
|
if (links == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IrisBiomeGeneratorLink link : links) {
|
||||||
|
if (link != null && link.getGenerator() != null && !link.getGenerator().isBlank()) {
|
||||||
|
generatorKeys.add(link.getGenerator().trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reportStyles(PackCompileContext context, Gson gson, String folder, SortedMap<String, ?> resources) {
|
||||||
|
for (Map.Entry<String, ?> entry : resources.entrySet()) {
|
||||||
|
if (usesImageMaps(gson.toJsonTree(entry.getValue()))) {
|
||||||
|
report(context, folder + entry.getKey(), STYLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void report(PackCompileContext context, String resource, String message) {
|
||||||
|
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.IMAGE_MAP, resource, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean usesImageMaps(JsonElement element) {
|
||||||
|
if (element.isJsonArray()) {
|
||||||
|
for (JsonElement child : element.getAsJsonArray()) {
|
||||||
|
if (usesImageMaps(child)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!element.isJsonObject()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JsonObject object = element.getAsJsonObject();
|
||||||
|
if (namesImageMap(object.get("imageMap")) || bindsImageMaps(object.get("imageMaps"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JsonElement> member : object.entrySet()) {
|
||||||
|
if (usesImageMaps(member.getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean namesImageMap(JsonElement value) {
|
||||||
|
return value != null && value.isJsonPrimitive() && !value.getAsString().isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean bindsImageMaps(JsonElement value) {
|
||||||
|
return value != null && value.isJsonArray() && !value.getAsJsonArray().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
+390
@@ -0,0 +1,390 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeDecision;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeIdSanitizer;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.decoration.BiomeFeatureLists;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BinaryResource;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.validation.ContentGate;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class PackCompileContext {
|
||||||
|
private static final Field NEUTRAL_VOLUME = new Field.Const(0.0);
|
||||||
|
private static final Field NO_CAVES = new Field.Const(1.0e6);
|
||||||
|
private static final Field ZERO_CODE = new Field.Const(0.0);
|
||||||
|
private static final double CLIMATE_SPAN = 1.0;
|
||||||
|
private static final List<String> LEGACY_BIOME_KEYS = List.of("surface_builder", "depth", "scale", "precipitation", "category", "starts");
|
||||||
|
private static final StyleCompiler NO_STYLE_COMPILER = (style, seedSalt, registry, capabilities) -> {
|
||||||
|
throw new IllegalStateException("No style compiler installed");
|
||||||
|
};
|
||||||
|
|
||||||
|
private final IrisData data;
|
||||||
|
private final IrisDimension dimension;
|
||||||
|
private final ExportCapabilities capabilities;
|
||||||
|
private final String namespace;
|
||||||
|
private final WorldgenExportMode mode;
|
||||||
|
private final boolean objects;
|
||||||
|
private final StyleCompiler styles;
|
||||||
|
private final NoiseRegistry noises;
|
||||||
|
private final List<ExportDiagnostic> diagnostics;
|
||||||
|
private final BiomeFeatureLists features = new BiomeFeatureLists();
|
||||||
|
private final List<JsonResource> jsonResources = new ArrayList<>();
|
||||||
|
private final List<BinaryResource> binaryResources = new ArrayList<>();
|
||||||
|
private DimensionDef dimensionDef;
|
||||||
|
private BiomeCodes biomeCodes;
|
||||||
|
private BiomeDecision biomeDecision;
|
||||||
|
private BiomeCodes boundBiomeCodes;
|
||||||
|
private Field boundHeight;
|
||||||
|
private List<BiomeDef> biomes;
|
||||||
|
private Field height;
|
||||||
|
private Field carvedHeight;
|
||||||
|
private Field climateHeight;
|
||||||
|
private Field surfaceEstimate;
|
||||||
|
private Field volumetric;
|
||||||
|
private Field caves;
|
||||||
|
private Field.Shared caveField;
|
||||||
|
private double wallShellBand;
|
||||||
|
private Field.Shared deepCaves;
|
||||||
|
private AquiferDef aquifer;
|
||||||
|
private MaterialProgram material;
|
||||||
|
private DefaultBiome defaultBiome;
|
||||||
|
|
||||||
|
private PackCompileContext(Builder builder) {
|
||||||
|
data = Objects.requireNonNull(builder.data, "Pack compile context requires pack data");
|
||||||
|
dimension = Objects.requireNonNull(builder.dimension, "Pack compile context requires a dimension");
|
||||||
|
capabilities = Objects.requireNonNull(builder.capabilities, "Pack compile context requires export capabilities");
|
||||||
|
namespace = VanillaResourceIdentifier.requireNamespace(builder.namespace, "export namespace");
|
||||||
|
mode = Objects.requireNonNull(builder.mode, "Pack compile context requires an export mode");
|
||||||
|
objects = builder.objects;
|
||||||
|
styles = builder.styles;
|
||||||
|
diagnostics = builder.diagnostics;
|
||||||
|
noises = new NoiseRegistry(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisData data() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisDimension dimension() {
|
||||||
|
return dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportCapabilities capabilities() {
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String namespace() {
|
||||||
|
return namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldgenExportMode mode() {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean objects() {
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StyleCompiler styles() {
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoiseRegistry noises() {
|
||||||
|
return noises;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ExportDiagnostic> diagnostics() {
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiomeFeatureLists features() {
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JsonResource> jsonResources() {
|
||||||
|
return jsonResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BinaryResource> binaryResources() {
|
||||||
|
return binaryResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DimensionDef dimensionDef() {
|
||||||
|
if (dimensionDef == null) {
|
||||||
|
throw new IllegalStateException("Dimension definition for '" + dimension.getLoadKey() + "' is read before the dimension stage ran");
|
||||||
|
}
|
||||||
|
return dimensionDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDimensionDef(DimensionDef value) {
|
||||||
|
dimensionDef = Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiomeCodes biomeCodes() {
|
||||||
|
if (biomeDecision != null) {
|
||||||
|
return height == null ? biomeDecision.base() : heightBoundBiomeCodes();
|
||||||
|
}
|
||||||
|
return biomeCodes == null ? defaultBiome().codes() : biomeCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiomeCodes baseBiomeCodes() {
|
||||||
|
if (biomeDecision != null) {
|
||||||
|
return biomeDecision.base();
|
||||||
|
}
|
||||||
|
return biomeCodes == null ? defaultBiome().codes() : biomeCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBiomeCodes(BiomeCodes value) {
|
||||||
|
biomeCodes = Objects.requireNonNull(value);
|
||||||
|
biomeDecision = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBiomeDecision(BiomeDecision value) {
|
||||||
|
biomeDecision = Objects.requireNonNull(value);
|
||||||
|
biomeCodes = null;
|
||||||
|
boundBiomeCodes = null;
|
||||||
|
boundHeight = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BiomeDef> biomes() {
|
||||||
|
return biomes == null ? defaultBiome().biomes() : biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBiomes(List<BiomeDef> value) {
|
||||||
|
biomes = List.copyOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field height() {
|
||||||
|
return carvedHeight == null ? naturalHeight() : carvedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field naturalHeight() {
|
||||||
|
return height == null ? new Field.Const(dimensionDef().fluidHeight()) : height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeight(Field value) {
|
||||||
|
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "height");
|
||||||
|
height = value;
|
||||||
|
carvedHeight = null;
|
||||||
|
climateHeight = null;
|
||||||
|
surfaceEstimate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field climateHeight() {
|
||||||
|
return climateHeight == null ? naturalHeight() : climateHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClimateHeight(Field value) {
|
||||||
|
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "climate height");
|
||||||
|
climateHeight = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCarvedHeight(Field value) {
|
||||||
|
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "carved height");
|
||||||
|
carvedHeight = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field surfaceEstimate() {
|
||||||
|
return surfaceEstimate == null ? climateHeight() : surfaceEstimate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSurfaceEstimate(Field value) {
|
||||||
|
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "surface estimate");
|
||||||
|
surfaceEstimate = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field volumetric() {
|
||||||
|
return volumetric == null ? NEUTRAL_VOLUME : volumetric;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVolumetric(Field value) {
|
||||||
|
volumetric = Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field caves() {
|
||||||
|
return caves == null ? NO_CAVES : caves;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCaves(Field value) {
|
||||||
|
caves = Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Field.Shared> caveField() {
|
||||||
|
return Optional.ofNullable(caveField);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCaveField(Field.Shared value, double wallBand) {
|
||||||
|
if (!(wallBand >= 0.0) || Double.isInfinite(wallBand)) {
|
||||||
|
throw new IllegalArgumentException("Wall shell band must be finite and non-negative, got " + wallBand);
|
||||||
|
}
|
||||||
|
caveField = Objects.requireNonNull(value);
|
||||||
|
wallShellBand = wallBand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double wallShellBand() {
|
||||||
|
return wallShellBand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Field.Shared> deepCaves() {
|
||||||
|
return Optional.ofNullable(deepCaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeepCaves(Field.Shared value) {
|
||||||
|
deepCaves = Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AquiferDef aquifer() {
|
||||||
|
return aquifer == null ? AquiferDef.dry(surfaceEstimate()) : aquifer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAquifer(AquiferDef value) {
|
||||||
|
aquifer = Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MaterialProgram material() {
|
||||||
|
return material == null ? MaterialProgram.rock(dimensionDef().defaultBlock()) : material;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaterial(MaterialProgram value) {
|
||||||
|
material = Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeCodes heightBoundBiomeCodes() {
|
||||||
|
Field bindingHeight = climateHeight();
|
||||||
|
if (boundHeight != bindingHeight) {
|
||||||
|
boundBiomeCodes = biomeDecision.bind(bindingHeight);
|
||||||
|
boundHeight = bindingHeight;
|
||||||
|
}
|
||||||
|
return boundBiomeCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultBiome defaultBiome() {
|
||||||
|
if (defaultBiome == null) {
|
||||||
|
defaultBiome = createDefaultBiome();
|
||||||
|
}
|
||||||
|
return defaultBiome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultBiome createDefaultBiome() {
|
||||||
|
String loadKey = firstLandBiome();
|
||||||
|
String id = BiomeIdSanitizer.sanitize(namespace, loadKey, new HashSet<>());
|
||||||
|
String body = new IrisBiomeCustom().generateJson(capabilities.target().get(), new ContentGate(null, Map.of(), null));
|
||||||
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
for (String key : LEGACY_BIOME_KEYS) {
|
||||||
|
json.remove(key);
|
||||||
|
}
|
||||||
|
BiomeDef biome = new BiomeDef(id.substring(namespace.length() + 1), json);
|
||||||
|
ClimateLayout layout = new ClimateLayout(List.of(
|
||||||
|
new ClimateLayout.ClimateEntry(id, ClimateLayout.ClimateAxis.TEMPERATURE, -CLIMATE_SPAN, CLIMATE_SPAN)));
|
||||||
|
BiomeCodes codes = new BiomeCodes(ZERO_CODE, ZERO_CODE, ZERO_CODE, layout, Map.of(id, loadKey));
|
||||||
|
return new DefaultBiome(List.of(biome), codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstLandBiome() {
|
||||||
|
for (String regionKey : dimension.getRegions()) {
|
||||||
|
IrisRegion region = data.getRegionLoader().load(regionKey, false);
|
||||||
|
if (region == null || region.isCompatExcluded()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String biomeKey : region.getLandBiomes()) {
|
||||||
|
IrisBiome biome = biomeKey == null ? null : data.getBiomeLoader().load(biomeKey, false);
|
||||||
|
if (biome != null && !biome.isCompatExcluded()) {
|
||||||
|
return biome.getLoadKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Dimension '" + dimension.getLoadKey() + "' has no exportable land biome in any of its regions");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DefaultBiome(List<BiomeDef> biomes, BiomeCodes codes) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
private IrisData data;
|
||||||
|
private IrisDimension dimension;
|
||||||
|
private ExportCapabilities capabilities;
|
||||||
|
private String namespace;
|
||||||
|
private WorldgenExportMode mode = WorldgenExportMode.OVERWORLD;
|
||||||
|
private boolean objects = true;
|
||||||
|
private StyleCompiler styles = NO_STYLE_COMPILER;
|
||||||
|
private List<ExportDiagnostic> diagnostics = new ArrayList<>();
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder data(IrisData value) {
|
||||||
|
data = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder dimension(IrisDimension value) {
|
||||||
|
dimension = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder capabilities(ExportCapabilities value) {
|
||||||
|
capabilities = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder namespace(String value) {
|
||||||
|
namespace = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder mode(WorldgenExportMode value) {
|
||||||
|
mode = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder objects(boolean value) {
|
||||||
|
objects = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder styles(StyleCompiler value) {
|
||||||
|
styles = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder diagnostics(List<ExportDiagnostic> value) {
|
||||||
|
diagnostics = Objects.requireNonNull(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackCompileContext build() {
|
||||||
|
return new PackCompileContext(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.cave.CaveStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.decoration.DecorationStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.object.ObjectStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.structure.StructureStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.subterrain.SubterrainStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.surface.SurfaceStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.terrain.RiverStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.terrain.TerrainStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class PackCompiler {
|
||||||
|
private static final String ROUTER_STAGE = "router";
|
||||||
|
|
||||||
|
private final List<CompileStage> stages;
|
||||||
|
|
||||||
|
public PackCompiler(List<CompileStage> stages) {
|
||||||
|
this.stages = List.copyOf(stages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PackCompiler create() {
|
||||||
|
return new PackCompiler(List.of(
|
||||||
|
new DimensionStage(),
|
||||||
|
new ImageMapStage(),
|
||||||
|
new BiomeStage(),
|
||||||
|
new TerrainStage(),
|
||||||
|
new RiverStage(),
|
||||||
|
new CaveStage(),
|
||||||
|
new SurfaceStage(),
|
||||||
|
new DecorationStage(),
|
||||||
|
new StructureStage(),
|
||||||
|
new SubterrainStage(),
|
||||||
|
new ObjectStage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportIr compile(PackCompileContext context, WorldgenExportProgress progress) {
|
||||||
|
for (CompileStage stage : stages) {
|
||||||
|
progress.stage(stage.name());
|
||||||
|
stage.apply(context);
|
||||||
|
}
|
||||||
|
progress.stage(ROUTER_STAGE);
|
||||||
|
DimensionDef dimension = context.dimensionDef();
|
||||||
|
BiomeCodes codes = context.biomeCodes();
|
||||||
|
RouterDef router = RouterCompiler.compile(
|
||||||
|
context.height(),
|
||||||
|
context.volumetric(),
|
||||||
|
context.caves(),
|
||||||
|
context.aquifer(),
|
||||||
|
codes,
|
||||||
|
dimension);
|
||||||
|
return new ExportIr(
|
||||||
|
context.namespace(),
|
||||||
|
context.capabilities().target(),
|
||||||
|
context.mode(),
|
||||||
|
dimension,
|
||||||
|
context.noises().definitions(),
|
||||||
|
router,
|
||||||
|
codes.layout(),
|
||||||
|
context.features().apply(context.biomes(), context.namespace()),
|
||||||
|
context.material(),
|
||||||
|
context.diagnostics(),
|
||||||
|
context.jsonResources(),
|
||||||
|
context.binaryResources());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
|
||||||
|
|
||||||
|
public final class RouterCompiler {
|
||||||
|
private static final Field DENSITY_PER_BLOCK = new Field.Const(1.0);
|
||||||
|
private static final Field UNUSED_CLIMATE = new Field.Const(0.0);
|
||||||
|
private static final double HALF_BLOCK = 0.5;
|
||||||
|
private static final int VOLUME_CELL_XZ = 4;
|
||||||
|
private static final int VOLUME_CELL_Y = 8;
|
||||||
|
private static final int SURFACE_CELL_XZ = 16;
|
||||||
|
private static final int SURFACE_CELL_Y = 1;
|
||||||
|
|
||||||
|
private RouterCompiler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RouterDef compile(Field height, Field volumetric, Field caves, AquiferDef aquifer, BiomeCodes codes, DimensionDef dimension) {
|
||||||
|
FieldValidator.requireTwoDimensional(height, "height");
|
||||||
|
Field ramp = new Field.Binary(Field.BinaryOp.MUL, DENSITY_PER_BLOCK, new Field.Binary(Field.BinaryOp.SUB, height, blockCenterY(dimension)));
|
||||||
|
Field terrain = new Field.Binary(Field.BinaryOp.ADD, ramp, new Field.Grid(volumetric, VOLUME_CELL_XZ, VOLUME_CELL_Y));
|
||||||
|
return new RouterDef(
|
||||||
|
codes.surfaceCode(),
|
||||||
|
codes.caveCode(),
|
||||||
|
UNUSED_CLIMATE,
|
||||||
|
UNUSED_CLIMATE,
|
||||||
|
codes.class3d(),
|
||||||
|
UNUSED_CLIMATE,
|
||||||
|
new Field.Grid(height, SURFACE_CELL_XZ, SURFACE_CELL_Y),
|
||||||
|
new Field.Binary(Field.BinaryOp.MIN, terrain, caves),
|
||||||
|
aquifer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field blockCenterY(DimensionDef dimension) {
|
||||||
|
double bottom = dimension.minY();
|
||||||
|
double top = dimension.minY() + dimension.height();
|
||||||
|
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, bottom, top, bottom - HALF_BLOCK, top - HALF_BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record WeightedState(VanillaBlockState state, int weight) {
|
||||||
|
public WeightedState {
|
||||||
|
Objects.requireNonNull(state);
|
||||||
|
if (weight < 1) {
|
||||||
|
throw new IllegalArgumentException("Block state weight must be positive, got " + weight + " for " + state.canonical());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
|
||||||
|
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class WorldgenSyntax {
|
||||||
|
private final DataVersion target;
|
||||||
|
|
||||||
|
private WorldgenSyntax(DataVersion target) {
|
||||||
|
this.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldgenSyntax forTarget(DataVersion target) {
|
||||||
|
if (!WorldgenExportTargets.SUPPORTED.contains(target)) {
|
||||||
|
throw WorldgenExportTargets.unsupported(target.getVersion());
|
||||||
|
}
|
||||||
|
return new WorldgenSyntax(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataVersion target() {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String featureFolder() {
|
||||||
|
return "worldgen/feature";
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject blockState(VanillaBlockState state) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("id", state.name());
|
||||||
|
if (!state.properties().isEmpty()) {
|
||||||
|
JsonObject properties = new JsonObject();
|
||||||
|
for (Map.Entry<String, String> property : state.properties().entrySet()) {
|
||||||
|
properties.addProperty(property.getKey(), property.getValue());
|
||||||
|
}
|
||||||
|
json.add("properties", properties);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject provider(List<WeightedState> entries) {
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("A block state provider needs at least one state");
|
||||||
|
}
|
||||||
|
if (entries.size() == 1) {
|
||||||
|
return blockState(entries.getFirst().state());
|
||||||
|
}
|
||||||
|
JsonArray weighted = new JsonArray(entries.size());
|
||||||
|
for (WeightedState entry : entries) {
|
||||||
|
JsonObject item = new JsonObject();
|
||||||
|
item.add("data", blockState(entry.state()));
|
||||||
|
item.addProperty("weight", entry.weight());
|
||||||
|
weighted.add(item);
|
||||||
|
}
|
||||||
|
JsonObject provider = new JsonObject();
|
||||||
|
provider.addProperty("type", "minecraft:weighted");
|
||||||
|
provider.add("entries", weighted);
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject feature(String type, JsonObject config) {
|
||||||
|
JsonObject feature = new JsonObject();
|
||||||
|
feature.addProperty("type", type);
|
||||||
|
for (Map.Entry<String, JsonElement> field : config.entrySet()) {
|
||||||
|
feature.add(field.getKey(), field.getValue().deepCopy());
|
||||||
|
}
|
||||||
|
return feature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
public record BiomeCodes(Field surfaceCode, Field caveCode, Field class3d, ClimateLayout layout, Map<String, String> loadKeyByBiomeId) {
|
||||||
|
public static final String BASE_CODE = "biome/base_code";
|
||||||
|
|
||||||
|
public BiomeCodes {
|
||||||
|
Objects.requireNonNull(surfaceCode);
|
||||||
|
Objects.requireNonNull(caveCode);
|
||||||
|
Objects.requireNonNull(class3d);
|
||||||
|
Objects.requireNonNull(layout);
|
||||||
|
FieldValidator.requireTwoDimensional(surfaceCode, "surface code");
|
||||||
|
FieldValidator.requireTwoDimensional(caveCode, "cave code");
|
||||||
|
SortedMap<String, String> loadKeys = new TreeMap<>(loadKeyByBiomeId);
|
||||||
|
for (Map.Entry<String, String> entry : loadKeys.entrySet()) {
|
||||||
|
VanillaResourceIdentifier.requireIdentifier(entry.getKey(), "biome id");
|
||||||
|
Objects.requireNonNull(entry.getValue());
|
||||||
|
}
|
||||||
|
loadKeyByBiomeId = Collections.unmodifiableSortedMap(loadKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class BiomeDecision {
|
||||||
|
static final int LAND = 0;
|
||||||
|
static final int SEA = 1;
|
||||||
|
static final int SHORE = 2;
|
||||||
|
static final int SURFACE_SLOT = 0;
|
||||||
|
private static final double UNDERGROUND_DEPTH = 8.0;
|
||||||
|
private static final int BRIDGE_WEIGHT = 8;
|
||||||
|
private static final int LEVEL_WEIGHT = 2;
|
||||||
|
private static final int ZONE_WEIGHT = 4;
|
||||||
|
private static final int DEPTH_WEIGHT = 2;
|
||||||
|
private static final Field UNUSED_CODE = new Field.Const(0.0);
|
||||||
|
|
||||||
|
private final BiomeCodes base;
|
||||||
|
private final Parts parts;
|
||||||
|
|
||||||
|
BiomeDecision(BiomeCodes base, Parts parts) {
|
||||||
|
this.base = Objects.requireNonNull(base);
|
||||||
|
this.parts = Objects.requireNonNull(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiomeCodes base() {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiomeCodes bind(Field height) {
|
||||||
|
FieldValidator.requireTwoDimensional(Objects.requireNonNull(height), "height");
|
||||||
|
return new BiomeCodes(surfaceCode(height), base.caveCode(), class3d(height), base.layout(), base.loadKeyByBiomeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field surfaceCode(Field height) {
|
||||||
|
double fluid = parts.fluid();
|
||||||
|
double fluidTop = Math.nextUp((float) fluid);
|
||||||
|
IndexedField levels = IndexedField.partition(height, List.of(fluid - 1.0, fluid, fluidTop), List.of(0, LEVEL_WEIGHT, 2 * LEVEL_WEIGHT, 3 * LEVEL_WEIGHT));
|
||||||
|
IndexedField above = IndexedField.partition(new Field.Binary(Field.BinaryOp.SUB, height, parts.shoreHeight()), List.of(fluidTop), List.of(0, 1));
|
||||||
|
IndexedField key = IndexedField.sum(List.of(parts.bridge().remap(bridge -> bridge * BRIDGE_WEIGHT), levels, above));
|
||||||
|
IndexedField list = key.remap(BiomeDecision::listFor);
|
||||||
|
List<Field> codes = new ArrayList<>(parts.chains().size());
|
||||||
|
for (IndexedField chain : parts.chains()) {
|
||||||
|
codes.add(chain.lookup(index -> parts.surfaceCodes()[index]));
|
||||||
|
}
|
||||||
|
if (list.isConstant()) {
|
||||||
|
return codes.get(list.values().getFirst());
|
||||||
|
}
|
||||||
|
List<Field> cases = new ArrayList<>(list.values().getLast() + 1);
|
||||||
|
for (int value = 0; value <= list.values().getLast(); value++) {
|
||||||
|
cases.add(list.values().contains(value) ? codes.get(value) : UNUSED_CODE);
|
||||||
|
}
|
||||||
|
return new Field.Mux(list.field(), cases);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field class3d(Field height) {
|
||||||
|
if (parts.caveSlot().equals(IndexedField.constant(SURFACE_SLOT)) && parts.carvingSlots().isEmpty()) {
|
||||||
|
return new Field.Const(0.0);
|
||||||
|
}
|
||||||
|
IndexedField zones = parts.zoneKeys();
|
||||||
|
Field caveExtra = new Field.Binary(Field.BinaryOp.SUB, parts.requiredDepth(), new Field.Const(UNDERGROUND_DEPTH));
|
||||||
|
Field caveZone = zones.lookup(key -> key / ZONE_WEIGHT == CarvingZones.CAVE_ZONE ? 1.0 : 0.0);
|
||||||
|
Field depthGate = new Field.Binary(Field.BinaryOp.ADD, new Field.Const(UNDERGROUND_DEPTH), new Field.Gate(caveZone, caveExtra));
|
||||||
|
Field clearance = new Field.Binary(Field.BinaryOp.SUB, new Field.Binary(Field.BinaryOp.SUB, height, parts.y()), depthGate);
|
||||||
|
IndexedField deepEnough = IndexedField.partition(clearance, List.of(0.0), List.of(0, 1));
|
||||||
|
IndexedField key = IndexedField.sum(List.of(zones.remap(zone -> zone / ZONE_WEIGHT * DEPTH_WEIGHT), deepEnough));
|
||||||
|
IndexedField classIndex = key.remap(BiomeDecision::classFor);
|
||||||
|
List<IndexedField> cases = new ArrayList<>(parts.carvingSlots().size() + 2);
|
||||||
|
cases.add(IndexedField.constant(SURFACE_SLOT));
|
||||||
|
cases.add(parts.caveSlot());
|
||||||
|
cases.addAll(parts.carvingSlots());
|
||||||
|
IndexedField slot = IndexedField.select(classIndex, cases);
|
||||||
|
return slot.lookup(value -> value == SURFACE_SLOT ? 0.0 : parts.undergroundCodes()[value - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int listFor(int key) {
|
||||||
|
boolean landBase = key / BRIDGE_WEIGHT == LAND;
|
||||||
|
int level = (key % BRIDGE_WEIGHT) / LEVEL_WEIGHT;
|
||||||
|
boolean aboveShoreBand = key % LEVEL_WEIGHT == 1;
|
||||||
|
boolean nearFluid = level >= 1;
|
||||||
|
boolean belowFluid = level <= 1;
|
||||||
|
boolean atFluid = level == 2;
|
||||||
|
int base = landBase ? LAND : SEA;
|
||||||
|
if (nearFluid) {
|
||||||
|
if (!aboveShoreBand) {
|
||||||
|
return SHORE;
|
||||||
|
}
|
||||||
|
if (!landBase) {
|
||||||
|
return LAND;
|
||||||
|
}
|
||||||
|
} else if (!landBase && aboveShoreBand) {
|
||||||
|
return LAND;
|
||||||
|
}
|
||||||
|
if (belowFluid && landBase) {
|
||||||
|
return SEA;
|
||||||
|
}
|
||||||
|
if (atFluid) {
|
||||||
|
return SHORE;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int classFor(int key) {
|
||||||
|
int zone = key / DEPTH_WEIGHT;
|
||||||
|
boolean deepEnough = key % DEPTH_WEIGHT == 1;
|
||||||
|
if (zone == CarvingZones.SHALLOW_ZONE || !deepEnough) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return zone == CarvingZones.CAVE_ZONE ? 1 : zone;
|
||||||
|
}
|
||||||
|
|
||||||
|
record Parts(
|
||||||
|
double fluid,
|
||||||
|
IndexedField bridge,
|
||||||
|
Field shoreHeight,
|
||||||
|
List<IndexedField> chains,
|
||||||
|
double[] surfaceCodes,
|
||||||
|
Field y,
|
||||||
|
IndexedField zoneKeys,
|
||||||
|
IndexedField caveSlot,
|
||||||
|
Field requiredDepth,
|
||||||
|
List<IndexedField> carvingSlots,
|
||||||
|
double[] undergroundCodes) {
|
||||||
|
Parts {
|
||||||
|
Objects.requireNonNull(bridge);
|
||||||
|
Objects.requireNonNull(shoreHeight);
|
||||||
|
chains = List.copyOf(chains);
|
||||||
|
Objects.requireNonNull(surfaceCodes);
|
||||||
|
Objects.requireNonNull(y);
|
||||||
|
Objects.requireNonNull(zoneKeys);
|
||||||
|
Objects.requireNonNull(caveSlot);
|
||||||
|
Objects.requireNonNull(requiredDepth);
|
||||||
|
carvingSlots = List.copyOf(carvingSlots);
|
||||||
|
Objects.requireNonNull(undergroundCodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+299
@@ -0,0 +1,299 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||||
|
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||||
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public final class BiomeDecisionCompiler {
|
||||||
|
private static final long REGION_SALT = 883L;
|
||||||
|
private static final long CONTINENT_SALT = 234_234_565L;
|
||||||
|
private static final long PICK_SALT = 1_000L;
|
||||||
|
private static final long CHILD_SALT = 123L;
|
||||||
|
private static final long SHORE_SALT = 3_458_612L;
|
||||||
|
private static final double SURFACE_SPAN = 1.0;
|
||||||
|
private static final double UNDERGROUND_MIN = 1.25;
|
||||||
|
private static final double UNDERGROUND_MAX = 2.0;
|
||||||
|
private static final double MINIMUM_CAVE_DEPTH = 8.0;
|
||||||
|
private static final double NO_CAVE_DEPTH = 100_000.0;
|
||||||
|
private static final int DEEP_FLOOR_Y = -8;
|
||||||
|
private static final int DEEP_ABOVE_MIN_Y = 40;
|
||||||
|
|
||||||
|
private final StyleCompiler styles;
|
||||||
|
|
||||||
|
public BiomeDecisionCompiler(StyleCompiler styles) {
|
||||||
|
this.styles = Objects.requireNonNull(styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiomeDecision compile(PackCompileContext context) {
|
||||||
|
IrisDimension dimension = context.dimension();
|
||||||
|
IrisData data = context.data();
|
||||||
|
DimensionDef dimensionDef = context.dimensionDef();
|
||||||
|
BiomePools pools = BiomePools.load(dimension, data);
|
||||||
|
CarvingZones carving = CarvingZones.plan(dimension, data, Math.max(DEEP_FLOOR_Y, dimensionDef.minY() + DEEP_ABOVE_MIN_Y));
|
||||||
|
List<String> surfaceKeys = List.copyOf(pools.surface());
|
||||||
|
SortedSet<String> undergroundSet = new TreeSet<>(pools.cave());
|
||||||
|
undergroundSet.addAll(carving.biomes());
|
||||||
|
List<String> undergroundKeys = List.copyOf(undergroundSet);
|
||||||
|
Map<String, Integer> surfaceIndex = indexOf(surfaceKeys);
|
||||||
|
Map<String, Integer> undergroundIndex = indexOf(undergroundKeys);
|
||||||
|
Map<String, String> ids = biomeIds(context.namespace(), surfaceKeys, undergroundKeys);
|
||||||
|
double[] surfaceCodes = codes(-SURFACE_SPAN, SURFACE_SPAN, surfaceKeys.size());
|
||||||
|
double[] undergroundCodes = codes(UNDERGROUND_MIN, UNDERGROUND_MAX, undergroundKeys.size());
|
||||||
|
Build build = new Build(pools, surfaceKeys, surfaceIndex,
|
||||||
|
new SelectorCompiler(styles.selectors(), new SelectorCompiler.Sources(data, context.noises(), context.capabilities())));
|
||||||
|
IndexedField region = build.region(dimension);
|
||||||
|
IndexedField bridge = build.selectors.scaled(dimension.getContinentalStyle(), 1.0 / dimension.getContinentZoom(), CONTINENT_SALT)
|
||||||
|
.partition(List.of(dimension.getLandChance()), List.of(BiomeDecision.LAND, BiomeDecision.SEA))
|
||||||
|
.shared("biome/bridge");
|
||||||
|
RegionPicks picks = build.regionPicks(dimension, bridge, undergroundIndex, context.diagnostics());
|
||||||
|
List<IndexedField> chains = List.of(
|
||||||
|
build.implode("biome/land", IndexedField.select(region, picks.land()).shared("biome/land/pick")),
|
||||||
|
build.implode("biome/sea", IndexedField.select(region, picks.sea()).shared("biome/sea/pick")),
|
||||||
|
build.implode("biome/shore", IndexedField.select(region, picks.shore()).shared("biome/shore/pick")));
|
||||||
|
IndexedField baseSurface = IndexedField.select(bridge, chains.subList(0, 2));
|
||||||
|
IndexedField caveSlot = IndexedField.select(region, picks.caves()).shared("biome/cave");
|
||||||
|
Field caveCode = caveSlot.lookup(value -> value == BiomeDecision.SURFACE_SLOT ? 0.0 : undergroundCodes[value - 1]);
|
||||||
|
Field requiredDepth = caveSlot.lookup(value -> value == BiomeDecision.SURFACE_SLOT
|
||||||
|
? NO_CAVE_DEPTH
|
||||||
|
: Math.max(MINIMUM_CAVE_DEPTH, pools.biome(undergroundKeys.get(value - 1)).getCaveMinDepthBelowSurface()));
|
||||||
|
Field y = blockY(dimensionDef);
|
||||||
|
List<IndexedField> carvingSlots = carving.slots(build.selectors, data, key -> undergroundIndex.get(key) + 1);
|
||||||
|
BiomeCodes base = new BiomeCodes(
|
||||||
|
new Field.Shared(BiomeCodes.BASE_CODE, baseSurface.lookup(index -> surfaceCodes[index])),
|
||||||
|
caveCode,
|
||||||
|
new Field.Const(0.0),
|
||||||
|
layout(ids, surfaceKeys, surfaceCodes, undergroundKeys, undergroundCodes),
|
||||||
|
loadKeys(ids));
|
||||||
|
BiomeDecision.Parts parts = new BiomeDecision.Parts(
|
||||||
|
dimensionDef.fluidHeight(),
|
||||||
|
bridge,
|
||||||
|
shoreHeight(region, picks.shoreHeights()),
|
||||||
|
chains,
|
||||||
|
surfaceCodes,
|
||||||
|
y,
|
||||||
|
carving.zoneKeys(y),
|
||||||
|
caveSlot,
|
||||||
|
requiredDepth,
|
||||||
|
carvingSlots,
|
||||||
|
undergroundCodes);
|
||||||
|
return new BiomeDecision(base, parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field shoreHeight(IndexedField region, List<Field> heights) {
|
||||||
|
if (region.isConstant()) {
|
||||||
|
return heights.get(region.values().getFirst());
|
||||||
|
}
|
||||||
|
if (new HashSet<>(heights).size() == 1) {
|
||||||
|
return heights.getFirst();
|
||||||
|
}
|
||||||
|
return new Field.Shared("biome/shore_height", new Field.Mux(region.field(), heights));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedMap<String, String> loadKeys(Map<String, String> ids) {
|
||||||
|
SortedMap<String, String> loadKeys = new TreeMap<>();
|
||||||
|
for (Map.Entry<String, String> entry : ids.entrySet()) {
|
||||||
|
loadKeys.put(entry.getValue(), entry.getKey());
|
||||||
|
}
|
||||||
|
return loadKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Integer> indexOf(List<String> keys) {
|
||||||
|
Map<String, Integer> index = new HashMap<>(keys.size() * 2);
|
||||||
|
for (int position = 0; position < keys.size(); position++) {
|
||||||
|
index.put(keys.get(position), position);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> biomeIds(String namespace, List<String> surfaceKeys, List<String> undergroundKeys) {
|
||||||
|
SortedSet<String> all = new TreeSet<>(surfaceKeys);
|
||||||
|
all.addAll(undergroundKeys);
|
||||||
|
Set<String> taken = new HashSet<>();
|
||||||
|
Map<String, String> ids = new TreeMap<>();
|
||||||
|
for (String key : all) {
|
||||||
|
ids.put(key, BiomeIdSanitizer.sanitize(namespace, key, taken));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double[] codes(double min, double max, int count) {
|
||||||
|
double[] codes = new double[count];
|
||||||
|
double half = (max - min) / (2.0 * Math.max(1, count));
|
||||||
|
for (int index = 0; index < count; index++) {
|
||||||
|
codes[index] = min + (2 * index + 1) * half;
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClimateLayout layout(Map<String, String> ids, List<String> surfaceKeys, double[] surfaceCodes, List<String> undergroundKeys, double[] undergroundCodes) {
|
||||||
|
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>(surfaceKeys.size() + undergroundKeys.size());
|
||||||
|
double surfaceHalf = SURFACE_SPAN / Math.max(1, surfaceKeys.size());
|
||||||
|
for (int index = 0; index < surfaceKeys.size(); index++) {
|
||||||
|
entries.add(new ClimateLayout.ClimateEntry(ids.get(surfaceKeys.get(index)), ClimateLayout.ClimateAxis.TEMPERATURE,
|
||||||
|
surfaceCodes[index] - surfaceHalf, surfaceCodes[index] + surfaceHalf));
|
||||||
|
}
|
||||||
|
double undergroundHalf = (UNDERGROUND_MAX - UNDERGROUND_MIN) / (2.0 * Math.max(1, undergroundKeys.size()));
|
||||||
|
for (int index = 0; index < undergroundKeys.size(); index++) {
|
||||||
|
entries.add(new ClimateLayout.ClimateEntry(ids.get(undergroundKeys.get(index)), ClimateLayout.ClimateAxis.DEPTH,
|
||||||
|
undergroundCodes[index] - undergroundHalf, Math.min(UNDERGROUND_MAX, undergroundCodes[index] + undergroundHalf)));
|
||||||
|
}
|
||||||
|
return new ClimateLayout(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field blockY(DimensionDef dimension) {
|
||||||
|
double bottom = dimension.minY();
|
||||||
|
double top = dimension.minY() + dimension.height();
|
||||||
|
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, bottom, top, bottom, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Build {
|
||||||
|
private final BiomePools pools;
|
||||||
|
private final List<String> surfaceKeys;
|
||||||
|
private final Map<String, Integer> surfaceIndex;
|
||||||
|
private final SelectorCompiler selectors;
|
||||||
|
private final Map<String, IndexedField> hops = new HashMap<>();
|
||||||
|
|
||||||
|
private Build(BiomePools pools, List<String> surfaceKeys, Map<String, Integer> surfaceIndex, SelectorCompiler selectors) {
|
||||||
|
this.pools = pools;
|
||||||
|
this.surfaceKeys = surfaceKeys;
|
||||||
|
this.surfaceIndex = surfaceIndex;
|
||||||
|
this.selectors = selectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexedField region(IrisDimension dimension) {
|
||||||
|
List<IrisRegion> regions = new ArrayList<>(pools.regions().size());
|
||||||
|
List<Integer> leaves = new ArrayList<>(pools.regions().size());
|
||||||
|
for (BiomePools.RegionPools regionPools : pools.regions()) {
|
||||||
|
leaves.add(regions.size());
|
||||||
|
regions.add(regionPools.region());
|
||||||
|
}
|
||||||
|
if (regions.size() == 1) {
|
||||||
|
return IndexedField.constant(0);
|
||||||
|
}
|
||||||
|
return selectors.zoomed(dimension.getRegionStyle(), dimension.getRegionZoom(), REGION_SALT)
|
||||||
|
.partition(BiomePools.rarityCuts(regions), leaves)
|
||||||
|
.shared("biome/region");
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegionPicks regionPicks(IrisDimension dimension, IndexedField bridge, Map<String, Integer> undergroundIndex, List<ExportDiagnostic> diagnostics) {
|
||||||
|
int count = pools.regions().size();
|
||||||
|
RegionPicks picks = new RegionPicks(new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count));
|
||||||
|
for (int position = 0; position < count; position++) {
|
||||||
|
BiomePools.RegionPools regionPools = pools.regions().get(position);
|
||||||
|
IrisRegion region = regionPools.region();
|
||||||
|
IndexedField landPick = pick(dimension, regionPools, InferredType.LAND, surfaceIndex::get, 0);
|
||||||
|
IndexedField seaPick = pick(dimension, regionPools, InferredType.SEA, surfaceIndex::get, 0);
|
||||||
|
IndexedField land = landPick == null ? seaPick : landPick;
|
||||||
|
IndexedField sea = seaPick == null ? landPick : seaPick;
|
||||||
|
IndexedField shore = pick(dimension, regionPools, InferredType.SHORE, surfaceIndex::get, 0);
|
||||||
|
IndexedField cave = pick(dimension, regionPools, InferredType.CAVE, undergroundIndex::get, 1);
|
||||||
|
picks.land().add(land);
|
||||||
|
picks.sea().add(sea);
|
||||||
|
picks.shore().add(shore == null ? IndexedField.select(bridge, List.of(land, sea)) : shore);
|
||||||
|
picks.caves().add(cave == null ? IndexedField.constant(BiomeDecision.SURFACE_SLOT) : cave);
|
||||||
|
picks.shoreHeights().add(shoreHeight(region, position));
|
||||||
|
if (region.getShoreMinimumWidth() > 0.0) {
|
||||||
|
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.SHORE_WIDTH, "regions/" + region.getLoadKey(),
|
||||||
|
"Shore widening by slope is not exported; the shore band follows the shore height only"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return picks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexedField pick(IrisDimension dimension, BiomePools.RegionPools regionPools, InferredType type, Function<String, Integer> index, int offset) {
|
||||||
|
List<IrisBiome> pool = regionPools.of(type);
|
||||||
|
if (pool.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<Integer> leaves = new ArrayList<>(pool.size());
|
||||||
|
for (IrisBiome biome : pool) {
|
||||||
|
leaves.add(index.apply(biome.getLoadKey()) + offset);
|
||||||
|
}
|
||||||
|
if (pool.size() == 1) {
|
||||||
|
return IndexedField.constant(leaves.getFirst());
|
||||||
|
}
|
||||||
|
IrisRegion region = regionPools.region();
|
||||||
|
double zoom = switch (type) {
|
||||||
|
case LAND -> dimension.getBiomeZoom() * dimension.getLandZoom() * region.getLandBiomeZoom();
|
||||||
|
case SEA -> dimension.getBiomeZoom() * dimension.getSeaZoom() * region.getSeaBiomeZoom();
|
||||||
|
case SHORE -> dimension.getBiomeZoom() * region.getShoreBiomeZoom();
|
||||||
|
case CAVE -> dimension.getBiomeZoom() * region.getCaveBiomeZoom();
|
||||||
|
};
|
||||||
|
IrisGeneratorStyle style = switch (type) {
|
||||||
|
case LAND -> dimension.getLandBiomeStyle();
|
||||||
|
case SEA -> dimension.getSeaBiomeStyle();
|
||||||
|
case SHORE -> dimension.getShoreBiomeStyle();
|
||||||
|
case CAVE -> dimension.getCaveBiomeStyle();
|
||||||
|
};
|
||||||
|
return selectors.zoomed(style, zoom, PICK_SALT + type.ordinal()).partition(BiomePools.rarityCuts(pool), leaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field shoreHeight(IrisRegion region, int position) {
|
||||||
|
if (region.getShoreHeightMin() == region.getShoreHeightMax()) {
|
||||||
|
return new Field.Const(region.getShoreHeightMin());
|
||||||
|
}
|
||||||
|
return selectors.sampled(NoiseStyle.IRIS.style(), region.getShoreHeightZoom(), SHORE_SALT + position, region::getShoreHeight).curve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexedField implode(String name, IndexedField start) {
|
||||||
|
IndexedField current = start;
|
||||||
|
for (int hop = 0; hop < BiomePools.MAX_CHILD_HOPS; hop++) {
|
||||||
|
List<Integer> movers = new ArrayList<>();
|
||||||
|
for (int value : current.values()) {
|
||||||
|
BiomePools.ChildPlan plan = pools.plan(surfaceKeys.get(value));
|
||||||
|
if (plan != null && plan.moves()) {
|
||||||
|
movers.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (movers.isEmpty()) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
Map<Integer, IndexedField> moved = new HashMap<>(movers.size() * 2);
|
||||||
|
for (int mover : movers) {
|
||||||
|
moved.put(mover, hops.computeIfAbsent(surfaceKeys.get(mover), this::hop));
|
||||||
|
}
|
||||||
|
current = IndexedField.advance(current, moved).shared(name + "/hop" + (hop + 1));
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexedField hop(String parent) {
|
||||||
|
IrisBiome biome = pools.biome(parent);
|
||||||
|
BiomePools.ChildPlan plan = pools.plan(parent);
|
||||||
|
List<Integer> leaves = new ArrayList<>(plan.choiceByIndex().size());
|
||||||
|
for (String choice : plan.choiceByIndex()) {
|
||||||
|
leaves.add(surfaceIndex.get(choice));
|
||||||
|
}
|
||||||
|
return selectors.scaled(biome.getChildStyle(), biome.getChildShrinkFactor(), CHILD_SALT)
|
||||||
|
.partition(BiomePools.selectionCuts(plan.choiceByIndex().size()), leaves);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RegionPicks(List<IndexedField> land, List<IndexedField> sea, List<IndexedField> shore, List<IndexedField> caves, List<Field> shoreHeights) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+299
@@ -0,0 +1,299 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.generation.biome.IrisVanillaBiomeTags;
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
|
||||||
|
import art.arcane.iris.pack.validation.ContentGate;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.VanillaDataSource;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
public final class BiomeDefinitionCompiler {
|
||||||
|
private static final String VANILLA_NAMESPACE = "minecraft";
|
||||||
|
private static final String BIOME_FOLDER = "/worldgen/biome/";
|
||||||
|
private static final String TAG_FOLDER = "/tags/worldgen/biome";
|
||||||
|
private static final String JSON_SUFFIX = ".json";
|
||||||
|
private static final String SPAWNS_ATTRIBUTE = "minecraft:gameplay/natural_mob_spawns";
|
||||||
|
private static final String SPAWN_PROBABILITY_ATTRIBUTE = "minecraft:gameplay/creature_world_gen_spawn_probability";
|
||||||
|
private static final String SPAWN_PROBABILITY = "creature_spawn_probability";
|
||||||
|
private static final List<String> CLIMATE_KEYS = List.of("has_precipitation", "temperature", "downfall", "temperature_modifier");
|
||||||
|
private static final List<String> CUSTOM_KEYS = List.of("has_precipitation", "temperature", "downfall", "temperature_modifier",
|
||||||
|
"effects", "attributes", "spawners", "spawn_costs", SPAWN_PROBABILITY);
|
||||||
|
|
||||||
|
private final Optional<VanillaDataSource> vanilla;
|
||||||
|
|
||||||
|
public BiomeDefinitionCompiler(Optional<VanillaDataSource> vanilla) {
|
||||||
|
this.vanilla = Objects.requireNonNull(vanilla);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BiomeDefinitionCompiler forTarget(DataVersion target) {
|
||||||
|
if (!IrisPlatforms.isBound()) {
|
||||||
|
return new BiomeDefinitionCompiler(Optional.empty());
|
||||||
|
}
|
||||||
|
IrisPlatform platform = IrisPlatforms.get();
|
||||||
|
boolean running = DataVersion.forMinecraftVersion(platform.minecraftVersion()) == target;
|
||||||
|
return new BiomeDefinitionCompiler(running ? Optional.ofNullable(platform.vanillaData()) : Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Definitions compile(PackCompileContext context, Map<String, String> loadKeyByBiomeId) {
|
||||||
|
DataVersion target = context.capabilities().target();
|
||||||
|
ContentGate gate = vanilla.isPresent() ? ContentGate.forData(context.data()) : new ContentGate(null, Map.of(), null);
|
||||||
|
if (vanilla.isEmpty()) {
|
||||||
|
diagnose(context, "dimensions/" + context.dimension().getLoadKey(), "Vanilla biome data for Minecraft " + target.getVersion()
|
||||||
|
+ " is not available on this server; biomes are built from their Iris fields without vanilla music, ambience, spawns or structure tags");
|
||||||
|
}
|
||||||
|
Map<String, List<String>> vanillaTags = vanilla.map(BiomeDefinitionCompiler::directTags).orElse(Map.of());
|
||||||
|
SortedMap<String, SortedSet<String>> members = new TreeMap<>();
|
||||||
|
List<BiomeDef> biomes = new ArrayList<>(loadKeyByBiomeId.size());
|
||||||
|
for (Map.Entry<String, String> entry : new TreeMap<>(loadKeyByBiomeId).entrySet()) {
|
||||||
|
String id = entry.getKey();
|
||||||
|
IrisBiome biome = Objects.requireNonNull(context.data().getBiomeLoader().load(entry.getValue(), false),
|
||||||
|
"Biome '" + entry.getValue() + "' could not be loaded");
|
||||||
|
IrisBiomeCustom custom = custom(context, biome);
|
||||||
|
String visualKey = biome.getBiomeScatter().isEmpty() ? biome.getDerivativeKey() : namespaced(biome.getBiomeScatter().getFirst());
|
||||||
|
Optional<JsonObject> base = vanilla.flatMap(source -> read(source, visualKey));
|
||||||
|
if (vanilla.isPresent() && base.isEmpty()) {
|
||||||
|
diagnose(context, "biomes/" + biome.getLoadKey(), "Vanilla biome '" + visualKey + "' has no data for Minecraft " + target.getVersion()
|
||||||
|
+ "; the biome is built from its Iris fields");
|
||||||
|
}
|
||||||
|
JsonObject overlay = custom == null ? null : customJson(custom, target, gate);
|
||||||
|
JsonObject json = base.isPresent()
|
||||||
|
? merge(base.get(), overlay, custom)
|
||||||
|
: retained(overlay == null ? customJson(new IrisBiomeCustom(), target, gate) : overlay);
|
||||||
|
json.add("carvers", new JsonArray());
|
||||||
|
json.add("features", new JsonArray());
|
||||||
|
biomes.add(new BiomeDef(id.substring(id.indexOf(':') + 1), json));
|
||||||
|
for (String tag : tags(biome, custom, vanillaTags)) {
|
||||||
|
members.computeIfAbsent(tag, ignored -> new TreeSet<>()).add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<JsonResource> tags = new ArrayList<>(members.size());
|
||||||
|
for (Map.Entry<String, SortedSet<String>> tag : members.entrySet()) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("replace", false);
|
||||||
|
JsonArray values = new JsonArray(tag.getValue().size());
|
||||||
|
for (String member : tag.getValue()) {
|
||||||
|
values.add(member);
|
||||||
|
}
|
||||||
|
json.add("values", values);
|
||||||
|
int separator = tag.getKey().indexOf(':');
|
||||||
|
tags.add(new JsonResource("data/" + tag.getKey().substring(0, separator) + TAG_FOLDER + "/" + tag.getKey().substring(separator + 1) + JSON_SUFFIX, json));
|
||||||
|
}
|
||||||
|
return new Definitions(biomes, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisBiomeCustom custom(PackCompileContext context, IrisBiome biome) {
|
||||||
|
boolean customs = biome.getCustomDerivitives() != null && biome.getCustomDerivitives().size() > 1;
|
||||||
|
if (customs || biome.getBiomeScatter().size() > 1) {
|
||||||
|
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.BIOME_VARIANT, "biomes/" + biome.getLoadKey(),
|
||||||
|
"Biomes with several custom derivatives or scatter biomes export as one biome using the first entry"));
|
||||||
|
}
|
||||||
|
return biome.isCustom() ? biome.getCustomDerivitives().getFirst() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject customJson(IrisBiomeCustom custom, DataVersion target, ContentGate gate) {
|
||||||
|
return JsonParser.parseString(custom.generateJson(target.get(), gate)).getAsJsonObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject merge(JsonObject base, JsonObject overlay, IrisBiomeCustom custom) {
|
||||||
|
JsonObject merged = base.deepCopy();
|
||||||
|
if (overlay == null) {
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
for (String key : CLIMATE_KEYS) {
|
||||||
|
if (overlay.has(key)) {
|
||||||
|
merged.add(key, overlay.get(key).deepCopy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean spawnRarity = custom.getSpawnRarity() > 0;
|
||||||
|
putAll(object(merged, "effects"), overlay.getAsJsonObject("effects"));
|
||||||
|
JsonObject overlayAttributes = overlay.getAsJsonObject("attributes");
|
||||||
|
if (overlayAttributes != null) {
|
||||||
|
JsonObject attributes = object(merged, "attributes");
|
||||||
|
for (Map.Entry<String, JsonElement> attribute : overlayAttributes.entrySet()) {
|
||||||
|
switch (attribute.getKey()) {
|
||||||
|
case SPAWNS_ATTRIBUTE -> mergeSpawnAttribute(attributes, attribute.getValue());
|
||||||
|
case SPAWN_PROBABILITY_ATTRIBUTE -> {
|
||||||
|
if (spawnRarity) {
|
||||||
|
attributes.add(attribute.getKey(), attribute.getValue().deepCopy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> attributes.add(attribute.getKey(), attribute.getValue().deepCopy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overlay.has("spawners")) {
|
||||||
|
mergeSpawns(merged, overlay);
|
||||||
|
}
|
||||||
|
if (spawnRarity && overlay.has(SPAWN_PROBABILITY)) {
|
||||||
|
merged.add(SPAWN_PROBABILITY, overlay.get(SPAWN_PROBABILITY).deepCopy());
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mergeSpawnAttribute(JsonObject attributes, JsonElement overlay) {
|
||||||
|
JsonElement existing = attributes.get(SPAWNS_ATTRIBUTE);
|
||||||
|
if (existing == null || !existing.isJsonObject() || !overlay.isJsonObject()) {
|
||||||
|
attributes.add(SPAWNS_ATTRIBUTE, overlay.deepCopy());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JsonObject target = spawnHolder(existing.getAsJsonObject());
|
||||||
|
JsonObject source = spawnHolder(overlay.getAsJsonObject());
|
||||||
|
appendCategories(object(target, "spawns_by_category"), source.getAsJsonObject("spawns_by_category"));
|
||||||
|
putAll(object(target, "spawn_costs"), source.getAsJsonObject("spawn_costs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mergeSpawns(JsonObject merged, JsonObject overlay) {
|
||||||
|
appendCategories(object(merged, "spawners"), overlay.getAsJsonObject("spawners"));
|
||||||
|
putAll(object(merged, "spawn_costs"), overlay.getAsJsonObject("spawn_costs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject spawnHolder(JsonObject attribute) {
|
||||||
|
JsonElement argument = attribute.get("argument");
|
||||||
|
return argument != null && argument.isJsonObject() ? argument.getAsJsonObject() : attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendCategories(JsonObject target, JsonObject source) {
|
||||||
|
if (source == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JsonElement> category : source.entrySet()) {
|
||||||
|
JsonElement existing = target.get(category.getKey());
|
||||||
|
JsonArray spawns = existing != null && existing.isJsonArray() ? existing.getAsJsonArray() : new JsonArray();
|
||||||
|
spawns.addAll(category.getValue().deepCopy().getAsJsonArray());
|
||||||
|
target.add(category.getKey(), spawns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putAll(JsonObject target, JsonObject source) {
|
||||||
|
if (source == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JsonElement> entry : source.entrySet()) {
|
||||||
|
target.add(entry.getKey(), entry.getValue().deepCopy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject object(JsonObject parent, String key) {
|
||||||
|
JsonElement existing = parent.get(key);
|
||||||
|
if (existing != null && existing.isJsonObject()) {
|
||||||
|
return existing.getAsJsonObject();
|
||||||
|
}
|
||||||
|
JsonObject created = new JsonObject();
|
||||||
|
parent.add(key, created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject retained(JsonObject custom) {
|
||||||
|
JsonObject retained = new JsonObject();
|
||||||
|
for (String key : CUSTOM_KEYS) {
|
||||||
|
if (custom.has(key)) {
|
||||||
|
retained.add(key, custom.get(key).deepCopy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retained;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SortedSet<String> tags(IrisBiome biome, IrisBiomeCustom custom, Map<String, List<String>> vanillaTags) {
|
||||||
|
SortedSet<String> tags = new TreeSet<>();
|
||||||
|
if (custom != null && custom.getTags() != null) {
|
||||||
|
for (String tag : custom.getTags()) {
|
||||||
|
String normalized = tagId(tag);
|
||||||
|
if (normalized != null) {
|
||||||
|
tags.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String structureKey = biome.getVanillaDerivativeKey();
|
||||||
|
tags.addAll(vanillaTags.isEmpty() ? IrisVanillaBiomeTags.tagsFor(structureKey) : vanillaTags.getOrDefault(structureKey, List.of()));
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<String>> directTags(VanillaDataSource source) {
|
||||||
|
String root = "data/" + VANILLA_NAMESPACE + TAG_FOLDER;
|
||||||
|
Map<String, List<String>> tags = new HashMap<>();
|
||||||
|
for (String path : source.list(root)) {
|
||||||
|
if (!path.endsWith(JSON_SUFFIX)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String tag = VANILLA_NAMESPACE + ":" + path.substring(root.length() + 1, path.length() - JSON_SUFFIX.length());
|
||||||
|
Optional<JsonObject> json = read(source, path);
|
||||||
|
JsonArray values = json.isEmpty() ? null : json.get().getAsJsonArray("values");
|
||||||
|
if (values == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (JsonElement value : values) {
|
||||||
|
String member = value.isJsonObject() ? value.getAsJsonObject().get("id").getAsString() : value.getAsString();
|
||||||
|
if (!member.startsWith("#")) {
|
||||||
|
tags.computeIfAbsent(namespaced(member), ignored -> new ArrayList<>()).add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<JsonObject> read(VanillaDataSource source, String biomeKeyOrPath) {
|
||||||
|
String path = biomeKeyOrPath.startsWith("data/") ? biomeKeyOrPath : biomePath(biomeKeyOrPath);
|
||||||
|
if (path == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return source.read(path).map(bytes -> JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String biomePath(String biomeKey) {
|
||||||
|
if (biomeKey == null || !VanillaResourceIdentifier.validIdentifier(biomeKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int separator = biomeKey.indexOf(':');
|
||||||
|
return "data/" + biomeKey.substring(0, separator) + BIOME_FOLDER + biomeKey.substring(separator + 1) + JSON_SUFFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String namespaced(String key) {
|
||||||
|
String trimmed = key.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return trimmed.indexOf(':') < 0 ? VANILLA_NAMESPACE + ":" + trimmed : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String tagId(String tag) {
|
||||||
|
if (tag == null || tag.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = tag.trim();
|
||||||
|
String normalized = namespaced(trimmed.startsWith("#") ? trimmed.substring(1) : trimmed);
|
||||||
|
return VanillaResourceIdentifier.validIdentifier(normalized) ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void diagnose(PackCompileContext context, String resource, String message) {
|
||||||
|
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.VANILLA_BIOME_DATA, resource, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Definitions(List<BiomeDef> biomes, List<JsonResource> tags) {
|
||||||
|
public Definitions {
|
||||||
|
biomes = List.copyOf(biomes);
|
||||||
|
tags = List.copyOf(tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class BiomeIdSanitizer {
|
||||||
|
private static final int FIRST_SUFFIX = 2;
|
||||||
|
|
||||||
|
private BiomeIdSanitizer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String sanitize(String namespace, String loadKey, Set<String> taken) {
|
||||||
|
VanillaResourceIdentifier.requireNamespace(namespace, "biome namespace");
|
||||||
|
Objects.requireNonNull(taken);
|
||||||
|
String path = VanillaResourceIdentifier.sanitizePath(loadKey);
|
||||||
|
String id = namespace + ":" + path;
|
||||||
|
int suffix = FIRST_SUFFIX;
|
||||||
|
while (taken.contains(id)) {
|
||||||
|
id = namespace + ":" + path + "_" + suffix;
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
taken.add(id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.value.IrisRaritySelection;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.math.Rarity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
final class BiomePools {
|
||||||
|
static final int MAX_CHILD_HOPS = 4;
|
||||||
|
static final List<InferredType> SURFACE_TYPES = List.of(InferredType.LAND, InferredType.SEA, InferredType.SHORE);
|
||||||
|
|
||||||
|
private final List<RegionPools> regions;
|
||||||
|
private final SortedMap<String, IrisBiome> biomes;
|
||||||
|
private final SortedSet<String> surface;
|
||||||
|
private final SortedSet<String> cave;
|
||||||
|
private final Map<String, ChildPlan> plans;
|
||||||
|
|
||||||
|
private BiomePools(List<RegionPools> regions, SortedMap<String, IrisBiome> biomes, SortedSet<String> surface, SortedSet<String> cave, Map<String, ChildPlan> plans) {
|
||||||
|
this.regions = regions;
|
||||||
|
this.biomes = biomes;
|
||||||
|
this.surface = surface;
|
||||||
|
this.cave = cave;
|
||||||
|
this.plans = plans;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BiomePools load(IrisDimension dimension, IrisData data) {
|
||||||
|
List<RegionPools> regions = new ArrayList<>();
|
||||||
|
SortedMap<String, IrisBiome> biomes = new TreeMap<>();
|
||||||
|
for (IrisRegion region : data.getRegionLoader().loadAll(dimension.getRegions())) {
|
||||||
|
if (region.isCompatExcluded()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RegionPools pools = new RegionPools(
|
||||||
|
region,
|
||||||
|
pool(data, region.getLandBiomes(), biomes),
|
||||||
|
pool(data, region.getSeaBiomes(), biomes),
|
||||||
|
pool(data, region.getShoreBiomes(), biomes),
|
||||||
|
pool(data, region.getCaveBiomes(), biomes));
|
||||||
|
if (pools.land().isEmpty() && pools.sea().isEmpty()) {
|
||||||
|
throw new IllegalStateException("Region '" + region.getLoadKey() + "' has no land or sea biome to export");
|
||||||
|
}
|
||||||
|
regions.add(pools);
|
||||||
|
}
|
||||||
|
if (regions.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Dimension '" + dimension.getLoadKey() + "' has no exportable region");
|
||||||
|
}
|
||||||
|
SortedSet<String> cave = new TreeSet<>();
|
||||||
|
SortedSet<String> frontier = new TreeSet<>();
|
||||||
|
for (RegionPools pools : regions) {
|
||||||
|
for (InferredType type : SURFACE_TYPES) {
|
||||||
|
for (IrisBiome biome : pools.of(type)) {
|
||||||
|
frontier.add(biome.getLoadKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (IrisBiome biome : pools.cave()) {
|
||||||
|
cave.add(biome.getLoadKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, ChildPlan> plans = new HashMap<>();
|
||||||
|
SortedSet<String> surface = new TreeSet<>(frontier);
|
||||||
|
for (int hop = 0; hop < MAX_CHILD_HOPS && !frontier.isEmpty(); hop++) {
|
||||||
|
SortedSet<String> next = new TreeSet<>();
|
||||||
|
for (String key : frontier) {
|
||||||
|
ChildPlan plan = plans.computeIfAbsent(key, ignored -> ChildPlan.of(biomes.get(key), data, biomes));
|
||||||
|
for (String choice : plan.choices()) {
|
||||||
|
if (surface.add(choice)) {
|
||||||
|
next.add(choice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frontier = next;
|
||||||
|
}
|
||||||
|
return new BiomePools(List.copyOf(regions), biomes, surface, cave, plans);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Double> rarityCuts(List<? extends Rarity> options) {
|
||||||
|
double total = 0.0;
|
||||||
|
for (Rarity option : options) {
|
||||||
|
total += 1.0 / Rarity.get(option);
|
||||||
|
}
|
||||||
|
List<Double> cuts = new ArrayList<>(Math.max(0, options.size() - 1));
|
||||||
|
double cumulative = 0.0;
|
||||||
|
for (int index = 0; index + 1 < options.size(); index++) {
|
||||||
|
cumulative += 1.0 / Rarity.get(options.get(index));
|
||||||
|
cuts.add(cumulative / total);
|
||||||
|
}
|
||||||
|
return cuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Double> selectionCuts(int size) {
|
||||||
|
int maximum = size - 1;
|
||||||
|
List<Double> cuts = new ArrayList<>(Math.max(0, maximum));
|
||||||
|
for (int index = 0; index < maximum; index++) {
|
||||||
|
cuts.add((index + 0.5) / maximum);
|
||||||
|
}
|
||||||
|
return cuts;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RegionPools> regions() {
|
||||||
|
return regions;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBiome biome(String loadKey) {
|
||||||
|
return Objects.requireNonNull(biomes.get(loadKey), "Biome '" + loadKey + "' was not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedSet<String> surface() {
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedSet<String> cave() {
|
||||||
|
return cave;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChildPlan plan(String loadKey) {
|
||||||
|
return plans.get(loadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<IrisBiome> pool(IrisData data, KList<String> keys, SortedMap<String, IrisBiome> biomes) {
|
||||||
|
List<IrisBiome> pool = new ArrayList<>();
|
||||||
|
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||||
|
if (biome.isCompatExcluded()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
biomes.putIfAbsent(biome.getLoadKey(), biome);
|
||||||
|
pool.add(biome);
|
||||||
|
}
|
||||||
|
return List.copyOf(pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
record RegionPools(IrisRegion region, List<IrisBiome> land, List<IrisBiome> sea, List<IrisBiome> shore, List<IrisBiome> cave) {
|
||||||
|
List<IrisBiome> of(InferredType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case LAND -> land;
|
||||||
|
case SEA -> sea;
|
||||||
|
case SHORE -> shore;
|
||||||
|
case CAVE -> cave;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ChildPlan(String parent, List<String> choiceByIndex) {
|
||||||
|
static ChildPlan of(IrisBiome parent, IrisData data, SortedMap<String, IrisBiome> biomes) {
|
||||||
|
if (parent.getChildren().isEmpty()) {
|
||||||
|
return new ChildPlan(parent.getLoadKey(), List.of(parent.getLoadKey()));
|
||||||
|
}
|
||||||
|
KList<IrisBiome> options = new KList<>();
|
||||||
|
for (IrisBiome child : parent.getRealChildren(() -> data)) {
|
||||||
|
if (child != null) {
|
||||||
|
biomes.putIfAbsent(child.getLoadKey(), child);
|
||||||
|
options.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.add(parent);
|
||||||
|
IrisRaritySelection<IrisBiome> selection = IrisRaritySelection.create(options);
|
||||||
|
long size = selection.size();
|
||||||
|
List<String> choices = new ArrayList<>((int) size);
|
||||||
|
for (long index = 0; index < size; index++) {
|
||||||
|
choices.add(selection.get(index).getLoadKey());
|
||||||
|
}
|
||||||
|
return new ChildPlan(parent.getLoadKey(), List.copyOf(choices));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean moves() {
|
||||||
|
for (String choice : choiceByIndex) {
|
||||||
|
if (!choice.equals(parent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedSet<String> choices() {
|
||||||
|
return new TreeSet<>(choiceByIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
|
||||||
|
|
||||||
|
public final class BiomeStage implements CompileStage {
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "biomes";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(PackCompileContext context) {
|
||||||
|
BiomeDecision decision = new BiomeDecisionCompiler(context.styles()).compile(context);
|
||||||
|
context.setBiomeDecision(decision);
|
||||||
|
BiomeDefinitionCompiler.Definitions definitions = BiomeDefinitionCompiler.forTarget(context.capabilities().target())
|
||||||
|
.compile(context, decision.base().loadKeyByBiomeId());
|
||||||
|
context.setBiomes(definitions.biomes());
|
||||||
|
context.jsonResources().addAll(definitions.tags());
|
||||||
|
}
|
||||||
|
}
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
|
import art.arcane.iris.pack.value.IrisRaritySelection;
|
||||||
|
import art.arcane.volmlib.util.math.Rarity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.ToIntFunction;
|
||||||
|
|
||||||
|
final class CarvingZones {
|
||||||
|
static final int SHALLOW_ZONE = 0;
|
||||||
|
static final int CAVE_ZONE = 1;
|
||||||
|
static final int FIRST_CARVING_ZONE = 2;
|
||||||
|
private static final int MAX_CHILD_DEPTH = 32;
|
||||||
|
private static final double MINIMUM_CHILD_SHRINK_FACTOR = 0.0001;
|
||||||
|
private static final long CARVING_SALT = 0x0CA4_7E00L;
|
||||||
|
|
||||||
|
private final List<Integer> cuts;
|
||||||
|
private final List<Integer> zoneByInterval;
|
||||||
|
private final List<Machine> machines;
|
||||||
|
private final SortedSet<String> biomes;
|
||||||
|
|
||||||
|
private CarvingZones(List<Integer> cuts, List<Integer> zoneByInterval, List<Machine> machines, SortedSet<String> biomes) {
|
||||||
|
this.cuts = cuts;
|
||||||
|
this.zoneByInterval = zoneByInterval;
|
||||||
|
this.machines = machines;
|
||||||
|
this.biomes = biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CarvingZones plan(IrisDimension dimension, IrisData data, int deepTop) {
|
||||||
|
List<IrisDimensionCarvingEntry> entries = new ArrayList<>();
|
||||||
|
if (dimension.getCarving() != null) {
|
||||||
|
for (IrisDimensionCarvingEntry entry : dimension.getCarving()) {
|
||||||
|
if (entry != null && entry.isEnabled() && entry.getRealBiome(data) != null) {
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SortedSet<Integer> cutSet = new TreeSet<>();
|
||||||
|
cutSet.add(deepTop + 1);
|
||||||
|
for (IrisDimensionCarvingEntry entry : entries) {
|
||||||
|
IrisRange range = entry.getWorldYRange();
|
||||||
|
if (range != null && range.getMin() <= range.getMax()) {
|
||||||
|
cutSet.add((int) Math.ceil(range.getMin()));
|
||||||
|
cutSet.add((int) Math.floor(range.getMax()) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Integer> cuts = List.copyOf(cutSet);
|
||||||
|
Map<String, IrisDimensionCarvingEntry> byId = dimension.getCarvingEntryIndex();
|
||||||
|
Map<ZoneKey, Integer> zones = new LinkedHashMap<>();
|
||||||
|
List<Machine> machines = new ArrayList<>();
|
||||||
|
SortedSet<String> biomes = new TreeSet<>();
|
||||||
|
List<Integer> zoneByInterval = new ArrayList<>(cuts.size() + 1);
|
||||||
|
for (int interval = 0; interval <= cuts.size(); interval++) {
|
||||||
|
int y = interval == 0 ? cuts.getFirst() - 1 : cuts.get(interval - 1);
|
||||||
|
if (y > deepTop) {
|
||||||
|
zoneByInterval.add(SHALLOW_ZONE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisDimensionCarvingEntry root = null;
|
||||||
|
List<Integer> inRange = new ArrayList<>();
|
||||||
|
for (int position = 0; position < entries.size(); position++) {
|
||||||
|
IrisDimensionCarvingEntry entry = entries.get(position);
|
||||||
|
if (contains(entry, y)) {
|
||||||
|
root = entry;
|
||||||
|
inRange.add(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root == null) {
|
||||||
|
zoneByInterval.add(CAVE_ZONE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ZoneKey key = new ZoneKey(entries.indexOf(root), List.copyOf(inRange));
|
||||||
|
Integer zone = zones.get(key);
|
||||||
|
if (zone == null) {
|
||||||
|
Machine machine = Machine.build(root, y, byId, data);
|
||||||
|
zone = FIRST_CARVING_ZONE + machines.size();
|
||||||
|
zones.put(key, zone);
|
||||||
|
machines.add(machine);
|
||||||
|
for (State state : machine.states()) {
|
||||||
|
biomes.add(state.entry().getRealBiome(data).getLoadKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zoneByInterval.add(zone);
|
||||||
|
}
|
||||||
|
return new CarvingZones(cuts, List.copyOf(zoneByInterval), List.copyOf(machines), biomes);
|
||||||
|
}
|
||||||
|
|
||||||
|
SortedSet<String> biomes() {
|
||||||
|
return biomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedField zoneKeys(Field y) {
|
||||||
|
List<Double> thresholds = new ArrayList<>(cuts.size());
|
||||||
|
for (int cut : cuts) {
|
||||||
|
thresholds.add((double) cut);
|
||||||
|
}
|
||||||
|
List<Integer> leaves = new ArrayList<>(zoneByInterval.size());
|
||||||
|
for (int zone : zoneByInterval) {
|
||||||
|
leaves.add(zone * 4);
|
||||||
|
}
|
||||||
|
return IndexedField.partition(y, thresholds, leaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IndexedField> slots(SelectorCompiler selectors, IrisData data, ToIntFunction<String> undergroundSlot) {
|
||||||
|
List<IndexedField> slots = new ArrayList<>(machines.size());
|
||||||
|
for (int zone = 0; zone < machines.size(); zone++) {
|
||||||
|
slots.add(machines.get(zone).slot("biome/carving/" + zone, selectors, data, undergroundSlot));
|
||||||
|
}
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean contains(IrisDimensionCarvingEntry entry, int y) {
|
||||||
|
IrisRange range = entry.getWorldYRange();
|
||||||
|
return range == null || range.contains(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int clampDepth(int depth) {
|
||||||
|
return depth <= 0 ? 0 : Math.min(depth, MAX_CHILD_DEPTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ZoneKey(int root, List<Integer> inRange) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record State(IrisDimensionCarvingEntry entry, int depth) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Move(IrisDimensionCarvingEntry entry, List<Integer> nextByIndex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CarvingOption(IrisDimensionCarvingEntry entry, int rarity) implements Rarity {
|
||||||
|
@Override
|
||||||
|
public int getRarity() {
|
||||||
|
return rarity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Machine(List<State> states, Map<Integer, Move> moves, int hops) {
|
||||||
|
static Machine build(IrisDimensionCarvingEntry root, int y, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
|
||||||
|
List<State> states = new ArrayList<>();
|
||||||
|
Map<State, Integer> ids = new HashMap<>();
|
||||||
|
Map<Integer, Move> moves = new HashMap<>();
|
||||||
|
int hops = clampDepth(root.getChildRecursionDepth());
|
||||||
|
State start = new State(root, terminal(root, byId, data) ? 0 : hops);
|
||||||
|
states.add(start);
|
||||||
|
ids.put(start, 0);
|
||||||
|
for (int cursor = 0; cursor < states.size(); cursor++) {
|
||||||
|
State state = states.get(cursor);
|
||||||
|
if (state.depth() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisRaritySelection<CarvingOption> selection = selection(state.entry(), byId, data);
|
||||||
|
if (selection.size() <= 1L) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Integer> nextByIndex = new ArrayList<>((int) selection.size());
|
||||||
|
for (long index = 0; index < selection.size(); index++) {
|
||||||
|
IrisDimensionCarvingEntry chosen = selection.get(index).entry();
|
||||||
|
State next = chosen == state.entry() || !contains(chosen, y)
|
||||||
|
? new State(state.entry(), 0)
|
||||||
|
: new State(chosen, Math.min(state.depth() - 1, clampDepth(chosen.getChildRecursionDepth())));
|
||||||
|
Integer id = ids.get(next);
|
||||||
|
if (id == null) {
|
||||||
|
id = states.size();
|
||||||
|
ids.put(next, id);
|
||||||
|
states.add(next);
|
||||||
|
}
|
||||||
|
nextByIndex.add(id);
|
||||||
|
}
|
||||||
|
moves.put(cursor, new Move(state.entry(), List.copyOf(nextByIndex)));
|
||||||
|
}
|
||||||
|
return new Machine(List.copyOf(states), Map.copyOf(moves), hops);
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedField slot(String name, SelectorCompiler selectors, IrisData data, ToIntFunction<String> undergroundSlot) {
|
||||||
|
IndexedField current = IndexedField.constant(0);
|
||||||
|
for (int hop = 0; hop < hops; hop++) {
|
||||||
|
List<Integer> movers = new ArrayList<>();
|
||||||
|
for (int state : current.values()) {
|
||||||
|
if (moves.containsKey(state)) {
|
||||||
|
movers.add(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (movers.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Map<Integer, IndexedField> moved = new HashMap<>(movers.size() * 2);
|
||||||
|
for (int state : movers) {
|
||||||
|
Move move = moves.get(state);
|
||||||
|
IrisDimensionCarvingEntry entry = move.entry();
|
||||||
|
SelectorCompiler.Selector selector = selectors.scaled(
|
||||||
|
entry.getChildStyle(),
|
||||||
|
Math.max(MINIMUM_CHILD_SHRINK_FACTOR, entry.getChildShrinkFactor()),
|
||||||
|
CARVING_SALT + Objects.toString(entry.getId(), "").trim().hashCode());
|
||||||
|
moved.put(state, selector.partition(BiomePools.selectionCuts(move.nextByIndex().size()), move.nextByIndex()));
|
||||||
|
}
|
||||||
|
current = IndexedField.advance(current, moved).shared(name + "/hop" + (hop + 1));
|
||||||
|
}
|
||||||
|
return current.remap(state -> undergroundSlot.applyAsInt(states.get(state).entry().getRealBiome(data).getLoadKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean terminal(IrisDimensionCarvingEntry entry, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
|
||||||
|
return entry.getChildren() == null || entry.getChildren().isEmpty() || selection(entry, byId, data).size() <= 1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisRaritySelection<CarvingOption> selection(IrisDimensionCarvingEntry parent, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
|
||||||
|
List<CarvingOption> options = new ArrayList<>();
|
||||||
|
if (parent.getChildren() != null) {
|
||||||
|
for (String childId : parent.getChildren()) {
|
||||||
|
if (childId == null || childId.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisDimensionCarvingEntry child = byId.get(childId.trim());
|
||||||
|
if (child == null || !child.isEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisBiome childBiome = child.getRealBiome(data);
|
||||||
|
if (childBiome != null) {
|
||||||
|
options.add(new CarvingOption(child, Math.max(childBiome.getRarity(), 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.add(new CarvingOption(parent, Math.max(parent.getRealBiome(data).getRarity(), 1)));
|
||||||
|
return IrisRaritySelection.create(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+162
@@ -0,0 +1,162 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.function.IntToDoubleFunction;
|
||||||
|
import java.util.function.IntUnaryOperator;
|
||||||
|
|
||||||
|
record IndexedField(Field field, List<Integer> values) {
|
||||||
|
private static final Field UNUSED_CASE = new Field.Const(0.0);
|
||||||
|
|
||||||
|
IndexedField {
|
||||||
|
Objects.requireNonNull(field);
|
||||||
|
values = List.copyOf(new TreeSet<>(values));
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("An indexed field needs at least one value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static IndexedField constant(int value) {
|
||||||
|
return new IndexedField(new Field.Const(value), List.of(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static IndexedField partition(Field input, List<Double> thresholds, List<Integer> leaves) {
|
||||||
|
if (leaves.size() != thresholds.size() + 1) {
|
||||||
|
throw new IllegalArgumentException("Partition needs " + (thresholds.size() + 1) + " leaves, got " + leaves.size());
|
||||||
|
}
|
||||||
|
List<Double> kept = new ArrayList<>(thresholds.size());
|
||||||
|
List<Integer> keptLeaves = new ArrayList<>(leaves.size());
|
||||||
|
keptLeaves.add(leaves.getFirst());
|
||||||
|
for (int index = 0; index < thresholds.size(); index++) {
|
||||||
|
int next = leaves.get(index + 1);
|
||||||
|
if (next == keptLeaves.getLast()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
kept.add(thresholds.get(index));
|
||||||
|
keptLeaves.add(next);
|
||||||
|
}
|
||||||
|
if (keptLeaves.size() == 1) {
|
||||||
|
return constant(keptLeaves.getFirst());
|
||||||
|
}
|
||||||
|
List<Field> constants = new ArrayList<>(keptLeaves.size());
|
||||||
|
for (int leaf : keptLeaves) {
|
||||||
|
constants.add(new Field.Const(leaf));
|
||||||
|
}
|
||||||
|
return new IndexedField(new Field.Partition(input, kept, constants), keptLeaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IndexedField select(IndexedField index, List<IndexedField> cases) {
|
||||||
|
if (index.isConstant()) {
|
||||||
|
return cases.get(index.values().getFirst());
|
||||||
|
}
|
||||||
|
IndexedField first = cases.get(index.values().getFirst());
|
||||||
|
boolean uniform = true;
|
||||||
|
SortedSet<Integer> reachable = new TreeSet<>();
|
||||||
|
for (int value : index.values()) {
|
||||||
|
IndexedField selected = cases.get(value);
|
||||||
|
uniform &= selected.equals(first);
|
||||||
|
reachable.addAll(selected.values());
|
||||||
|
}
|
||||||
|
if (uniform) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
int count = index.values().getLast() + 1;
|
||||||
|
List<Field> muxCases = new ArrayList<>(count);
|
||||||
|
for (int value = 0; value < count; value++) {
|
||||||
|
muxCases.add(index.values().contains(value) ? cases.get(value).field() : UNUSED_CASE);
|
||||||
|
}
|
||||||
|
return new IndexedField(new Field.Mux(index.field(), muxCases), List.copyOf(reachable));
|
||||||
|
}
|
||||||
|
|
||||||
|
static IndexedField advance(IndexedField current, Map<Integer, IndexedField> moved) {
|
||||||
|
int last = current.values().getLast();
|
||||||
|
List<IndexedField> cases = new ArrayList<>(last + 1);
|
||||||
|
for (int value = 0; value <= last; value++) {
|
||||||
|
IndexedField next = moved.get(value);
|
||||||
|
cases.add(next == null ? constant(value) : next);
|
||||||
|
}
|
||||||
|
return select(current, cases);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IndexedField sum(List<IndexedField> terms) {
|
||||||
|
SortedSet<Integer> values = new TreeSet<>(List.of(0));
|
||||||
|
int offset = 0;
|
||||||
|
Field field = null;
|
||||||
|
for (IndexedField term : terms) {
|
||||||
|
SortedSet<Integer> combined = new TreeSet<>();
|
||||||
|
for (int value : values) {
|
||||||
|
for (int addend : term.values()) {
|
||||||
|
combined.add(value + addend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values = combined;
|
||||||
|
if (term.isConstant()) {
|
||||||
|
offset += term.values().getFirst();
|
||||||
|
} else {
|
||||||
|
field = field == null ? term.field() : new Field.Binary(Field.BinaryOp.ADD, field, term.field());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field == null) {
|
||||||
|
return constant(offset);
|
||||||
|
}
|
||||||
|
Field total = offset == 0 ? field : new Field.Binary(Field.BinaryOp.ADD, field, new Field.Const(offset));
|
||||||
|
return new IndexedField(total, List.copyOf(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Double> midpoints(List<Integer> sorted) {
|
||||||
|
List<Double> thresholds = new ArrayList<>(sorted.size() - 1);
|
||||||
|
for (int index = 0; index + 1 < sorted.size(); index++) {
|
||||||
|
thresholds.add((sorted.get(index) + sorted.get(index + 1)) / 2.0);
|
||||||
|
}
|
||||||
|
return thresholds;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isConstant() {
|
||||||
|
return values.size() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedField remap(IntUnaryOperator mapping) {
|
||||||
|
List<Integer> mapped = new ArrayList<>(values.size());
|
||||||
|
for (int value : values) {
|
||||||
|
mapped.add(mapping.applyAsInt(value));
|
||||||
|
}
|
||||||
|
if (isConstant()) {
|
||||||
|
return constant(mapped.getFirst());
|
||||||
|
}
|
||||||
|
return partition(field, midpoints(values), mapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
Field lookup(IntToDoubleFunction table) {
|
||||||
|
if (isConstant()) {
|
||||||
|
return new Field.Const(table.applyAsDouble(values.getFirst()));
|
||||||
|
}
|
||||||
|
List<Double> thresholds = midpoints(values);
|
||||||
|
List<Double> keptThresholds = new ArrayList<>(thresholds.size());
|
||||||
|
List<Field> leaves = new ArrayList<>(values.size());
|
||||||
|
double previous = table.applyAsDouble(values.getFirst());
|
||||||
|
leaves.add(new Field.Const(previous));
|
||||||
|
for (int index = 1; index < values.size(); index++) {
|
||||||
|
double next = table.applyAsDouble(values.get(index));
|
||||||
|
if (next == previous) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
keptThresholds.add(thresholds.get(index - 1));
|
||||||
|
leaves.add(new Field.Const(next));
|
||||||
|
previous = next;
|
||||||
|
}
|
||||||
|
if (leaves.size() == 1) {
|
||||||
|
return leaves.getFirst();
|
||||||
|
}
|
||||||
|
return new Field.Partition(field, keptThresholds, leaves);
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedField shared(String name) {
|
||||||
|
return isConstant() ? this : new IndexedField(new Field.Shared(name, field), values);
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.DataVersion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
import java.util.function.DoubleBinaryOperator;
|
||||||
|
|
||||||
|
public final class RarityCalibrator {
|
||||||
|
private static final int SAMPLES = 8192;
|
||||||
|
private static final double WINDOW_FEATURES = 96.0;
|
||||||
|
private static final double MAX_WINDOW_BLOCKS = 4_000_000.0;
|
||||||
|
private static final long POSITION_SEED = 0x5EED_B10EL;
|
||||||
|
private static final long WORLD_SEED = 0x1215_B10EL;
|
||||||
|
|
||||||
|
private final NoiseRegistry registry;
|
||||||
|
private final DataVersion target;
|
||||||
|
|
||||||
|
public RarityCalibrator(NoiseRegistry registry, DataVersion target) {
|
||||||
|
this.registry = Objects.requireNonNull(registry);
|
||||||
|
this.target = Objects.requireNonNull(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Calibration calibrate(DoubleBinaryOperator iris, Field compiled, double featureBlocks, long salt) {
|
||||||
|
Objects.requireNonNull(iris);
|
||||||
|
Objects.requireNonNull(compiled);
|
||||||
|
double window = Math.min(MAX_WINDOW_BLOCKS, Math.max(1.0, featureBlocks) * WINDOW_FEATURES);
|
||||||
|
Map<String, NoiseDef> noises = noiseDefinitions();
|
||||||
|
SplittableRandom positions = new SplittableRandom(POSITION_SEED ^ salt);
|
||||||
|
double[] irisSamples = new double[SAMPLES];
|
||||||
|
double[] compiledSamples = new double[SAMPLES];
|
||||||
|
for (int index = 0; index < SAMPLES; index++) {
|
||||||
|
int x = (int) Math.floor((positions.nextDouble() - 0.5) * window);
|
||||||
|
int z = (int) Math.floor((positions.nextDouble() - 0.5) * window);
|
||||||
|
irisSamples[index] = iris.applyAsDouble(x, z);
|
||||||
|
compiledSamples[index] = compiled instanceof Field.Const constant
|
||||||
|
? constant.value()
|
||||||
|
: FieldEvaluator.sample(compiled, noises, WORLD_SEED, target, x, 0, z);
|
||||||
|
}
|
||||||
|
Arrays.sort(irisSamples);
|
||||||
|
Arrays.sort(compiledSamples);
|
||||||
|
return new Calibration(irisSamples, compiledSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, NoiseDef> noiseDefinitions() {
|
||||||
|
List<NoiseDef> definitions = registry.definitions();
|
||||||
|
Map<String, NoiseDef> noises = new HashMap<>(definitions.size() * 2);
|
||||||
|
for (NoiseDef definition : definitions) {
|
||||||
|
noises.put(registry.namespace() + ":" + definition.id(), definition);
|
||||||
|
}
|
||||||
|
return noises;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Calibration {
|
||||||
|
private final double[] iris;
|
||||||
|
private final double[] compiled;
|
||||||
|
|
||||||
|
private Calibration(double[] iris, double[] compiled) {
|
||||||
|
this.iris = iris;
|
||||||
|
this.compiled = compiled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double cumulative(double irisValue) {
|
||||||
|
int low = 0;
|
||||||
|
int high = iris.length;
|
||||||
|
while (low < high) {
|
||||||
|
int middle = (low + high) >>> 1;
|
||||||
|
if (iris[middle] < irisValue) {
|
||||||
|
low = middle + 1;
|
||||||
|
} else {
|
||||||
|
high = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return low / (double) iris.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double threshold(double probability) {
|
||||||
|
return quantile(compiled, probability);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double irisQuantile(double probability) {
|
||||||
|
return quantile(iris, probability);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Double> thresholds(List<Double> irisCuts) {
|
||||||
|
List<Double> thresholds = new ArrayList<>(irisCuts.size());
|
||||||
|
for (double cut : irisCuts) {
|
||||||
|
thresholds.add(threshold(cumulative(cut)));
|
||||||
|
}
|
||||||
|
return thresholds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double quantile(double[] sorted, double probability) {
|
||||||
|
double position = Math.clamp(probability, 0.0, 1.0) * sorted.length - 0.5;
|
||||||
|
if (position <= 0.0) {
|
||||||
|
return sorted[0];
|
||||||
|
}
|
||||||
|
if (position >= sorted.length - 1) {
|
||||||
|
return sorted[sorted.length - 1];
|
||||||
|
}
|
||||||
|
int lower = (int) Math.floor(position);
|
||||||
|
double fraction = position - lower;
|
||||||
|
return sorted[lower] + fraction * (sorted[lower + 1] - sorted[lower]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.biome;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import art.arcane.volmlib.util.noise.CNG;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.DoubleBinaryOperator;
|
||||||
|
|
||||||
|
public final class SelectorCompiler {
|
||||||
|
private static final double COHERENT_BLOCKS = 64.0;
|
||||||
|
private static final long CALIBRATION_SEED = 0x1215_5E1EL;
|
||||||
|
private static final int CURVE_KNOTS = 9;
|
||||||
|
private static final String SHARED_PREFIX = "biome/selector/";
|
||||||
|
|
||||||
|
private final StyleCompiler styles;
|
||||||
|
private final IrisData data;
|
||||||
|
private final NoiseRegistry registry;
|
||||||
|
private final ExportCapabilities capabilities;
|
||||||
|
private final RarityCalibrator calibrator;
|
||||||
|
private final Map<SelectorKey, Selector> selectors = new HashMap<>();
|
||||||
|
private int shared;
|
||||||
|
|
||||||
|
public SelectorCompiler(StyleCompiler styles, Sources sources) {
|
||||||
|
this.styles = Objects.requireNonNull(styles);
|
||||||
|
Objects.requireNonNull(sources);
|
||||||
|
data = sources.data();
|
||||||
|
registry = sources.registry();
|
||||||
|
capabilities = sources.capabilities();
|
||||||
|
calibrator = new RarityCalibrator(registry, capabilities.target());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Selector zoomed(IrisGeneratorStyle style, double zoom, long salt) {
|
||||||
|
return selectors.computeIfAbsent(new SelectorKey(style, false, zoom, salt), this::create);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Selector scaled(IrisGeneratorStyle style, double scale, long salt) {
|
||||||
|
return selectors.computeIfAbsent(new SelectorKey(style, true, scale, salt), this::create);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Selector sampled(IrisGeneratorStyle style, double zoom, long salt, DoubleBinaryOperator iris) {
|
||||||
|
IrisGeneratorStyle exported = exportable(style);
|
||||||
|
exported.setZoom(exported.getZoom() * zoom);
|
||||||
|
return compile(exported, salt, iris, COHERENT_BLOCKS * exported.getZoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Selector create(SelectorKey key) {
|
||||||
|
IrisGeneratorStyle exported = exportable(key.style());
|
||||||
|
RNG seed = new RNG(CALIBRATION_SEED).nextParallelRNG(key.salt());
|
||||||
|
if (key.scaled()) {
|
||||||
|
CNG iris = exported.createScaledGenerator(seed, data, key.factor());
|
||||||
|
IrisGeneratorStyle compiled = scaledCopy(exported, key.factor());
|
||||||
|
return compile(compiled, key.salt(), iris::noiseFast2D, COHERENT_BLOCKS * compiled.getZoom());
|
||||||
|
}
|
||||||
|
CNG iris = exported.create(seed, data);
|
||||||
|
double zoom = key.factor();
|
||||||
|
IrisGeneratorStyle compiled = copy(exported);
|
||||||
|
compiled.setZoom(exported.getZoom() * zoom);
|
||||||
|
return compile(compiled, key.salt(), (x, z) -> iris.noise(x / zoom, z / zoom), COHERENT_BLOCKS * compiled.getZoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Selector compile(IrisGeneratorStyle style, long salt, DoubleBinaryOperator iris, double featureBlocks) {
|
||||||
|
Field field = styles.compile(style, salt, registry, capabilities);
|
||||||
|
FieldValidator.requireTwoDimensional(field, "biome selector");
|
||||||
|
RarityCalibrator.Calibration calibration = calibrator.calibrate(iris, field, featureBlocks, salt);
|
||||||
|
Field selector = field instanceof Field.Const ? field : new Field.Shared(SHARED_PREFIX + shared++, field);
|
||||||
|
return new Selector(selector, calibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisGeneratorStyle exportable(IrisGeneratorStyle style) {
|
||||||
|
IrisGeneratorStyle copy = copy(style);
|
||||||
|
for (IrisGeneratorStyle current = copy; current != null; current = current.getFracture()) {
|
||||||
|
current.setImageMap(null);
|
||||||
|
current.setCacheSize(0);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisGeneratorStyle scaledCopy(IrisGeneratorStyle style, double scale) {
|
||||||
|
IrisGeneratorStyle copy = copy(style);
|
||||||
|
copy.setZoom(style.getZoom() / scale);
|
||||||
|
IrisGeneratorStyle fracture = copy.getFracture();
|
||||||
|
if (fracture != null) {
|
||||||
|
fracture.setZoom(fracture.getZoom() * scale);
|
||||||
|
fracture.setMultiplier(fracture.getMultiplier() * scale);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisGeneratorStyle copy(IrisGeneratorStyle style) {
|
||||||
|
IrisGeneratorStyle copy = new IrisGeneratorStyle(style.getStyle());
|
||||||
|
copy.setCellularFrequency(style.getCellularFrequency());
|
||||||
|
copy.setCellularZoom(style.getCellularZoom());
|
||||||
|
copy.setZoom(style.getZoom());
|
||||||
|
copy.setExpression(style.getExpression());
|
||||||
|
copy.setImageMap(style.getImageMap());
|
||||||
|
copy.setMultiplier(style.getMultiplier());
|
||||||
|
copy.setExponent(style.getExponent());
|
||||||
|
copy.setCacheSize(style.getCacheSize());
|
||||||
|
copy.setFracture(style.getFracture() == null ? null : copy(style.getFracture()));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Sources(IrisData data, NoiseRegistry registry, ExportCapabilities capabilities) {
|
||||||
|
public Sources {
|
||||||
|
Objects.requireNonNull(data);
|
||||||
|
Objects.requireNonNull(registry);
|
||||||
|
Objects.requireNonNull(capabilities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Selector(Field field, RarityCalibrator.Calibration calibration) {
|
||||||
|
public Selector {
|
||||||
|
Objects.requireNonNull(field);
|
||||||
|
Objects.requireNonNull(calibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedField partition(List<Double> irisCuts, List<Integer> leaves) {
|
||||||
|
if (leaves.size() != irisCuts.size() + 1) {
|
||||||
|
throw new IllegalArgumentException("Selector partition needs " + (irisCuts.size() + 1) + " leaves, got " + leaves.size());
|
||||||
|
}
|
||||||
|
List<Double> upper = new ArrayList<>(leaves.size());
|
||||||
|
List<Integer> kept = new ArrayList<>(leaves.size());
|
||||||
|
double lower = 0.0;
|
||||||
|
for (int index = 0; index < leaves.size(); index++) {
|
||||||
|
double bound = index == irisCuts.size() ? 1.0 : Math.max(lower, calibration.cumulative(irisCuts.get(index)));
|
||||||
|
if (bound > lower) {
|
||||||
|
upper.add(bound);
|
||||||
|
kept.add(leaves.get(index));
|
||||||
|
}
|
||||||
|
lower = bound;
|
||||||
|
}
|
||||||
|
if (kept.isEmpty()) {
|
||||||
|
return IndexedField.constant(leaves.getLast());
|
||||||
|
}
|
||||||
|
List<Double> thresholds = new ArrayList<>(kept.size() - 1);
|
||||||
|
for (int index = 0; index + 1 < kept.size(); index++) {
|
||||||
|
thresholds.add(calibration.threshold(upper.get(index)));
|
||||||
|
}
|
||||||
|
return IndexedField.partition(field, thresholds, kept);
|
||||||
|
}
|
||||||
|
|
||||||
|
Field curve() {
|
||||||
|
List<Field.Knot> knots = new ArrayList<>(CURVE_KNOTS);
|
||||||
|
double previousLocation = Double.NEGATIVE_INFINITY;
|
||||||
|
List<double[]> points = new ArrayList<>(CURVE_KNOTS);
|
||||||
|
for (int index = 0; index < CURVE_KNOTS; index++) {
|
||||||
|
double probability = (index + 0.5) / CURVE_KNOTS;
|
||||||
|
double location = calibration.threshold(probability);
|
||||||
|
if (location <= previousLocation) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
points.add(new double[]{location, calibration.irisQuantile(probability)});
|
||||||
|
previousLocation = location;
|
||||||
|
}
|
||||||
|
if (points.size() == 1) {
|
||||||
|
return new Field.Const(points.getFirst()[1]);
|
||||||
|
}
|
||||||
|
for (int index = 0; index < points.size(); index++) {
|
||||||
|
double derivative = 0.0;
|
||||||
|
if (index > 0 && index + 1 < points.size()) {
|
||||||
|
double[] before = points.get(index - 1);
|
||||||
|
double[] after = points.get(index + 1);
|
||||||
|
derivative = (after[1] - before[1]) / (after[0] - before[0]);
|
||||||
|
}
|
||||||
|
knots.add(new Field.Knot(points.get(index)[0], new Field.Const(points.get(index)[1]), derivative));
|
||||||
|
}
|
||||||
|
return new Field.Curve(field, knots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SelectorKey(IrisGeneratorStyle style, boolean scaled, double factor, long salt) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public final class AquiferCompiler {
|
||||||
|
private static final int LAVA_LIMIT_Y = -10;
|
||||||
|
private static final int LAVA_CENTER_MARGIN = 12;
|
||||||
|
private static final int FLUID_BAND_HEIGHT = 40;
|
||||||
|
private static final int FLUID_BAND_CENTER = 20;
|
||||||
|
private static final double SPREAD_LEVEL_SCALE = 10.0;
|
||||||
|
private static final double LEVEL_ROUNDING_MARGIN = 0.5;
|
||||||
|
private static final Field DRY = new Field.Const(-1.0);
|
||||||
|
private static final Field PARTIAL = new Field.Const(0.6);
|
||||||
|
private static final Field LOW_BARRIER = new Field.Const(-1.0);
|
||||||
|
private static final Field NO_EXCLUSION = new Field.Const(-1.0);
|
||||||
|
private static final Field LAVA = new Field.Const(1.0);
|
||||||
|
private static final Field NO_LAVA = new Field.Const(0.0);
|
||||||
|
private static final Field OUTSIDE_BAND = new Field.Const(0.0);
|
||||||
|
private static final Field INSIDE_BAND = new Field.Const(1.0);
|
||||||
|
|
||||||
|
private final CaveInputs inputs;
|
||||||
|
private final Field blockY;
|
||||||
|
private final int lowFluidBand;
|
||||||
|
private final int highFluidBand;
|
||||||
|
|
||||||
|
public AquiferCompiler(CaveInputs inputs) {
|
||||||
|
this.inputs = Objects.requireNonNull(inputs);
|
||||||
|
this.blockY = inputs.blockY();
|
||||||
|
this.lowFluidBand = Math.floorDiv(inputs.minY(), FLUID_BAND_HEIGHT) - 1;
|
||||||
|
this.highFluidBand = Math.floorDiv(inputs.maxY(), FLUID_BAND_HEIGHT) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AquiferDef compile(CaveSelection selection) {
|
||||||
|
AquiferDef dry = AquiferDef.dry(inputs.aquiferSurface());
|
||||||
|
boolean lavaWanted = any(selection, IrisCaveProfile::isAllowLava);
|
||||||
|
boolean lavaExported = lavaWanted && inputs.lavaY() <= LAVA_LIMIT_Y;
|
||||||
|
if (lavaWanted && !lavaExported) {
|
||||||
|
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.LAVA_HEIGHT, inputs.resource(),
|
||||||
|
"Cave lava height Y " + inputs.lavaY() + " is above Y " + LAVA_LIMIT_Y + "; caves export without lava"));
|
||||||
|
}
|
||||||
|
boolean wet = any(selection, IrisCaveProfile::isAllowFluid);
|
||||||
|
if (!lavaExported && !wet) {
|
||||||
|
return dry;
|
||||||
|
}
|
||||||
|
Field spread = lavaExported ? lavaSpread(wet, dry.spread()) : dry.spread();
|
||||||
|
Field lava = lavaExported ? lavaBands() : NO_LAVA;
|
||||||
|
return new AquiferDef(LOW_BARRIER, floodedness(selection, lavaExported), spread, lava, NO_EXCLUSION, inputs.aquiferSurface());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field floodedness(CaveSelection selection, boolean lavaExported) {
|
||||||
|
Field flood = selection.profiles().isEmpty() ? DRY : selection.select(source -> flood(source, lavaExported), DRY);
|
||||||
|
for (CaveSource band : selection.bands()) {
|
||||||
|
Field banded = flood(band, lavaExported);
|
||||||
|
if (banded.equals(flood)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Field inside = new Field.Partition(blockY, List.of((double) band.minY(), band.maxY() + 1.0), List.of(OUTSIDE_BAND, INSIDE_BAND, OUTSIDE_BAND));
|
||||||
|
flood = new Field.Mux(inside, List.of(flood, banded));
|
||||||
|
}
|
||||||
|
return flood;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field flood(CaveSource source, boolean lavaExported) {
|
||||||
|
IrisCaveProfile profile = source.profile();
|
||||||
|
int top = Integer.MIN_VALUE;
|
||||||
|
if (lavaExported && profile.isAllowLava()) {
|
||||||
|
top = inputs.lavaY() + LAVA_CENTER_MARGIN;
|
||||||
|
}
|
||||||
|
if (profile.isAllowFluid()) {
|
||||||
|
top = Math.max(top, inputs.fluidHeight());
|
||||||
|
}
|
||||||
|
if (top < inputs.minY()) {
|
||||||
|
return DRY;
|
||||||
|
}
|
||||||
|
if (top >= inputs.maxY()) {
|
||||||
|
return PARTIAL;
|
||||||
|
}
|
||||||
|
return new Field.RangeIf(blockY, inputs.minY(), top + 1.0, PARTIAL, DRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field lavaSpread(boolean wet, Field vanillaSpread) {
|
||||||
|
Field levels = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, lowFluidBand, highFluidBand, spreadAt(lowFluidBand), spreadAt(highFluidBand));
|
||||||
|
return wet ? new Field.RangeIf(fluidBand(), lowFluidBand, lavaBand() + 1.0, levels, vanillaSpread) : levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field lavaBands() {
|
||||||
|
return new Field.RangeIf(fluidBand(), lowFluidBand, lavaBand() + 1.0, LAVA, NO_LAVA);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field fluidBand() {
|
||||||
|
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, lowFluidBand, highFluidBand, lowFluidBand, highFluidBand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lavaBand() {
|
||||||
|
return Math.floorDiv(inputs.lavaY() + LAVA_CENTER_MARGIN, FLUID_BAND_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double spreadAt(int band) {
|
||||||
|
return (inputs.lavaY() - FLUID_BAND_CENTER + LEVEL_ROUNDING_MARGIN) / SPREAD_LEVEL_SCALE - band * (FLUID_BAND_HEIGHT / SPREAD_LEVEL_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean any(CaveSelection selection, Predicate<IrisCaveProfile> property) {
|
||||||
|
for (CaveSource source : selection.sources()) {
|
||||||
|
if (property.test(source.profile())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class CaveFieldCompiler {
|
||||||
|
static final int CELL_XZ = 4;
|
||||||
|
static final int CELL_Y = 4;
|
||||||
|
static final double WALL_BAND = 0.03125;
|
||||||
|
private static final double MIN_GAIN = 1.0 / 64.0;
|
||||||
|
private static final double MAX_GAIN = 64.0;
|
||||||
|
private static final Field UNCOVERED = new Field.Const(CaveProfileCompiler.SOLID);
|
||||||
|
|
||||||
|
private final CaveProfileCompiler profiles;
|
||||||
|
private final CaveInputs inputs;
|
||||||
|
|
||||||
|
public CaveFieldCompiler(StyleCompiler styles, CaveInputs inputs) {
|
||||||
|
this.profiles = new CaveProfileCompiler(styles, inputs);
|
||||||
|
this.inputs = inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Field> compile(CaveSelection selection) {
|
||||||
|
return union(selection).map(union -> new Field.Grid(union, CELL_XZ, CELL_Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Field> union(CaveSelection selection) {
|
||||||
|
List<Field> parts = new ArrayList<>(selection.bands().size() + 1);
|
||||||
|
if (!selection.profiles().isEmpty()) {
|
||||||
|
parts.add(selection.select(this::normalized, UNCOVERED));
|
||||||
|
}
|
||||||
|
for (CaveSource band : selection.bands()) {
|
||||||
|
parts.add(normalized(band));
|
||||||
|
}
|
||||||
|
if (parts.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Field union = parts.getFirst();
|
||||||
|
for (int index = 1; index < parts.size(); index++) {
|
||||||
|
union = new Field.Binary(Field.BinaryOp.MIN, union, parts.get(index));
|
||||||
|
}
|
||||||
|
return Optional.of(union);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field normalized(CaveSource source) {
|
||||||
|
return profiles.compile(source, gain(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double gain(CaveSource source) {
|
||||||
|
Optional<CaveInterior> interior = profiles.interior(source);
|
||||||
|
if (interior.isEmpty()) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
double band = WallShellCalibrator.band(interior.get(), inputs.noises().byReference(), inputs.capabilities().target());
|
||||||
|
return band > 0.0 ? Math.clamp(WALL_BAND / band, MIN_GAIN, MAX_GAIN) : 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record CaveInputs(
|
||||||
|
String resource,
|
||||||
|
int minY,
|
||||||
|
int maxY,
|
||||||
|
int fluidHeight,
|
||||||
|
int lavaY,
|
||||||
|
Field surface,
|
||||||
|
Field aquiferSurface,
|
||||||
|
NoiseRegistry noises,
|
||||||
|
ExportCapabilities capabilities,
|
||||||
|
List<ExportDiagnostic> diagnostics
|
||||||
|
) {
|
||||||
|
public CaveInputs {
|
||||||
|
Objects.requireNonNull(resource);
|
||||||
|
Objects.requireNonNull(surface);
|
||||||
|
Objects.requireNonNull(aquiferSurface);
|
||||||
|
Objects.requireNonNull(noises);
|
||||||
|
Objects.requireNonNull(capabilities);
|
||||||
|
Objects.requireNonNull(diagnostics);
|
||||||
|
if (maxY <= minY) {
|
||||||
|
throw new IllegalArgumentException("Cave bounds need maxY above minY, got " + minY + ".." + maxY);
|
||||||
|
}
|
||||||
|
FieldValidator.requireTwoDimensional(surface, "cave surface");
|
||||||
|
FieldValidator.requireTwoDimensional(aquiferSurface, "aquifer surface");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CaveInputs of(PackCompileContext context) {
|
||||||
|
IrisDimension dimension = context.dimension();
|
||||||
|
int minY = dimension.getMinHeight();
|
||||||
|
return new CaveInputs(
|
||||||
|
"dimensions/" + dimension.getLoadKey(),
|
||||||
|
minY,
|
||||||
|
dimension.getMaxHeight() - 1,
|
||||||
|
context.dimensionDef().fluidHeight(),
|
||||||
|
minY + dimension.getCaveLavaHeight(),
|
||||||
|
context.height(),
|
||||||
|
context.surfaceEstimate(),
|
||||||
|
context.noises(),
|
||||||
|
context.capabilities(),
|
||||||
|
context.diagnostics());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field blockY() {
|
||||||
|
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, minY, maxY + 1, minY, maxY + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CaveInterior(Field field, int minY, int maxY) {
|
||||||
|
CaveInterior {
|
||||||
|
Objects.requireNonNull(field);
|
||||||
|
if (maxY < minY) {
|
||||||
|
throw new IllegalArgumentException("Cave interior needs maxY at or above minY, got " + minY + ".." + maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
|
||||||
|
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||||
|
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||||
|
import art.arcane.iris.generation.noise.IrisStyledRange;
|
||||||
|
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Axis;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class CaveProfileCompiler {
|
||||||
|
static final double SOLID = 1.0;
|
||||||
|
private static final long BASE_SALT = 934_447L;
|
||||||
|
private static final long DETAIL_SALT = 612_991L;
|
||||||
|
private static final long THRESHOLD_SALT = 489_112L;
|
||||||
|
private static final long SURFACE_BREAK_SALT = 341_219L;
|
||||||
|
private static final long MODULE_SALT = 1_000_003L;
|
||||||
|
private static final long MODULE_SALT_STEP = 65_537L;
|
||||||
|
private static final double LOW_WEIGHT = 0.1;
|
||||||
|
private static final int CEILING_FADE_DEPTH = 12;
|
||||||
|
private static final double CLOSURE_EPSILON = 1.0e-6;
|
||||||
|
private static final double SEALED_MARGIN = 1.0;
|
||||||
|
private static final Set<NoiseStyle> CELLULAR_STYLES = cellularStyles();
|
||||||
|
|
||||||
|
private final StyleCompiler styles;
|
||||||
|
private final CaveInputs inputs;
|
||||||
|
private final Field blockY;
|
||||||
|
|
||||||
|
public CaveProfileCompiler(StyleCompiler styles, CaveInputs inputs) {
|
||||||
|
this.styles = Objects.requireNonNull(styles);
|
||||||
|
this.inputs = Objects.requireNonNull(inputs);
|
||||||
|
this.blockY = inputs.blockY();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field compile(CaveSource source, double gain) {
|
||||||
|
if (!(gain > 0.0) || Double.isInfinite(gain)) {
|
||||||
|
throw new IllegalArgumentException("Cave field gain must be finite and positive, got " + gain);
|
||||||
|
}
|
||||||
|
IrisCaveProfile profile = source.profile();
|
||||||
|
IrisRange range = profile.getVerticalRange();
|
||||||
|
int floor = floor(inputs, source);
|
||||||
|
int ceiling = Math.min(source.maxY(), Math.min(inputs.maxY(), inputs.minY() + (int) Math.ceil(range.getMax())));
|
||||||
|
if (ceiling < floor) {
|
||||||
|
return new Field.Const(SOLID);
|
||||||
|
}
|
||||||
|
double normalization = normalization(profile) / gain;
|
||||||
|
List<Field> terms = densityTerms(source, normalization, floor, ceiling);
|
||||||
|
edgeFade(profile, floor, ceiling, gain).ifPresent(terms::add);
|
||||||
|
terms.add(surface(source, closure(profile, normalization), gain));
|
||||||
|
return bounded(sum(terms), floor, ceiling);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<CaveInterior> interior(CaveSource source) {
|
||||||
|
IrisCaveProfile profile = source.profile();
|
||||||
|
int floor = floor(inputs, source);
|
||||||
|
int ceiling = Math.min(source.maxY(), Math.min(inputs.maxY(), inputs.minY() + (int) Math.ceil(profile.getVerticalRange().getMax())));
|
||||||
|
if (ceiling < floor) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
List<Field> terms = densityTerms(source, normalization(profile), floor, ceiling);
|
||||||
|
terms.add(negativeThreshold(profile, 1.0));
|
||||||
|
return Optional.of(new CaveInterior(sum(terms), floor, ceiling));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int floor(CaveInputs inputs, CaveSource source) {
|
||||||
|
return Math.max(source.minY(), inputs.minY() + Math.max(0, (int) Math.floor(source.profile().getVerticalRange().getMin())));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean cellular(IrisGeneratorStyle style) {
|
||||||
|
if (style.getExpression() != null || style.getImageMap() != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (style.getCellularFrequency() > 0.0 || CELLULAR_STYLES.contains(style.getStyle())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return style.getFracture() != null && cellular(style.getFracture());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Field> densityTerms(CaveSource source, double normalization, int floor, int ceiling) {
|
||||||
|
IrisCaveProfile profile = source.profile();
|
||||||
|
List<Field> terms = new ArrayList<>();
|
||||||
|
addTerm(terms, source, new Term(profile.getBaseDensityStyle(), BASE_SALT, profile.getBaseWeight(), 0.0, false), normalization);
|
||||||
|
addTerm(terms, source, new Term(profile.getDetailDensityStyle(), DETAIL_SALT, profile.getDetailWeight(), 0.0, false), normalization);
|
||||||
|
List<IrisCaveFieldModule> modules = profile.getModules();
|
||||||
|
for (int index = 0; index < modules.size(); index++) {
|
||||||
|
IrisCaveFieldModule module = modules.get(index);
|
||||||
|
IrisRange range = module.getVerticalRange();
|
||||||
|
int low = inputs.minY() + (int) Math.floor(range.getMin());
|
||||||
|
int high = inputs.minY() + (int) Math.ceil(range.getMax());
|
||||||
|
if (module.getWeight() == 0.0 || high < floor || low > ceiling) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Field term = term(source, new Term(module.getStyle(), MODULE_SALT + index * MODULE_SALT_STEP, module.getWeight(), module.getThreshold(), module.isInvert()), normalization);
|
||||||
|
terms.add(low <= floor && high >= ceiling ? term : new Field.Gate(window(low, high), term));
|
||||||
|
}
|
||||||
|
return terms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTerm(List<Field> terms, CaveSource source, Term term, double normalization) {
|
||||||
|
if (term.weight() != 0.0) {
|
||||||
|
terms.add(term(source, term, normalization));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field term(CaveSource source, Term term, double normalization) {
|
||||||
|
double sign = term.invert() ? -1.0 : 1.0;
|
||||||
|
double scale = sign * term.weight() / normalization;
|
||||||
|
Field unit = volume(source, term.style(), term.salt(), Math.abs(term.weight()) < LOW_WEIGHT);
|
||||||
|
return affine(unit, 2.0 * scale, -scale * (1.0 + term.threshold()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field volume(CaveSource source, IrisGeneratorStyle style, long salt, boolean lowWeight) {
|
||||||
|
boolean cellularStyle = cellular(style);
|
||||||
|
if (cellularStyle && lowWeight) {
|
||||||
|
return VolumeLift.lift(styles.compile(substitute(style), salt, inputs.noises(), inputs.capabilities()));
|
||||||
|
}
|
||||||
|
Field compiled = styles.compile(style, salt, inputs.noises(), inputs.capabilities());
|
||||||
|
if (compiled.axes().contains(Axis.Y)) {
|
||||||
|
return compiled;
|
||||||
|
}
|
||||||
|
if (!cellularStyle) {
|
||||||
|
return VolumeLift.lift(compiled);
|
||||||
|
}
|
||||||
|
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.NOISE_STYLE, source.resource(),
|
||||||
|
"Cellular cave style " + style.getStyle() + " exports as smooth noise"));
|
||||||
|
return VolumeLift.lift(styles.compile(substitute(style), salt, inputs.noises(), inputs.capabilities()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field planar(IrisGeneratorStyle style, long salt) {
|
||||||
|
Field compiled = styles.compile(style, salt, inputs.noises(), inputs.capabilities());
|
||||||
|
return compiled.axes().contains(Axis.Y) ? new Field.Slice(compiled, Axis.Y, 0) : compiled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field surface(CaveSource source, double closure, double gain) {
|
||||||
|
IrisCaveProfile profile = source.profile();
|
||||||
|
Field negativeThreshold = negativeThreshold(profile, gain);
|
||||||
|
Field depthBelowClearance = new Field.Binary(Field.BinaryOp.SUB,
|
||||||
|
new Field.Binary(Field.BinaryOp.ADD, inputs.surface(), new Field.Const(-Math.max(0, profile.getSurfaceClearance()))),
|
||||||
|
blockY);
|
||||||
|
Field sealed = new Field.Curve(depthBelowClearance, List.of(
|
||||||
|
new Field.Knot(-1.0, new Field.Const(SEALED_MARGIN * gain - closure), 0.0),
|
||||||
|
new Field.Knot(0.0, new Field.Const(-closure), 0.0),
|
||||||
|
new Field.Knot(CEILING_FADE_DEPTH, negativeThreshold, 0.0)));
|
||||||
|
if (!profile.isAllowSurfaceBreak()) {
|
||||||
|
return sealed;
|
||||||
|
}
|
||||||
|
Field breaking = new Field.Binary(Field.BinaryOp.ADD, breakSteps(profile, closure, gain), negativeThreshold);
|
||||||
|
Field breakNoise = planar(profile.getSurfaceBreakStyle(), SURFACE_BREAK_SALT);
|
||||||
|
Field cutoff = new Field.Const((profile.getSurfaceBreakNoiseThreshold() + 1.0) / 2.0);
|
||||||
|
return new Field.Binary(Field.BinaryOp.ADD,
|
||||||
|
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, cutoff, breakNoise), sealed),
|
||||||
|
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, breakNoise, cutoff), breaking));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field breakSteps(IrisCaveProfile profile, double closure, double gain) {
|
||||||
|
int depth = Math.max(0, profile.getSurfaceBreakDepth());
|
||||||
|
double boost = Math.max(0.0, profile.getSurfaceBreakThresholdBoost()) * gain;
|
||||||
|
Field belowSurface = new Field.Binary(Field.BinaryOp.SUB, inputs.surface(), blockY);
|
||||||
|
List<Field.Knot> knots = new ArrayList<>();
|
||||||
|
knots.add(new Field.Knot(-1.0, new Field.Const((thresholdCeiling(profile) + SEALED_MARGIN) * gain - closure + boost), 0.0));
|
||||||
|
knots.add(new Field.Knot(0.0, new Field.Const(-boost), 0.0));
|
||||||
|
if (boost > 0.0) {
|
||||||
|
if (depth > 0) {
|
||||||
|
knots.add(new Field.Knot(depth, new Field.Const(-boost), 0.0));
|
||||||
|
}
|
||||||
|
knots.add(new Field.Knot(depth + 1.0, new Field.Const(0.0), 0.0));
|
||||||
|
}
|
||||||
|
return new Field.Curve(belowSurface, knots);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field negativeThreshold(IrisCaveProfile profile, double gain) {
|
||||||
|
IrisStyledRange range = profile.getDensityThreshold();
|
||||||
|
double bias = profile.getThresholdBias();
|
||||||
|
if (range.getMin() == range.getMax()) {
|
||||||
|
return new Field.Const(gain * (bias - range.getMin()));
|
||||||
|
}
|
||||||
|
if (range.getStyle().isFlat()) {
|
||||||
|
return new Field.Const(gain * (bias - (range.getMin() + 0.5 * (range.getMax() - range.getMin()))));
|
||||||
|
}
|
||||||
|
return affine(planar(range.getStyle(), THRESHOLD_SALT), gain * (range.getMin() - range.getMax()), gain * (bias - range.getMin()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Field> edgeFade(IrisCaveProfile profile, int floor, int ceiling, double gain) {
|
||||||
|
int fade = Math.max(0, profile.getVerticalEdgeFade());
|
||||||
|
double strength = Math.max(0.0, profile.getVerticalEdgeFadeStrength()) * gain;
|
||||||
|
if (fade <= 0 || strength <= 0.0 || ceiling <= floor) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
List<Field.Knot> knots = new ArrayList<>();
|
||||||
|
knots.add(new Field.Knot(floor, new Field.Const(strength), 0.0));
|
||||||
|
if (floor + fade <= ceiling - fade) {
|
||||||
|
knots.add(new Field.Knot(floor + fade, new Field.Const(0.0), 0.0));
|
||||||
|
if (ceiling - fade > floor + fade) {
|
||||||
|
knots.add(new Field.Knot(ceiling - fade, new Field.Const(0.0), 0.0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double middle = floor + (ceiling - floor) / 2.0;
|
||||||
|
double progress = Math.min(middle - floor, ceiling - middle) / fade;
|
||||||
|
knots.add(new Field.Knot(middle, new Field.Const((1.0 - smoothstep(progress)) * strength), 0.0));
|
||||||
|
}
|
||||||
|
knots.add(new Field.Knot(ceiling, new Field.Const(strength), 0.0));
|
||||||
|
return Optional.of(new Field.Curve(blockY, knots));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field bounded(Field core, int floor, int ceiling) {
|
||||||
|
if (floor <= inputs.minY() && ceiling >= inputs.maxY()) {
|
||||||
|
return core;
|
||||||
|
}
|
||||||
|
Field shifted = new Field.Binary(Field.BinaryOp.ADD, core, new Field.Const(-SOLID));
|
||||||
|
return new Field.Binary(Field.BinaryOp.ADD, new Field.Gate(window(floor, ceiling), shifted), new Field.Const(SOLID));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field window(int low, int high) {
|
||||||
|
return new Field.RangeIf(blockY, low, high + 1.0, new Field.Const(1.0), new Field.Const(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field affine(Field input, double scale, double offset) {
|
||||||
|
if (input instanceof Field.Const constant) {
|
||||||
|
return new Field.Const(scale * constant.value() + offset);
|
||||||
|
}
|
||||||
|
if (scale == 0.0) {
|
||||||
|
return new Field.Const(offset);
|
||||||
|
}
|
||||||
|
return new Field.Curve(input, List.of(
|
||||||
|
new Field.Knot(0.0, new Field.Const(offset), scale),
|
||||||
|
new Field.Knot(1.0, new Field.Const(scale + offset), scale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Field sum(List<Field> terms) {
|
||||||
|
Field total = terms.getFirst();
|
||||||
|
for (int index = 1; index < terms.size(); index++) {
|
||||||
|
total = new Field.Binary(Field.BinaryOp.ADD, total, terms.get(index));
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double normalization(IrisCaveProfile profile) {
|
||||||
|
double weight = Math.abs(profile.getBaseWeight()) + Math.abs(profile.getDetailWeight());
|
||||||
|
for (IrisCaveFieldModule module : profile.getModules()) {
|
||||||
|
weight += Math.abs(module.getWeight());
|
||||||
|
}
|
||||||
|
return weight <= 0.0 ? 1.0 : weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double closure(IrisCaveProfile profile, double normalization) {
|
||||||
|
double minimum = -Math.abs(profile.getBaseWeight()) - Math.abs(profile.getDetailWeight());
|
||||||
|
for (IrisCaveFieldModule module : profile.getModules()) {
|
||||||
|
double threshold = module.getThreshold();
|
||||||
|
double low = module.isInvert() ? threshold - 1.0 : -1.0 - threshold;
|
||||||
|
double high = module.isInvert() ? threshold + 1.0 : 1.0 - threshold;
|
||||||
|
minimum += Math.min(low * module.getWeight(), high * module.getWeight());
|
||||||
|
}
|
||||||
|
return minimum / normalization - CLOSURE_EPSILON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double thresholdCeiling(IrisCaveProfile profile) {
|
||||||
|
IrisStyledRange range = profile.getDensityThreshold();
|
||||||
|
return Math.max(range.getMin(), range.getMax()) - profile.getThresholdBias();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double smoothstep(double progress) {
|
||||||
|
double clamped = Math.max(0.0, Math.min(1.0, progress));
|
||||||
|
return clamped * clamped * (3.0 - 2.0 * clamped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisGeneratorStyle substitute(IrisGeneratorStyle style) {
|
||||||
|
return new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(style.getZoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<NoiseStyle> cellularStyles() {
|
||||||
|
Set<NoiseStyle> cellular = EnumSet.noneOf(NoiseStyle.class);
|
||||||
|
for (NoiseStyle style : NoiseStyle.values()) {
|
||||||
|
String name = style.name();
|
||||||
|
if (name.contains("CELL") || name.contains("VASCULAR") || name.contains("GLOB") || name.contains("HEX")) {
|
||||||
|
cellular.add(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cellular;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Term(IrisGeneratorStyle style, long salt, double weight, double threshold, boolean invert) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.pack.value.IrisRange;
|
||||||
|
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public record CaveSelection(Field index, List<CaveSource> profiles, boolean uncovered, List<CaveSource> bands) {
|
||||||
|
private static final int UNCOVERED = -1;
|
||||||
|
private static final Field NO_INDEX = new Field.Const(0.0);
|
||||||
|
private static final Comparator<ClimateLayout.ClimateEntry> CODE_ORDER = Comparator
|
||||||
|
.comparingDouble(ClimateLayout.ClimateEntry::min)
|
||||||
|
.thenComparingDouble(ClimateLayout.ClimateEntry::max)
|
||||||
|
.thenComparing(ClimateLayout.ClimateEntry::biomeId);
|
||||||
|
|
||||||
|
public CaveSelection {
|
||||||
|
Objects.requireNonNull(index);
|
||||||
|
profiles = List.copyOf(profiles);
|
||||||
|
bands = List.copyOf(bands);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CaveSelection resolve(CaveInputs inputs, IrisDimension dimension, IrisData data, BiomeCodes codes) {
|
||||||
|
if (!dimension.isCarvingEnabled() || dimension.getDisabledComponents().contains(ReservedFlag.CARVED)) {
|
||||||
|
return new CaveSelection(NO_INDEX, List.of(), false, List.of());
|
||||||
|
}
|
||||||
|
Map<IrisCaveProfile, Integer> caseByProfile = new LinkedHashMap<>();
|
||||||
|
List<CaveSource> sources = new ArrayList<>();
|
||||||
|
IrisCaveProfile dimensionProfile = dimension.getCaveProfile();
|
||||||
|
int fallback = enabled(dimensionProfile)
|
||||||
|
? caseOf(source(inputs, inputs.resource(), dimensionProfile), caseByProfile, sources)
|
||||||
|
: UNCOVERED;
|
||||||
|
List<Double> thresholds = new ArrayList<>();
|
||||||
|
List<Integer> leaves = new ArrayList<>();
|
||||||
|
leaves.add(fallback);
|
||||||
|
List<ClimateLayout.ClimateEntry> entries = caveEntries(codes, regionCaveBiomes(dimension, data));
|
||||||
|
for (int position = 0; position < entries.size(); position++) {
|
||||||
|
ClimateLayout.ClimateEntry entry = entries.get(position);
|
||||||
|
Optional<CaveSource> source = entrySource(inputs, dimension, data, codes.loadKeyByBiomeId().get(entry.biomeId()));
|
||||||
|
int selected = source.isPresent() ? caseOf(source.get(), caseByProfile, sources) : fallback;
|
||||||
|
append(thresholds, leaves, entry.min(), selected);
|
||||||
|
double nextMin = position + 1 < entries.size() ? entries.get(position + 1).min() : Double.POSITIVE_INFINITY;
|
||||||
|
if (entry.max() < nextMin) {
|
||||||
|
append(thresholds, leaves, entry.max(), fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean uncovered = leaves.contains(UNCOVERED);
|
||||||
|
int uncoveredCase = sources.size();
|
||||||
|
List<Field> constants = new ArrayList<>(leaves.size());
|
||||||
|
for (int leaf : leaves) {
|
||||||
|
constants.add(new Field.Const(leaf == UNCOVERED ? uncoveredCase : leaf));
|
||||||
|
}
|
||||||
|
Field index = thresholds.isEmpty() ? constants.getFirst() : new Field.Partition(codes.caveCode(), thresholds, constants);
|
||||||
|
return new CaveSelection(index, sources, uncovered, bands(inputs, dimension, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return profiles.isEmpty() && bands.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CaveSource> sources() {
|
||||||
|
List<CaveSource> all = new ArrayList<>(profiles.size() + bands.size());
|
||||||
|
all.addAll(profiles);
|
||||||
|
all.addAll(bands);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field select(Function<CaveSource, Field> compiler, Field uncoveredCase) {
|
||||||
|
List<Field> cases = new ArrayList<>(profiles.size() + 1);
|
||||||
|
for (CaveSource source : profiles) {
|
||||||
|
cases.add(compiler.apply(source));
|
||||||
|
}
|
||||||
|
if (uncovered || cases.isEmpty()) {
|
||||||
|
cases.add(uncoveredCase);
|
||||||
|
}
|
||||||
|
return cases.size() == 1 ? cases.getFirst() : new Field.Mux(index, cases);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void append(List<Double> thresholds, List<Integer> leaves, double start, int selected) {
|
||||||
|
if (leaves.getLast() == selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thresholds.add(start);
|
||||||
|
leaves.add(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ClimateLayout.ClimateEntry> caveEntries(BiomeCodes codes, Set<String> caveBiomes) {
|
||||||
|
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>();
|
||||||
|
for (ClimateLayout.ClimateEntry entry : codes.layout().entries()) {
|
||||||
|
if (entry.axis() == ClimateLayout.ClimateAxis.DEPTH && caveBiomes.contains(codes.loadKeyByBiomeId().get(entry.biomeId()))) {
|
||||||
|
entries.add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.sort(CODE_ORDER);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> regionCaveBiomes(IrisDimension dimension, IrisData data) {
|
||||||
|
Set<String> caveBiomes = new HashSet<>();
|
||||||
|
for (String regionKey : dimension.getRegions()) {
|
||||||
|
IrisRegion region = regionKey == null ? null : data.getRegionLoader().load(regionKey, false);
|
||||||
|
if (region != null && !region.isCompatExcluded()) {
|
||||||
|
caveBiomes.addAll(region.getCaveBiomes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caveBiomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<CaveSource> entrySource(CaveInputs inputs, IrisDimension dimension, IrisData data, String loadKey) {
|
||||||
|
IrisBiome biome = loadKey == null ? null : data.getBiomeLoader().load(loadKey, false);
|
||||||
|
if (biome == null || biome.isCompatExcluded()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (enabled(biome.getCaveProfile())) {
|
||||||
|
return Optional.of(source(inputs, "biomes/" + biome.getLoadKey(), biome.getCaveProfile()));
|
||||||
|
}
|
||||||
|
for (String regionKey : dimension.getRegions()) {
|
||||||
|
IrisRegion region = regionKey == null ? null : data.getRegionLoader().load(regionKey, false);
|
||||||
|
if (region == null || region.isCompatExcluded() || !region.getCaveBiomes().contains(loadKey) || !enabled(region.getCaveProfile())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Optional.of(source(inputs, "regions/" + region.getLoadKey(), region.getCaveProfile()));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CaveSource> bands(CaveInputs inputs, IrisDimension dimension, IrisData data) {
|
||||||
|
List<CaveSource> bands = new ArrayList<>();
|
||||||
|
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
|
||||||
|
if (entries == null) {
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
for (IrisDimensionCarvingEntry entry : entries) {
|
||||||
|
if (entry == null || !entry.isEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisBiome biome = entry.getRealBiome(data);
|
||||||
|
if (biome == null || !enabled(biome.getCaveProfile())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisRange range = entry.getWorldYRange();
|
||||||
|
int low = range == null ? inputs.minY() : Math.max(inputs.minY(), (int) Math.ceil(range.getMin()));
|
||||||
|
int high = range == null ? inputs.maxY() : Math.min(inputs.maxY(), (int) Math.floor(range.getMax()));
|
||||||
|
if (high >= low) {
|
||||||
|
bands.add(new CaveSource("biomes/" + biome.getLoadKey(), biome.getCaveProfile(), low, high));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bands;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int caseOf(CaveSource source, Map<IrisCaveProfile, Integer> caseByProfile, List<CaveSource> sources) {
|
||||||
|
Integer existing = caseByProfile.get(source.profile());
|
||||||
|
if (existing != null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
caseByProfile.put(source.profile(), sources.size());
|
||||||
|
sources.add(source);
|
||||||
|
return sources.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CaveSource source(CaveInputs inputs, String resource, IrisCaveProfile profile) {
|
||||||
|
return new CaveSource(resource, profile, inputs.minY(), inputs.maxY());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean enabled(IrisCaveProfile profile) {
|
||||||
|
return profile != null && profile.isEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record CaveSource(String resource, IrisCaveProfile profile, int minY, int maxY) {
|
||||||
|
public CaveSource {
|
||||||
|
Objects.requireNonNull(resource);
|
||||||
|
Objects.requireNonNull(profile);
|
||||||
|
if (maxY < minY) {
|
||||||
|
throw new IllegalArgumentException("Cave source '" + resource + "' needs maxY at or above minY, got " + minY + ".." + maxY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
|
||||||
|
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class CaveStage implements CompileStage {
|
||||||
|
private static final String CARVERS = "carvers";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "caves";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(PackCompileContext context) {
|
||||||
|
CaveInputs inputs = CaveInputs.of(context);
|
||||||
|
CaveSelection selection = CaveSelection.resolve(inputs, context.dimension(), context.data(), context.biomeCodes());
|
||||||
|
CaveFieldCompiler compiler = new CaveFieldCompiler(context.styles(), inputs);
|
||||||
|
Optional<Field> caves = compiler.compile(selection);
|
||||||
|
if (caves.isPresent()) {
|
||||||
|
Field.Shared field = new Field.Shared(DeepCaveCompiler.FIELD, caves.get());
|
||||||
|
Optional<DeepCaveCompiler.DeepCaves> deep = new DeepCaveCompiler(inputs).compile(field, selection);
|
||||||
|
context.setCaveField(field, CaveFieldCompiler.WALL_BAND);
|
||||||
|
context.setCaves(deep.map(DeepCaveCompiler.DeepCaves::router).orElse(field));
|
||||||
|
deep.ifPresent(found -> context.setDeepCaves(found.opening()));
|
||||||
|
}
|
||||||
|
context.setAquifer(new AquiferCompiler(inputs).compile(selection));
|
||||||
|
List<BiomeDef> biomes = context.biomes();
|
||||||
|
if (biomes.stream().anyMatch(CaveStage::carves)) {
|
||||||
|
context.setBiomes(withoutCarvers(biomes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<BiomeDef> withoutCarvers(List<BiomeDef> biomes) {
|
||||||
|
List<BiomeDef> cleaned = new ArrayList<>(biomes.size());
|
||||||
|
for (BiomeDef biome : biomes) {
|
||||||
|
if (!carves(biome)) {
|
||||||
|
cleaned.add(biome);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JsonObject json = biome.json().deepCopy();
|
||||||
|
json.add(CARVERS, new JsonArray());
|
||||||
|
cleaned.add(new BiomeDef(biome.id(), json));
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean carves(BiomeDef biome) {
|
||||||
|
JsonElement carvers = biome.json().get(CARVERS);
|
||||||
|
return carvers != null && !(carvers.isJsonArray() && carvers.getAsJsonArray().isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package art.arcane.iris.pack.datapack.export.compile.cave;
|
||||||
|
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
|
||||||
|
import art.arcane.iris.pack.datapack.export.ir.Field;
|
||||||
|
import art.arcane.iris.pack.datapack.export.lower.FieldDomains;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class DeepCaveCompiler {
|
||||||
|
static final int LAVA_LINE = -54;
|
||||||
|
static final String FIELD = "cave/field";
|
||||||
|
static final String OPENING = "cave/deep_opening";
|
||||||
|
private static final double OPEN_MARGIN = Math.nextUp(Float.MIN_VALUE);
|
||||||
|
private static final Field OPEN = new Field.Const(1.0);
|
||||||
|
private static final Field INSIDE = new Field.Const(0.0);
|
||||||
|
private static final Field CLOSED = new Field.Const(CaveProfileCompiler.SOLID);
|
||||||
|
private static final Field UNMASKED = new Field.Const(-FieldDomains.CONSTANT_LIMIT);
|
||||||
|
|
||||||
|
private final CaveInputs inputs;
|
||||||
|
private final Field blockY;
|
||||||
|
|
||||||
|
public DeepCaveCompiler(CaveInputs inputs) {
|
||||||
|
this.inputs = Objects.requireNonNull(inputs);
|
||||||
|
this.blockY = inputs.blockY();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<DeepCaves> compile(Field.Shared field, CaveSelection selection) {
|
||||||
|
Optional<Zone> found = zone(selection);
|
||||||
|
if (found.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Zone zone = found.get();
|
||||||
|
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.DEEP_CAVE_LAYERS, inputs.resource(),
|
||||||
|
"Caves between Y " + zone.low() + " and Y " + zone.high() + " open in the material pass, so they get wall palettes but no floor or ceiling layers"));
|
||||||
|
Field opened = new Field.Binary(Field.BinaryOp.MAX, field, inside(zone, INSIDE, CLOSED));
|
||||||
|
Field.Shared opening = new Field.Shared(OPENING,
|
||||||
|
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, new Field.Const(OPEN_MARGIN), opened), OPEN));
|
||||||
|
Field mask = new Field.Binary(Field.BinaryOp.ADD, opening, inside(zone, INSIDE, UNMASKED));
|
||||||
|
return Optional.of(new DeepCaves(new Field.Binary(Field.BinaryOp.MAX, field, mask), opening));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Zone> zone(CaveSelection selection) {
|
||||||
|
int high = Math.min(line() - 1, inputs.maxY());
|
||||||
|
int low = inputs.minY();
|
||||||
|
int deepest = Integer.MAX_VALUE;
|
||||||
|
for (CaveSource source : selection.sources()) {
|
||||||
|
deepest = Math.min(deepest, CaveProfileCompiler.floor(inputs, source));
|
||||||
|
if (source.profile().isAllowLava()) {
|
||||||
|
low = Math.max(low, inputs.lavaY() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (high < low || high < deepest) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(new Zone(low, high));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int line() {
|
||||||
|
return Math.min(LAVA_LINE, DimensionDef.seaLevel(inputs.fluidHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Field inside(Zone zone, Field inRange, Field outOfRange) {
|
||||||
|
return new Field.RangeIf(blockY, zone.low(), zone.high() + 1.0, inRange, outOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DeepCaves(Field router, Field.Shared opening) {
|
||||||
|
public DeepCaves {
|
||||||
|
Objects.requireNonNull(router);
|
||||||
|
Objects.requireNonNull(opening);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Zone(int low, int high) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 385 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user