mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +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
+48102
-535
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;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.platform.ClasspathVanillaDataSource;
|
||||
import art.arcane.iris.platform.bukkit.nms.BukkitGenerationRegistry;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
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.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.VanillaDataSource;
|
||||
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
|
||||
import art.arcane.volmlib.nativelib.item.NativeItem;
|
||||
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 PlatformBiomeWriter biomeWriter;
|
||||
private final PlatformGenerationRegistry generationRegistry;
|
||||
private final ClasspathVanillaDataSource vanillaData = new ClasspathVanillaDataSource(() -> SharedConstants.class);
|
||||
private final NativeBlockResolver blocks = new NativeBlockResolver(this);
|
||||
private final NativeBlockState air = blocks.getAir();
|
||||
private final ConcurrentHashMap<String, NativeBlockState> states = new ConcurrentHashMap<>();
|
||||
@@ -215,6 +218,11 @@ final class HeadlessNativePlatform implements IrisPlatform, PlatformRegistries,
|
||||
return biomeWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VanillaDataSource vanillaData() {
|
||||
return vanillaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File 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.volmlib.util.collection.KList;
|
||||
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -25,6 +26,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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"),
|
||||
PackSource.WORLD, Optional.empty());
|
||||
MultiPackResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, List.of(
|
||||
@@ -78,12 +80,17 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
||||
try {
|
||||
RegistryAccess.Frozen builtins = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
|
||||
RegistryAccess.Frozen loaded;
|
||||
RegistryAccess.Frozen dimensions;
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
|
||||
loaded = RegistryDataLoader.load(resources, builtins.listRegistries().toList(),
|
||||
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(
|
||||
Stream.concat(builtins.registries(), loaded.registries())).freeze();
|
||||
RegistryAccess.Frozen registries = new RegistryAccess.ImmutableRegistryAccess(Stream.of(
|
||||
builtins.registries(), loaded.registries(), dimensions.registries()).flatMap(Function.identity())).freeze();
|
||||
HeadlessNativeBootstrap.bindComponents(registries);
|
||||
return new HeadlessNativeRegistries(directory, resources, registries);
|
||||
} catch (RuntimeException failure) {
|
||||
@@ -106,7 +113,7 @@ final class HeadlessNativeRegistries implements AutoCloseable {
|
||||
delete(directory);
|
||||
}
|
||||
|
||||
private static void delete(Path directory) throws IOException {
|
||||
static void delete(Path directory) throws IOException {
|
||||
try (Stream<Path> paths = Files.walk(directory)) {
|
||||
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
|
||||
Files.delete(path);
|
||||
|
||||
+9
-3
@@ -32,6 +32,7 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
||||
private final Path stagingRoot;
|
||||
private NativeRegionTerrainWriter writer;
|
||||
private HeadlessNativeRegistries nativeRegistries;
|
||||
private File preparedPack;
|
||||
private HolderLookup.Provider registries;
|
||||
|
||||
HeadlessNativeRuntime() {
|
||||
@@ -60,12 +61,16 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
||||
@Override
|
||||
public void prepare(IrisData data, IrisDimension dimension) {
|
||||
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");
|
||||
}
|
||||
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
||||
if (nativeRegistries == null) {
|
||||
nativeRegistries = HeadlessNativeRegistries.compile(data.getDataFolder());
|
||||
preparedPack = pack;
|
||||
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
||||
}
|
||||
registries = nativeRegistries.registries();
|
||||
writer = new NativeRegionTerrainWriter(nativeRegistries.registries());
|
||||
} catch (IOException failure) {
|
||||
throw new UncheckedIOException("Cannot load native generation registries", failure);
|
||||
}
|
||||
@@ -92,6 +97,7 @@ final class HeadlessNativeRuntime implements RealPackProbeSupport.RuntimeBinding
|
||||
if (nativeRegistries != null) {
|
||||
nativeRegistries.close();
|
||||
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;
|
||||
|
||||
import io.papermc.paper.registry.PaperRegistryAccess;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
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 {
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
Map<Object, Object> registries = registries();
|
||||
Map<Object, Object> previous = new HashMap<>(registries);
|
||||
registries.clear();
|
||||
try {
|
||||
try (PaperRegistryScope scope = PaperRegistryScope.open()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user