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:
Brian Neumann-Fopiano committed 2026-10-30 22:59:04 -04:00
1 parent b00e28e42e
commit 1ed1044474
385 files changed
+48102 -535

No files matched your search

@@ -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;
}
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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();
@@ -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);
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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();
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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";
}
}
@@ -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,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());
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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");
}
}
@@ -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());
}
}
}
}
}
@@ -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) {
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
}
}
@@ -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);
}
}
@@ -0,0 +1,189 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ExportIrFixtures;
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
import art.arcane.iris.probe.PaperRegistryIsolation;
import art.arcane.iris.probe.WorldgenExportSampler;
import com.google.gson.JsonParser;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class VanillaNoiseFidelitySourceTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
private static final long SEED = 1337L;
private static final String DIMENSION_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.DIMENSION;
private static final String BIOME_ID = ExportIrFixtures.NAMESPACE + ":" + ExportIrFixtures.BIOME;
private static final String LOAD_KEY = "Flat Plains";
private static final int LAND_HEIGHT = 80;
private static final int MAX_Y = ExportIrFixtures.MIN_Y + ExportIrFixtures.HEIGHT;
private static final String WALL_SHELL = ExportIrFixtures.NAMESPACE + ":cave/wall_shell";
private static final int WALL_MIN_Y = 10;
private static final int WALL_MAX_Y = 20;
private static final int CARVED_HEIGHT = 120;
private static final int VOID_MIN_Y = 96;
private static final int CAVE_MIN_Y = 64;
private static final Field BLOCK_Y = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, -4064.0, 4062.0, -4064.0, 4062.0);
@Test
public void flatExportReportsTheConstantSurface() throws IOException {
WorldgenExportSampler sampler = open(LAND_HEIGHT);
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
assertEquals(ExportIrFixtures.MIN_Y, source.minY());
assertEquals(MAX_Y, source.maxY());
for (int x : new int[]{-37, 0, 113}) {
assertEquals(LAND_HEIGHT, sampler.baseHeight(x, -x));
}
for (int[] chunkPosition : new int[][]{{0, 0}, {-3, 5}}) {
FidelityChunk chunk = source.sample(chunkPosition[0], chunkPosition[1]);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertEquals(LAND_HEIGHT - 1, chunk.surfaceY(x, z));
assertEquals(LOAD_KEY, chunk.surfaceBiome(x, z));
assertTrue(chunk.open(x, LAND_HEIGHT, z));
assertNull(chunk.block(x, LAND_HEIGHT - 1, z));
}
}
for (int y = ExportIrFixtures.MIN_Y; y < LAND_HEIGHT; y++) {
assertFalse("y " + y, chunk.open(7, y, 9));
assertFalse("y " + y, chunk.fluid(7, y, 9));
}
}
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
assertEquals(144L, stats.heightColumns());
assertEquals(LAND_HEIGHT - 1, stats.heightQuantile(0.1D), 0.0D);
assertEquals(LAND_HEIGHT - 1, stats.heightQuantile(0.9D), 0.0D);
assertEquals(1.0D, stats.landFraction(), 0.0D);
assertEquals(Map.of(LOAD_KEY, 144L), stats.biomeColumns());
assertEquals(0L, stats.blockColumns());
assertEquals(0L, stats.wallSolidVoxels());
}
}
@Test
public void wallShellDensityMarksWallVoxels() throws IOException {
WorldgenExportSampler sampler = WorldgenExportSampler.open(flatExport(LAND_HEIGHT, wallShell()), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
FidelityChunk chunk = source.sample(2, -1);
assertTrue(chunk.wallsKnown());
assertTrue(chunk.wall(7, WALL_MIN_Y, 9));
assertTrue(chunk.wall(7, WALL_MAX_Y - 1, 9));
assertFalse(chunk.wall(7, WALL_MIN_Y - 1, 9));
assertFalse(chunk.wall(7, WALL_MAX_Y, 9));
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
long interiorColumns = 9L * 9L;
assertEquals(interiorColumns * (LAND_HEIGHT - 1 - ExportIrFixtures.MIN_Y), stats.wallSolidVoxels());
assertEquals(interiorColumns * (WALL_MAX_Y - WALL_MIN_Y), stats.wallVoxels());
}
}
@Test
public void openSpaceOutsideTheTerrainOperandIsATerrainVoid() throws IOException {
WorldgenExportSampler sampler = WorldgenExportSampler.open(carvedExport(), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(sampler, Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
FidelityChunk chunk = source.sample(1, -2);
assertEquals(CARVED_HEIGHT - 1, chunk.surfaceY(7, 9));
assertTrue(chunk.open(7, VOID_MIN_Y, 9));
assertFalse(chunk.terrainMass(7, VOID_MIN_Y, 9));
assertTrue(chunk.open(7, CAVE_MIN_Y + 3, 9));
assertTrue(chunk.terrainMass(7, CAVE_MIN_Y + 3, 9));
assertFalse(chunk.open(7, VOID_MIN_Y - 4, 9));
assertTrue(chunk.terrainMass(7, VOID_MIN_Y - 4, 9));
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
int voidBand = (VOID_MIN_Y - ExportIrFixtures.MIN_Y) / 16;
int caveBand = (CAVE_MIN_Y - ExportIrFixtures.MIN_Y) / 16;
assertEquals(1.0D, stats.voidShare(voidBand), 0.0D);
assertEquals(0.0D, stats.caveShare(voidBand), 0.0D);
assertEquals(1.0D, stats.caveShare(caveBand), 0.0D);
assertEquals(0.0D, stats.voidShare(caveBand), 0.0D);
}
}
@Test
public void exportWithoutAWallShellReportsNoWalls() throws IOException {
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(open(LAND_HEIGHT), Map.of(BIOME_ID, LOAD_KEY), WALL_SHELL)) {
assertFalse(source.sample(0, 0).wallsKnown());
}
}
@Test
public void exportedBiomeWithoutALoadKeyIsRejected() throws IOException {
try (VanillaNoiseFidelitySource source = new VanillaNoiseFidelitySource(open(LAND_HEIGHT), Map.of(), WALL_SHELL)) {
FidelityChunk chunk = source.sample(0, 0);
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> chunk.surfaceBiome(0, 0));
assertTrue(failure.getMessage(), failure.getMessage().contains(BIOME_ID));
}
}
private static WorldgenExportSampler open(int height) throws IOException {
return WorldgenExportSampler.open(flatExport(height), WorldgenExportMode.PRESET, DIMENSION_ID, SEED);
}
private static JsonResource wallShell() {
return new JsonResource("data/" + ExportIrFixtures.NAMESPACE + "/worldgen/density_function/cave/wall_shell.json", JsonParser.parseString(
"{\"type\":\"minecraft:range_choice\",\"input\":{\"type\":\"minecraft:gradient\",\"axis\":\"y\",\"tiling\":\"clamp_to_edge\","
+ "\"from_coordinate\":-4064,\"to_coordinate\":4062,\"from_value\":-4064.0,\"to_value\":4062.0},"
+ "\"min_inclusive\":" + WALL_MIN_Y + ".0,\"max_exclusive\":" + WALL_MAX_Y + ".0,\"when_in_range\":1.0,\"when_out_of_range\":0.0}"));
}
private static SortedMap<String, byte[]> flatExport(int height, JsonResource... resources) {
Field level = new Field.Const(height);
Field finalDensity = new Field.Grid(new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125),
new Field.Binary(Field.BinaryOp.SUB, level, BLOCK_Y)), 4, 8);
return export(height, finalDensity, resources);
}
private static SortedMap<String, byte[]> carvedExport() {
Field ramp = new Field.Binary(Field.BinaryOp.MUL, new Field.Const(0.125), new Field.Binary(Field.BinaryOp.SUB, new Field.Const(CARVED_HEIGHT), BLOCK_Y));
Field terrain = new Field.RangeIf(BLOCK_Y, VOID_MIN_Y, VOID_MIN_Y + 16, new Field.Const(-1.0), ramp);
Field caves = new Field.RangeIf(BLOCK_Y, CAVE_MIN_Y, CAVE_MIN_Y + 16, new Field.Const(-1.0), new Field.Const(1.0));
return export(CARVED_HEIGHT, new Field.Binary(Field.BinaryOp.MIN, terrain, caves));
}
private static SortedMap<String, byte[]> export(int height, Field finalDensity, JsonResource... resources) {
Field zero = new Field.Const(0.0);
Field level = new Field.Const(height);
RouterDef router = new RouterDef(zero, zero, zero, zero, zero, zero, new Field.Grid(level, 16, 1), finalDensity, AquiferDef.dry(level));
DimensionDef dimension = new DimensionDef(ExportIrFixtures.DIMENSION, ExportIrFixtures.MIN_Y, ExportIrFixtures.HEIGHT,
ExportIrFixtures.HEIGHT, ExportIrFixtures.FLUID_HEIGHT, "minecraft:stone", "minecraft:water", ExportIrFixtures.dimensionType());
ClimateLayout climate = new ClimateLayout(List.of(new ClimateLayout.ClimateEntry(BIOME_ID, ClimateLayout.ClimateAxis.TEMPERATURE, -2.0, 2.0)));
ExportIr ir = new ExportIr(ExportIrFixtures.NAMESPACE, DataVersion.V26_3, WorldgenExportMode.PRESET, dimension, List.of(), router, climate,
List.of(new BiomeDef(ExportIrFixtures.BIOME, ExportIrFixtures.biome())), MaterialProgram.rock("minecraft:stone"),
List.of(), List.of(resources), List.of());
return WorldgenJsonWriter.forTarget(DataVersion.V26_3).write(ir);
}
}
@@ -18,21 +18,41 @@
package art.arcane.iris.command;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.Iris;
import art.arcane.iris.command.handlers.DimensionHandler;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.iris.localization.C;
import art.arcane.iris.localization.DirectorCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.DatapackIngestService;
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
import art.arcane.iris.pack.datapack.export.WorldgenExportServices;
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.localization.LanguageAudience;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.TextKey;
import java.util.List;
import java.util.UUID;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports", descriptionKey = "iris.director.commanddatapack.director.download_manage_external_datapack_imports_modrinth")
public class CommandDatapack implements DirectorExecutor {
private static final int RESOURCE_REPORTS = 4;
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", descriptionKey = "iris.director.commanddatapack.director.download_update_every_datapack_listed_pack_dimension_s_datapackimports_install_it_into", aliases = {"pull"})
public void ingest(
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", descriptionKey = "iris.director.commanddatapack.param.restart_server_when_new_datapacks_are_installed_required_new_structures_register_generate", defaultValue = "false")
@@ -71,4 +91,128 @@ public class CommandDatapack implements DirectorExecutor {
) {
DatapackIngestService.remove(sender(), id);
}
@Director(description = "Export a dimension as a vanilla worldgen datapack", descriptionKey = "iris.director.commanddatapack.director.export_dimension_vanilla_worldgen_datapack")
public void export(
@Param(name = "dimension", description = "The dimension to export", descriptionKey = "iris.director.commanddatapack.param.export_dimension", customHandler = DimensionHandler.class)
IrisDimension dimension,
@Param(name = "version", description = "Target Minecraft version, 26.3 or later (blank uses the latest)", descriptionKey = "iris.director.commanddatapack.param.export_version", defaultValue = "")
String version,
@Param(name = "mode", description = "Output mode: preset, dimension, or overworld", descriptionKey = "iris.director.commanddatapack.param.export_mode", defaultValue = WorldgenExportRequest.DEFAULT_MODE)
String mode,
@Param(name = "namespace", description = "Namespace for the exported resources", descriptionKey = "iris.director.commanddatapack.param.export_namespace", defaultValue = WorldgenExportRequest.DEFAULT_NAMESPACE)
String namespace,
@Param(name = "output", description = "Output name in the datapack exports folder (blank uses the dimension and version)", descriptionKey = "iris.director.commanddatapack.param.export_output", defaultValue = "")
String output,
@Param(name = "format", description = "zip or directory", descriptionKey = "iris.director.commanddatapack.param.export_format", defaultValue = WorldgenExportRequest.DEFAULT_FORMAT)
String format,
@Param(name = "objects", description = "Include objects as structure templates", descriptionKey = "iris.director.commanddatapack.param.export_objects", defaultValue = "true")
boolean objects,
@Param(name = "replace", description = "Replace an existing export with the same name", descriptionKey = "iris.director.commanddatapack.param.export_replace", defaultValue = "false")
boolean replace
) {
VolmitSender sender = sender();
WorldgenExportRequest request = WorldgenExportRequest.builder()
.pack(dimension.getLoader().getDataFolder().toPath())
.dimension(dimension.getLoadKey())
.version(version)
.mode(mode)
.namespace(namespace)
.output(output)
.format(format)
.objects(objects)
.replace(replace)
.exportRoot(IrisPlatforms.get().dataFolder("exports", "datapacks").toPath())
.build();
DataVersion target;
try {
target = validateExport(request);
} catch (IllegalArgumentException invalid) {
sender.sendMessage(C.RED + IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_INVALID, MessageArgument.untrusted("reason", invalid.getMessage())));
return;
}
WorldgenExportService service = WorldgenExportServices.get();
sender.sendMessage(C.GRAY + IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STARTED, MessageArgument.untrusted("dimension", request.dimension()), MessageArgument.untrusted("version", target.getVersion())));
UUID audience = LanguageAudience.current();
ExportFeedback feedback = new ExportFeedback(sender);
J.a(() -> LanguageAudience.run(audience, () -> runExport(service, request, feedback)));
}
static DataVersion validateExport(WorldgenExportRequest request) {
DataVersion target = WorldgenExportTargets.resolve(request.version());
WorldgenExportMode.parse(request.mode());
DatapackExportDestinations.parseFormat(request.format());
return target;
}
private static void runExport(WorldgenExportService service, WorldgenExportRequest request, ExportFeedback feedback) {
WorldgenExportResult result;
try {
result = service.export(request, feedback);
} catch (Throwable failure) {
Iris.reportError("Datapack export failed for '" + request.dimension() + "'.", failure);
feedback.send(C.RED, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED, MessageArgument.untrusted("reason", IrisLanguage.errorDetail(failure))));
return;
}
reportExport(feedback, result);
}
private static void reportExport(ExportFeedback feedback, WorldgenExportResult result) {
for (ExportDiagnostic diagnostic : result.diagnostics()) {
feedback.send(diagnosticColor(diagnostic.severity()), IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_DIAGNOSTIC, MessageArgument.untrusted("key", diagnosticLabel(diagnostic)), MessageArgument.untrusted("message", diagnostic.message())));
}
if (result.isSuccess()) {
feedback.send(C.GREEN, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_EXPORTED, MessageArgument.untrusted("path", result.output())));
return;
}
TextKey outcome = result.status() == WorldgenExportResult.Status.REJECTED
? DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_REJECTED
: DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED;
feedback.send(C.RED, IrisLanguage.text(outcome, MessageArgument.untrusted("reason", result.message())));
}
private static C diagnosticColor(ExportDiagnostic.Severity severity) {
return switch (severity) {
case ERROR -> C.RED;
case WARNING -> C.YELLOW;
case INFO -> C.GRAY;
};
}
private static String diagnosticLabel(ExportDiagnostic diagnostic) {
return diagnostic.resource().isEmpty()
? diagnostic.limit().name()
: diagnostic.limit().name() + " " + diagnostic.resource();
}
private static final class ExportFeedback implements WorldgenExportProgress {
private final VolmitSender sender;
private ExportFeedback(VolmitSender sender) {
this.sender = sender;
}
@Override
public void stage(String stage) {
send(C.GRAY, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STAGE, MessageArgument.untrusted("stage", stage)));
}
@Override
public void resources(int done, int total) {
int stride = Math.max(1, total / RESOURCE_REPORTS);
if (done != total && done % stride != 0) {
return;
}
send(C.GRAY, IrisLanguage.text(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_RESOURCES, MessageArgument.untrusted("done", done), MessageArgument.untrusted("total", total)));
}
private void send(C color, String message) {
String line = color + message;
if (sender.isPlayer()) {
J.runEntity(sender.player(), () -> sender.sendMessage(line));
return;
}
sender.sendMessage(line);
}
}
}
@@ -21,7 +21,8 @@ import art.arcane.iris.structure.StructureImporter;
import art.arcane.iris.structure.VillageImporter;
import art.arcane.iris.structure.authoring.StructureKey;
import art.arcane.iris.structure.authoring.StructureWriteResult;
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
import art.arcane.iris.pack.datapack.export.DatapackExportFormat;
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
import art.arcane.iris.structure.export.VanillaJigsawExportSettings;
import art.arcane.iris.structure.export.VanillaJigsawExportSource;
@@ -420,9 +421,9 @@ public class CommandJigsaw implements DirectorExecutor {
sendError("Wait for the pending autosave or discard the edits before exporting the on-disk graph.");
return;
}
VanillaJigsawExportFormat exportFormat;
DatapackExportFormat exportFormat;
try {
exportFormat = CommandJigsawExports.parseFormat(format);
exportFormat = DatapackExportDestinations.parseFormat(format);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
@@ -431,7 +432,7 @@ public class CommandJigsaw implements DirectorExecutor {
.toPath().toAbsolutePath().normalize();
Path destination;
try {
destination = CommandJigsawExports.resolveExportDestination(exportRoot, output, exportFormat);
destination = DatapackExportDestinations.resolve(exportRoot, output, exportFormat);
} catch (IllegalArgumentException exception) {
sendError(exception.getMessage());
return;
@@ -4,7 +4,7 @@ import art.arcane.iris.Iris;
import art.arcane.iris.studio.jigsaw.JigsawStudioService;
import art.arcane.iris.structure.export.VanillaJigsawDatapackExporter;
import art.arcane.iris.structure.export.VanillaJigsawExportDiagnostic;
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
import art.arcane.iris.structure.export.VanillaJigsawExportRequest;
import art.arcane.iris.structure.export.VanillaJigsawExportResult;
import art.arcane.iris.localization.C;
@@ -13,18 +13,14 @@ import art.arcane.iris.world.task.J;
import org.bukkit.entity.Player;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
final class CommandJigsawExports {
private static final Pattern EXPORT_OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
private static final Set<UUID> EXPORTING_PLAYERS = ConcurrentHashMap.newKeySet();
private static final Set<Path> EXPORT_DESTINATIONS = ConcurrentHashMap.newKeySet();
private CommandJigsawExports() {
}
@@ -45,11 +41,11 @@ final class CommandJigsawExports {
static boolean beginExport(UUID playerId, Path destination) {
Objects.requireNonNull(playerId);
Path normalizedDestination = Objects.requireNonNull(destination).toAbsolutePath().normalize();
Objects.requireNonNull(destination);
if (!EXPORTING_PLAYERS.add(playerId)) {
return false;
}
if (EXPORT_DESTINATIONS.add(normalizedDestination)) {
if (DatapackExportDestinations.claim(destination)) {
return true;
}
EXPORTING_PLAYERS.remove(playerId);
@@ -58,7 +54,7 @@ final class CommandJigsawExports {
static void finishExport(UUID playerId, Path destination) {
EXPORTING_PLAYERS.remove(Objects.requireNonNull(playerId));
EXPORT_DESTINATIONS.remove(Objects.requireNonNull(destination).toAbsolutePath().normalize());
DatapackExportDestinations.release(destination);
}
static void runExport(ExportOperation operation) {
@@ -102,35 +98,6 @@ final class CommandJigsawExports {
+ result.status() + ": " + result.output());
}
static VanillaJigsawExportFormat parseFormat(String value) {
return switch (value.trim().toLowerCase(Locale.ROOT)) {
case "zip" -> VanillaJigsawExportFormat.ZIP;
case "directory", "dir", "folder" -> VanillaJigsawExportFormat.DIRECTORY;
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
};
}
static Path resolveExportDestination(
Path exportRoot,
String output,
VanillaJigsawExportFormat format
) {
Path root = Objects.requireNonNull(exportRoot, "Jigsaw export root").toAbsolutePath().normalize();
String requested = Objects.requireNonNull(output, "Jigsaw export output name");
String name = requested.trim();
if (!name.equals(requested) || !EXPORT_OUTPUT_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
}
String fileName = format == VanillaJigsawExportFormat.ZIP && !name.endsWith(".zip")
? name + ".zip" : name;
Path destination = root.resolve(fileName).normalize();
if (destination.equals(root) || !root.equals(destination.getParent())) {
throw new IllegalArgumentException("Export output must be one artifact inside the Studio exports folder.");
}
return destination;
}
record ExportOperation(
Player player,
VolmitSender sender,
@@ -0,0 +1,104 @@
package art.arcane.iris.command;
import art.arcane.iris.command.handlers.DimensionHandler;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class CommandDatapackExportContractTest {
private static final List<String> NAMES = List.of(
"dimension", "version", "mode", "namespace", "output", "format", "objects", "replace");
private static final List<String> DEFAULTS = List.of(
Param.NO_DEFAULT, "", "overworld", "iris_export", "", "zip", "true", "false");
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void exportParametersAreNamedInOrder() throws Exception {
List<String> names = new ArrayList<>();
for (Parameter parameter : exportMethod().getParameters()) {
names.add(parameter.getAnnotation(Param.class).name());
}
assertEquals(NAMES, names);
}
@Test
public void dimensionIsPositionalAndEveryOtherParameterIsKeyedWithItsDefault() throws Exception {
Parameter[] parameters = exportMethod().getParameters();
List<String> defaults = new ArrayList<>();
for (Parameter parameter : parameters) {
defaults.add(parameter.getAnnotation(Param.class).defaultValue());
}
assertEquals(DEFAULTS, defaults);
assertEquals(DimensionHandler.class, parameters[0].getAnnotation(Param.class).customHandler());
}
@Test
public void exportCommandAndParametersUseRegisteredDescriptions() throws Exception {
Method export = exportMethod();
Director director = export.getAnnotation(Director.class);
assertNotNull(director);
assertEquals(0L, Arrays.stream(director.aliases()).filter((String alias) -> !alias.isBlank()).count());
assertNotNull(director.descriptionKey(), IrisLanguage.catalog().key(director.descriptionKey()));
for (Parameter parameter : export.getParameters()) {
String key = parameter.getAnnotation(Param.class).descriptionKey();
assertNotNull(key, IrisLanguage.catalog().key(key));
}
}
@Test
public void validRequestResolvesItsTarget() {
assertSame(DataVersion.V26_3, CommandDatapack.validateExport(request().build()));
assertSame(DataVersion.V26_3, CommandDatapack.validateExport(request().version("26.3").build()));
}
@Test
public void unsupportedArgumentsAreRejectedWithSharedMessages() {
assertEquals("Unsupported datapack export version '1.21.1'. Supported: 26.3",
reason(request().version("1.21.1").build()));
assertEquals("Unsupported datapack export version '26.2'. Supported: 26.3",
reason(request().version("26.2").build()));
assertEquals("Unknown export mode 'nether'. Expected: preset, dimension, overworld",
reason(request().mode("nether").build()));
assertEquals("Export format must be directory or zip.", reason(request().format("rar").build()));
}
private WorldgenExportRequest.Builder request() {
Path root = temporaryFolder.getRoot().toPath();
return WorldgenExportRequest.builder()
.pack(root.resolve("packs").resolve("overworld"))
.dimension("overworld")
.exportRoot(root.resolve("exports"));
}
private static String reason(WorldgenExportRequest request) {
return assertThrows(IllegalArgumentException.class, () -> CommandDatapack.validateExport(request)).getMessage();
}
private static Method exportMethod() throws NoSuchMethodException {
return CommandDatapack.class.getDeclaredMethod("export", IrisDimension.class, String.class, String.class,
String.class, String.class, String.class, boolean.class, boolean.class);
}
}
@@ -17,7 +17,6 @@ import art.arcane.iris.structure.authoring.StructureOwnershipManifest;
import art.arcane.iris.structure.authoring.StructureSource;
import art.arcane.iris.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.structure.conversion.IrisStructureAdoptionInputKind;
import art.arcane.iris.structure.export.VanillaJigsawExportFormat;
import art.arcane.iris.world.IrisCreator;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.value.IrisDirection;
@@ -413,24 +412,6 @@ public class CommandJigsawContractTest {
}
}
@Test
public void exportOutputIsOneSafeChildArtifact() {
Path root = Path.of("build", "jigsaw-exports").toAbsolutePath().normalize();
assertEquals(root.resolve("village.zip"), CommandJigsawExports.resolveExportDestination(
root, "village", VanillaJigsawExportFormat.ZIP));
assertEquals(root.resolve("village"), CommandJigsawExports.resolveExportDestination(
root, "village", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, "", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, ".", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, "../all-exports", VanillaJigsawExportFormat.DIRECTORY));
assertThrows(IllegalArgumentException.class, () -> CommandJigsawExports.resolveExportDestination(
root, "nested/export", VanillaJigsawExportFormat.DIRECTORY));
}
@Test
public void exportLeaseRejectsDuplicatePlayerAndDestination() {
UUID firstPlayer = UUID.randomUUID();
@@ -25,6 +25,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeEntitySpawns;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedLoader;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeBiomeRegistry;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.platform.ClasspathVanillaDataSource;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandExecutor;
import art.arcane.iris.spi.PlatformGenerationRegistry;
@@ -35,6 +36,7 @@ import art.arcane.volmlib.nativelib.entity.NativeEntityType;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.VanillaDataSource;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import java.io.File;
@@ -56,6 +58,7 @@ public final class ModdedPlatform implements IrisPlatform {
private final ModdedScheduler scheduler;
private final ModdedStructureHooks structureHooks;
private final ModdedBiomeWriter biomeWriter;
private final ClasspathVanillaDataSource vanillaData;
public ModdedPlatform(NativeModdedLoader loader) {
this.loader = loader;
@@ -65,6 +68,7 @@ public final class ModdedPlatform implements IrisPlatform {
this.scheduler = new ModdedScheduler();
this.structureHooks = new ModdedStructureHooks(ModdedEngineBootstrap::currentServer);
this.biomeWriter = new ModdedBiomeWriter(new NativeBiomeRegistry(ModdedEngineBootstrap::currentServer, "minecraft:plains"));
this.vanillaData = new ClasspathVanillaDataSource(() -> overworldClass(ModdedEngineBootstrap.currentServer()));
}
public static void errorSink(Consumer<Throwable> sink) {
@@ -110,6 +114,11 @@ public final class ModdedPlatform implements IrisPlatform {
return biomeWriter;
}
@Override
public VanillaDataSource vanillaData() {
return vanillaData;
}
@Override
public File dataFolder() {
File folder = loader.configDir().resolve("iris").toFile();
@@ -365,4 +374,15 @@ public final class ModdedPlatform implements IrisPlatform {
"modded-generation-registry-v1");
}
static Class<?> overworldClass(NativeModdedServer server) {
if (server == null) {
throw new IllegalStateException("Vanilla data needs a running Minecraft server.");
}
NativeWorld overworld = server.overworld();
if (overworld == null) {
throw new IllegalStateException("Vanilla data needs the Minecraft server overworld to be loaded.");
}
return overworld.nativeHandle().getClass();
}
}
@@ -189,7 +189,8 @@ final class ModdedCommandHelp {
Entry.command("install", "", ModdedHelpMessages.COMMAND_INSTALL_INSTALL_DIMENSION_TYPE_OVERRIDES_FOR_LOADED_IRIS_DIMENSIONS),
Entry.command("list", "", ModdedHelpMessages.COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS, "ls"),
Entry.command("ingest", "", ModdedHelpMessages.COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW, "pull"),
Entry.command("remove", "<id>", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm")
Entry.command("remove", "<id>", ModdedHelpMessages.COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW, "rm"),
Entry.command("export", "<dimension> [version=] [mode=overworld] [namespace=iris_export] [output=] [format=zip] [objects=true] [replace=false]", ModdedHelpMessages.COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK)
));
SECTIONS.put("datapacks", SECTIONS.get("datapack"));
SECTIONS.put("dp", SECTIONS.get("datapack"));
@@ -18,7 +18,14 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedIrisLog;
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
import art.arcane.iris.pack.datapack.export.WorldgenExportResult;
import art.arcane.iris.pack.datapack.export.WorldgenExportService;
import art.arcane.iris.pack.datapack.export.WorldgenExportServices;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.PackDirectoryResolver;
@@ -33,6 +40,7 @@ import com.mojang.brigadier.context.CommandContext;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandRegistration;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldDimensions;
@@ -40,6 +48,7 @@ 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.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -47,15 +56,20 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import art.arcane.iris.localization.DirectorCommandMessages;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.modded.localization.ModdedCommandMessages;
import art.arcane.iris.localization.RuntimeUiMessages;
import art.arcane.volmlib.util.localization.LanguageAudience;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.TextKey;
public final class ModdedDatapackCommands {
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
private static final String WORLD_PACK_NAME = "iris";
private static final int RESOURCE_REPORTS = 4;
private ModdedDatapackCommands() {
}
@@ -79,6 +93,11 @@ public final class ModdedDatapackCommands {
root.then(message("ingest", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
root.then(message("pull", "Managed Modrinth datapack ingest is Bukkit-only. On modded servers install the datapack folder or zip in world/datapacks, enable it, restart, then use /iris datapack list to confirm it is enabled. Registered structures generate natively in Iris dimensions."));
root.then(NativeCommandRegistration.literal("export")
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> export(context.getSource(), "")))
.then(NativeCommandRegistration.argument("args", StringArgumentType.greedyString())
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> export(context.getSource(), StringArgumentType.getString(context, "args"))))));
root.then(message("remove", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
root.then(message("rm", "Managed datapack removal is Bukkit-only. On modded servers disable the pack, delete its folder or zip from world/datapacks, and restart."));
@@ -291,4 +310,107 @@ public final class ModdedDatapackCommands {
}
return null;
}
private static int export(NativeCommandSource source, String raw) {
ModdedDatapackExportArguments arguments;
try {
arguments = ModdedDatapackExportArguments.parse(raw);
} catch (IllegalArgumentException invalid) {
IrisModdedCommands.fail(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_INVALID, MessageArgument.untrusted("reason", invalid.getMessage())));
return 0;
}
File packsRoot = ModdedPackCommands.packsRoot();
File pack = PackDirectoryResolver.resolveExisting(packsRoot, arguments.dimension());
if (pack == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.MODDED_STUDIO_COMMANDS_PACK_NOT_FOUND_UNDER, MessageArgument.untrusted("name", arguments.dimension()), MessageArgument.untrusted("value", packsRoot.getAbsolutePath())));
return 0;
}
WorldgenExportService service = WorldgenExportServices.get();
WorldgenExportRequest request = arguments.toRequest(pack.toPath(), exportRoot());
IrisModdedCommands.ok(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STARTED, MessageArgument.untrusted("dimension", request.dimension()), MessageArgument.untrusted("version", arguments.target().getVersion())));
ExportFeedback feedback = new ExportFeedback(source);
Thread worker = new Thread(() -> runExport(service, request, feedback), "Iris Datapack Export");
worker.setDaemon(true);
worker.start();
return 1;
}
private static Path exportRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("exports").resolve("datapacks");
}
private static void runExport(WorldgenExportService service, WorldgenExportRequest request, ExportFeedback feedback) {
WorldgenExportResult result;
try {
result = service.export(request, feedback);
} catch (Throwable failure) {
ModdedIrisLog.error("Iris datapack export failed for " + request.dimension(), failure);
feedback.post(() -> IrisModdedCommands.fail(feedback.source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED, MessageArgument.untrusted("reason", IrisLanguage.errorDetail(failure)))));
return;
}
feedback.post(() -> reportExport(feedback.source, result));
}
private static void reportExport(NativeCommandSource source, WorldgenExportResult result) {
for (ExportDiagnostic diagnostic : result.diagnostics()) {
String line = IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_DIAGNOSTIC, MessageArgument.untrusted("key", diagnosticLabel(diagnostic)), MessageArgument.untrusted("message", diagnostic.message()));
ModdedCommandFeedback.send(source, NativeCommandText.literal(line).withStyle(diagnosticFormat(diagnostic.severity())));
}
if (result.isSuccess()) {
IrisModdedCommands.ok(source, IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_EXPORTED, MessageArgument.untrusted("path", result.output())));
return;
}
TextKey outcome = result.status() == WorldgenExportResult.Status.REJECTED
? DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_REJECTED
: DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_FAILED;
IrisModdedCommands.fail(source, IrisLanguage.plain(outcome, MessageArgument.untrusted("reason", result.message())));
}
private static NativeCommandText.Format diagnosticFormat(ExportDiagnostic.Severity severity) {
return switch (severity) {
case ERROR -> NativeCommandText.Format.RED;
case WARNING -> NativeCommandText.Format.YELLOW;
case INFO -> NativeCommandText.Format.GRAY;
};
}
private static String diagnosticLabel(ExportDiagnostic diagnostic) {
return diagnostic.resource().isEmpty()
? diagnostic.limit().name()
: diagnostic.limit().name() + " " + diagnostic.resource();
}
private static final class ExportFeedback implements WorldgenExportProgress {
private final NativeCommandSource source;
private final NativeModdedServer server;
private final UUID audience;
private ExportFeedback(NativeCommandSource source) {
this.source = source;
this.server = source.server();
this.audience = source.playerId();
}
@Override
public void stage(String stage) {
post(() -> progress(IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_STAGE, MessageArgument.untrusted("stage", stage))));
}
@Override
public void resources(int done, int total) {
int stride = Math.max(1, total / RESOURCE_REPORTS);
if (done != total && done % stride != 0) {
return;
}
post(() -> progress(IrisLanguage.plain(DirectorCommandMessages.COMMAND_DATAPACK_EXPORT_RESOURCES, MessageArgument.untrusted("done", done), MessageArgument.untrusted("total", total))));
}
private void post(Runnable task) {
server.execute(() -> LanguageAudience.run(audience, task));
}
private void progress(String message) {
ModdedCommandFeedback.send(source, NativeCommandText.literal(message).withStyle(NativeCommandText.Format.GRAY));
}
}
}
@@ -0,0 +1,128 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.DatapackExportDestinations;
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
record ModdedDatapackExportArguments(String dimension, String version, String mode, String namespace, String output,
String format, boolean objects, boolean replace) {
private static final Pattern DIMENSION = Pattern.compile("[A-Za-z0-9_/.-]+");
private static final String VERSION_KEY = "version";
private static final String MODE_KEY = "mode";
private static final String NAMESPACE_KEY = "namespace";
private static final String OUTPUT_KEY = "output";
private static final String FORMAT_KEY = "format";
private static final String OBJECTS_KEY = "objects";
private static final String REPLACE_KEY = "replace";
private static final Set<String> OPTIONS = Set.of(VERSION_KEY, MODE_KEY, NAMESPACE_KEY, OUTPUT_KEY, FORMAT_KEY,
OBJECTS_KEY, REPLACE_KEY);
ModdedDatapackExportArguments {
Objects.requireNonNull(dimension, "dimension");
Objects.requireNonNull(version, "version");
Objects.requireNonNull(mode, "mode");
Objects.requireNonNull(namespace, "namespace");
Objects.requireNonNull(output, "output");
Objects.requireNonNull(format, "format");
}
static ModdedDatapackExportArguments parse(String raw) {
String trimmed = raw == null ? "" : raw.trim();
String[] tokens = trimmed.isEmpty() ? new String[0] : trimmed.split("\\s+");
String dimension = null;
Map<String, String> options = new HashMap<>(OPTIONS.size());
for (String token : tokens) {
int equals = token.indexOf('=');
if (equals < 0) {
if (dimension != null) {
throw new IllegalArgumentException("unexpected argument '" + token + "'");
}
dimension = token;
continue;
}
String key = token.substring(0, equals);
if (!OPTIONS.contains(key)) {
throw new IllegalArgumentException("unknown option '" + key + "'");
}
if (options.putIfAbsent(key, token.substring(equals + 1)) != null) {
throw new IllegalArgumentException("option '" + key + "' given twice");
}
}
if (dimension == null) {
throw new IllegalArgumentException("missing dimension");
}
if (!DIMENSION.matcher(dimension).matches() || dimension.contains("..")) {
throw new IllegalArgumentException("invalid dimension key '" + dimension + "'");
}
ModdedDatapackExportArguments arguments = new ModdedDatapackExportArguments(
dimension,
options.getOrDefault(VERSION_KEY, ""),
options.getOrDefault(MODE_KEY, WorldgenExportRequest.DEFAULT_MODE),
options.getOrDefault(NAMESPACE_KEY, WorldgenExportRequest.DEFAULT_NAMESPACE),
options.getOrDefault(OUTPUT_KEY, ""),
options.getOrDefault(FORMAT_KEY, WorldgenExportRequest.DEFAULT_FORMAT),
flag(OBJECTS_KEY, options.get(OBJECTS_KEY), true),
flag(REPLACE_KEY, options.get(REPLACE_KEY), false));
arguments.target();
WorldgenExportMode.parse(arguments.mode());
DatapackExportDestinations.parseFormat(arguments.format());
return arguments;
}
DataVersion target() {
return WorldgenExportTargets.resolve(version);
}
WorldgenExportRequest toRequest(Path pack, Path exportRoot) {
return WorldgenExportRequest.builder()
.pack(pack)
.dimension(dimension)
.version(version)
.mode(mode)
.namespace(namespace)
.output(output)
.format(format)
.objects(objects)
.replace(replace)
.exportRoot(exportRoot)
.build();
}
private static boolean flag(String key, String raw, boolean fallback) {
if (raw == null) {
return fallback;
}
return switch (raw) {
case "true" -> true;
case "false" -> false;
default -> throw new IllegalArgumentException(key + " must be true or false");
};
}
}
@@ -330,6 +330,10 @@ public final class ModdedHelpMessages {
"iris.modded.help.entry.command.remove",
"Explain datapack removal workflow"
);
public static final TextKey COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK = TextKey.of(
"iris.modded.help.entry.command.export_datapack",
"Export a dimension as a vanilla worldgen datapack"
);
public static final TextKey COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON = TextKey.of(
"iris.modded.help.entry.command.list_structures",
"Regenerate structure-index.json"
@@ -505,6 +509,7 @@ public final class ModdedHelpMessages {
COMMAND_LIST_LIST_CONFIGURED_AND_INSTALLED_DATAPACKS,
COMMAND_INGEST_EXPLAIN_BUKKIT_DATAPACK_INGEST_WORKFLOW,
COMMAND_REMOVE_EXPLAIN_DATAPACK_REMOVAL_WORKFLOW,
COMMAND_EXPORT_EXPORT_A_DIMENSION_AS_A_VANILLA_WORLDGEN_DATAPACK,
COMMAND_LIST_REGENERATE_STRUCTURE_INDEX_JSON,
COMMAND_INFO_RESOLVE_AN_IRIS_STRUCTURE_GRAPH_AND_REPORT_BOUNDS,
COMMAND_PLACE_ASSEMBLE_AND_PLACE_AN_IRIS_STRUCTURE_AT_YOUR_LOCATION,
@@ -0,0 +1,50 @@
package art.arcane.iris.modded;
import art.arcane.iris.platform.ClasspathVanillaDataSource;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ModdedVanillaDataTest {
private static final String PLAINS = "data/minecraft/worldgen/biome/plains.json";
private static final String BIOME_TAGS = "data/minecraft/tags/worldgen/biome";
@BeforeClass
public static void bootstrapRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void overworldLevelClassReadsTheGameVanillaData() {
NativeModdedServer server = mock(NativeModdedServer.class);
NativeWorld overworld = mock(NativeWorld.class);
when(overworld.nativeHandle()).thenReturn(mock(ServerLevel.class));
when(server.overworld()).thenReturn(overworld);
Class<?> anchor = ModdedPlatform.overworldClass(server);
assertSame(ServerLevel.class, anchor);
ClasspathVanillaDataSource source = new ClasspathVanillaDataSource(() -> anchor);
String plains = new String(source.read(PLAINS).orElseThrow(), StandardCharsets.UTF_8);
assertTrue(plains.contains("\"temperature\""));
assertTrue(source.list(BIOME_TAGS).contains(BIOME_TAGS + "/is_overworld.json"));
}
@Test
public void vanillaDataNeedsALoadedOverworld() {
NativeModdedServer server = mock(NativeModdedServer.class);
assertThrows(IllegalStateException.class, () -> ModdedPlatform.overworldClass(null));
assertThrows(IllegalStateException.class, () -> ModdedPlatform.overworldClass(server));
}
}
@@ -63,6 +63,9 @@ public class IrisModdedCommandParityTest {
CommandNode<CommandSourceStack> studio = child(iris, "studio");
child(studio, "package");
child(studio, "pkg");
CommandNode<CommandSourceStack> datapackExport = child(child(iris, "datapack"), "export");
assertNotNull(datapackExport.getCommand());
assertNotNull(child(datapackExport, "args").getCommand());
CommandNode<CommandSourceStack> download = child(iris, "download");
CommandNode<CommandSourceStack> source = child(download, "source");
@@ -211,6 +214,8 @@ public class IrisModdedCommandParityTest {
assertTrue(root.stream().anyMatch((String line) -> line.startsWith("/iris teleport <dimension> [player] (tp) - ")));
assertTrue(ModdedCommandHelp.consoleLines("pregen").stream().anyMatch((String line) ->
line.startsWith("/iris pregen start <radius> [dimension] [at] [x] [z] [gui] [sync] [nocache] - ")));
assertTrue(ModdedCommandHelp.consoleLines("datapack").stream().anyMatch((String line) ->
line.startsWith("/iris datapack export <dimension> [version=] [mode=overworld] [namespace=iris_export] [output=] [format=zip] [objects=true] [replace=false] - ")));
}
private static CommandNode<CommandSourceStack> child(
@@ -0,0 +1,87 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.WorldgenExportRequest;
import org.junit.Test;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedDatapackExportArgumentsTest {
@Test
public void unknownKeyFails() {
assertEquals("unknown option 'seed'", reason("overworld seed=1"));
assertEquals("unknown option 'Version'", reason("overworld Version=26.3"));
}
@Test
public void duplicateKeyFails() {
assertEquals("option 'replace' given twice", reason("overworld replace=true replace=false"));
assertEquals("option 'version' given twice", reason("overworld version= version=26.3"));
}
@Test
public void firstBareTokenIsDimension() {
ModdedDatapackExportArguments arguments = ModdedDatapackExportArguments.parse(
" version=26.3 overworld mode=dimension namespace=worlds output=export_one format=directory objects=false replace=true ");
assertEquals("overworld", arguments.dimension());
assertEquals("26.3", arguments.version());
assertEquals("dimension", arguments.mode());
assertEquals("worlds", arguments.namespace());
assertEquals("export_one", arguments.output());
assertEquals("directory", arguments.format());
assertFalse(arguments.objects());
assertTrue(arguments.replace());
assertSame(DataVersion.V26_3, arguments.target());
assertEquals("unexpected argument 'underworld'", reason("overworld underworld"));
assertEquals("missing dimension", reason(" "));
assertEquals("missing dimension", reason("version=26.3"));
}
@Test
public void defaultsMatchBukkit() {
ModdedDatapackExportArguments arguments = ModdedDatapackExportArguments.parse("overworld");
assertEquals("", arguments.version());
assertEquals("overworld", arguments.mode());
assertEquals("iris_export", arguments.namespace());
assertEquals("", arguments.output());
assertEquals("zip", arguments.format());
assertTrue(arguments.objects());
assertFalse(arguments.replace());
assertSame(DataVersion.V26_3, arguments.target());
Path pack = Path.of("packs", "overworld");
Path exportRoot = Path.of("exports", "datapacks");
WorldgenExportRequest expected = WorldgenExportRequest.builder()
.pack(pack)
.dimension("overworld")
.exportRoot(exportRoot)
.build();
assertEquals(expected, arguments.toRequest(pack, exportRoot));
}
@Test
public void unsupportedValuesFailWithSharedMessages() {
assertEquals("Unsupported datapack export version '1.21.1'. Supported: 26.3",
reason("overworld version=1.21.1"));
assertEquals("Unsupported datapack export version '26.2'. Supported: 26.3",
reason("overworld version=26.2"));
assertEquals("Unknown export mode 'nether'. Expected: preset, dimension, overworld",
reason("overworld mode=nether"));
assertEquals("Export format must be directory or zip.", reason("overworld format=rar"));
assertEquals("objects must be true or false", reason("overworld objects=yes"));
assertEquals("replace must be true or false", reason("overworld replace=1"));
assertEquals("invalid dimension key 'a..b'", reason("a..b"));
}
private static String reason(String raw) {
return assertThrows(IllegalArgumentException.class, () -> ModdedDatapackExportArguments.parse(raw)).getMessage();
}
}
+57
View File
@@ -821,6 +821,13 @@ project(':adapters:bukkit:nms:v26_3_R1') {
}
sourceSets.test.compileClasspath += sourceSets.offline.output
sourceSets.test.runtimeClasspath += sourceSets.offline.output
tasks.named('test').configure {
String exportProbePack = providers.gradleProperty('iris.exportProbePack').orNull
if (exportProbePack != null) {
systemProperty('iris.exportProbePack', exportProbePack)
maxHeapSize = '6g'
}
}
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
dependsOn(offlineClasses)
@@ -848,6 +855,30 @@ project(':adapters:bukkit:nms:v26_3_R1') {
}
}
tasks.register('datapackCostProbe', JavaExec) {
group = 'verification'
description = 'Exports an Iris dimension as a 26.3 worldgen datapack and measures its load, chunk and density function cost against vanilla.'
dependsOn(offlineClasses)
mainClass = 'art.arcane.iris.probe.DatapackCostProbe'
classpath = sourceSets.offline.runtimeClasspath
maxHeapSize = providers.gradleProperty('probeHeap').getOrElse('24G')
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
doFirst {
String dimension = providers.gradleProperty('probeDimension').getOrElse('overworld')
String output = providers.gradleProperty('probeOutput')
.getOrElse(layout.buildDirectory.file("reports/datapack-cost/${dimension}.json").get().asFile.absolutePath)
args([rootProject.file(providers.gradleProperty('probePack').get()).absolutePath, dimension,
providers.gradleProperty('probeSeed').getOrElse('1337'),
providers.gradleProperty('probeChunkSide').getOrElse('6'),
providers.gradleProperty('probeRounds').getOrElse('4'),
providers.gradleProperty('probeWarmupSide').getOrElse('6'),
providers.gradleProperty('probeCalls').getOrElse('256'),
providers.gradleProperty('probeCallSeconds').getOrElse('60'),
rootProject.file(output).absolutePath,
layout.buildDirectory.dir('tmp/datapack-cost').get().asFile.absolutePath])
}
}
tasks.register('regionMantleInspection', JavaExec) {
group = 'verification'
dependsOn(testClasses)
@@ -877,6 +908,32 @@ project(':adapters:bukkit:nms:v26_3_R1') {
}
}
tasks.register('datapackFidelityProbe', JavaExec) {
group = 'verification'
description = 'Exports an Iris dimension as a 26.3 worldgen datapack and compares both generators on the same seed.'
dependsOn(offlineClasses)
mainClass = 'art.arcane.iris.probe.DatapackFidelityProbe'
classpath = sourceSets.offline.runtimeClasspath
maxHeapSize = '8G'
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
doFirst {
String dimension = providers.gradleProperty('probeDimension').get()
String seed = providers.gradleProperty('probeSeed').get()
String mode = providers.gradleProperty('probeMode').getOrElse('full')
boolean biomes = mode == 'biomes'
String output = providers.gradleProperty('probeOutput')
.getOrElse(layout.buildDirectory.file("reports/datapack-fidelity/${dimension}-${seed}${biomes ? '-biomes' : ''}.json").get().asFile.absolutePath)
args([rootProject.file(providers.gradleProperty('probePack').get()).absolutePath, dimension, seed,
biomes ? providers.gradleProperty('probeRadiusChunks').getOrElse('0') : providers.gradleProperty('probeRadiusChunks').get(),
biomes ? providers.gradleProperty('probeStride').getOrElse('1') : providers.gradleProperty('probeStride').get(),
rootProject.file(output).absolutePath,
providers.gradleProperty('probeSurveyBlocks').getOrElse('65536'),
providers.gradleProperty('probeSurveyStride').getOrElse('256'),
providers.gradleProperty('probeSurveySeeds').getOrElse(biomes ? '7' : '1'),
mode])
}
}
}
+4 -5
View File
@@ -193,6 +193,10 @@ tasks.named('test').configure {
maxHeapSize = '2g'
jvmArgs('-XX:+ExitOnOutOfMemoryError')
systemProperty('iris.generation.counters', 'true')
String exportProbePack = providers.gradleProperty('iris.exportProbePack').orNull
if (exportProbePack != null) {
systemProperty('iris.exportProbePack', exportProbePack)
}
}
java {
@@ -360,14 +364,9 @@ List<String> unusedVolmLibPackages = [
]
List<String> unusedVolmLibClasses = [
'art/arcane/volmlib/util/nbt/io/ParseException',
'art/arcane/volmlib/util/nbt/io/SNBTDeserializer',
'art/arcane/volmlib/util/nbt/io/SNBTParser',
'art/arcane/volmlib/util/nbt/io/SNBTSerializer',
'art/arcane/volmlib/util/nbt/io/SNBTUtil',
'art/arcane/volmlib/util/nbt/io/SNBTWriter',
'art/arcane/volmlib/util/nbt/io/StringPointer',
'art/arcane/volmlib/util/io/StringDeserializer',
'art/arcane/volmlib/util/io/StringSerializer',
'art/arcane/volmlib/util/json/EnumType',
'art/arcane/volmlib/util/json/HTTP',
+2
View File
@@ -119,6 +119,8 @@ art/arcane/iris/studio/StudioSVC.java
art/arcane/iris/studio/edit/BlockSignal.java
art/arcane/iris/studio/edit/DustRevealer.java
art/arcane/iris/studio/generation/JigsawStudioGenerator.java
art/arcane/iris/studio/jigsaw/JigsawStudioConnectorTool.java
art/arcane/iris/studio/jigsaw/JigsawStudioFloorPainter.java
art/arcane/iris/studio/generation/ObjectStudioGenerator.java
art/arcane/iris/studio/jigsaw/JigsawStudioAutosaveScheduler.java
art/arcane/iris/studio/jigsaw/JigsawStudioCapture.java
@@ -0,0 +1,74 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.validation;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.spi.IrisLogging;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class ChunkSpiralScan {
private ChunkSpiralScan() {
}
public static int scan(List<int[]> targets, int threads, ChunkVisitor visitor, Listener listener) throws InterruptedException {
Semaphore inFlight = new Semaphore(Math.max(1, threads));
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger failures = new AtomicInteger();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
visitor.visit(chunkX, chunkZ);
ok = true;
} catch (Throwable e) {
failures.incrementAndGet();
IrisLogging.reportError(e);
listener.chunkFailed(chunkX, chunkZ, e);
} finally {
listener.chunkDone(chunkX, chunkZ, ok);
inFlight.release();
done.countDown();
}
});
}
done.await();
return failures.get();
}
@FunctionalInterface
public interface ChunkVisitor {
void visit(int chunkX, int chunkZ) throws Exception;
}
public interface Listener {
void chunkDone(int chunkX, int chunkZ, boolean ok);
void chunkFailed(int chunkX, int chunkZ, Throwable error);
}
}
@@ -26,7 +26,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.ChunkSpiral;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -44,8 +43,6 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class GoldenHashEngine {
@@ -210,40 +207,18 @@ public final class GoldenHashEngine {
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
int total = targets.size();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
if (request.deep()) {
writeDeepDump(chunkX, chunkZ, snapshot);
}
ok = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
progress.chunkFailed(chunkX, chunkZ, e);
} finally {
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
inFlight.release();
done.countDown();
}
});
}
done.await();
ChunkSpiralScan.scan(targets, threads, (int chunkX, int chunkZ) -> hash(lines, chunkX, chunkZ), new ScanProgress(targets.size()));
return lines;
}
private void hash(Map<Long, String> lines, int chunkX, int chunkZ) throws Exception {
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
if (request.deep()) {
writeDeepDump(chunkX, chunkZ, snapshot);
}
}
private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
@@ -562,4 +537,23 @@ public final class GoldenHashEngine {
throw new IllegalStateException(e);
}
}
private final class ScanProgress implements ChunkSpiralScan.Listener {
private final AtomicInteger completed = new AtomicInteger();
private final int total;
private ScanProgress(int total) {
this.total = total;
}
@Override
public void chunkDone(int chunkX, int chunkZ, boolean ok) {
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
}
@Override
public void chunkFailed(int chunkX, int chunkZ, Throwable error) {
progress.chunkFailed(chunkX, chunkZ, error);
}
}
}
@@ -33,6 +33,74 @@ public final class DirectorCommandMessages {
"iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list",
"The datapack id (folder name) shown by /iris datapack list"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_DESCRIPTION = TextKey.of(
"iris.director.commanddatapack.director.export_dimension_vanilla_worldgen_datapack",
"Export a dimension as a vanilla worldgen datapack"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_DIMENSION = TextKey.of(
"iris.director.commanddatapack.param.export_dimension",
"The dimension to export"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_VERSION = TextKey.of(
"iris.director.commanddatapack.param.export_version",
"Target Minecraft version, 26.3 or later (blank uses the latest)"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_MODE = TextKey.of(
"iris.director.commanddatapack.param.export_mode",
"Output mode: preset, dimension, or overworld"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_NAMESPACE = TextKey.of(
"iris.director.commanddatapack.param.export_namespace",
"Namespace for the exported resources"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_OUTPUT = TextKey.of(
"iris.director.commanddatapack.param.export_output",
"Output name in the datapack exports folder (blank uses the dimension and version)"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_FORMAT = TextKey.of(
"iris.director.commanddatapack.param.export_format",
"zip or directory"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_OBJECTS = TextKey.of(
"iris.director.commanddatapack.param.export_objects",
"Include objects as structure templates"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_REPLACE = TextKey.of(
"iris.director.commanddatapack.param.export_replace",
"Replace an existing export with the same name"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_STARTED = TextKey.of(
"iris.director.commanddatapack.export.started",
"Exporting {dimension} as a datapack for Minecraft {version}..."
);
public static final TextKey COMMAND_DATAPACK_EXPORT_STAGE = TextKey.of(
"iris.director.commanddatapack.export.stage",
"Datapack export: {stage}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_RESOURCES = TextKey.of(
"iris.director.commanddatapack.export.resources",
"Datapack export: {done}/{total} resources"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_DIAGNOSTIC = TextKey.of(
"iris.director.commanddatapack.export.diagnostic",
"{key}: {message}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_EXPORTED = TextKey.of(
"iris.director.commanddatapack.export.exported",
"Datapack exported: {path}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_REJECTED = TextKey.of(
"iris.director.commanddatapack.export.rejected",
"Datapack export rejected: {reason}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_FAILED = TextKey.of(
"iris.director.commanddatapack.export.failed",
"Datapack export failed: {reason}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_INVALID = TextKey.of(
"iris.director.commanddatapack.export.invalid",
"Invalid datapack export arguments: {reason}"
);
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER = TextKey.of(
"iris.director.commanddeveloper.director.iris_world_manager",
"Iris World Manager"
@@ -888,6 +956,23 @@ public final class DirectorCommandMessages {
COMMAND_DATAPACK_DIRECTOR_LIST_CONFIGURED_DATAPACK_IMPORTS_THEIR_INSTALLED_VERSIONS,
COMMAND_DATAPACK_DIRECTOR_REMOVE_INSTALLED_DATAPACK_BY_ID_ALSO_DELETE_ITS_URL_FROM_DATAPACKIMPORTS_KEEP,
COMMAND_DATAPACK_PARAM_DATAPACK_ID_FOLDER_NAME_SHOWN_BY_IRIS_DATAPACK_LIST,
COMMAND_DATAPACK_EXPORT_DESCRIPTION,
COMMAND_DATAPACK_EXPORT_PARAM_DIMENSION,
COMMAND_DATAPACK_EXPORT_PARAM_VERSION,
COMMAND_DATAPACK_EXPORT_PARAM_MODE,
COMMAND_DATAPACK_EXPORT_PARAM_NAMESPACE,
COMMAND_DATAPACK_EXPORT_PARAM_OUTPUT,
COMMAND_DATAPACK_EXPORT_PARAM_FORMAT,
COMMAND_DATAPACK_EXPORT_PARAM_OBJECTS,
COMMAND_DATAPACK_EXPORT_PARAM_REPLACE,
COMMAND_DATAPACK_EXPORT_STARTED,
COMMAND_DATAPACK_EXPORT_STAGE,
COMMAND_DATAPACK_EXPORT_RESOURCES,
COMMAND_DATAPACK_EXPORT_DIAGNOSTIC,
COMMAND_DATAPACK_EXPORT_EXPORTED,
COMMAND_DATAPACK_EXPORT_REJECTED,
COMMAND_DATAPACK_EXPORT_FAILED,
COMMAND_DATAPACK_EXPORT_INVALID,
COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER,
COMMAND_DEVELOPER_DIRECTOR_GET_LOADED_TECTONICPLATES_COUNT,
COMMAND_DEVELOPER_DIRECTOR_HASH_GENERATED_BLOCK_OUTPUT_FIXED_AREA_DETERMINISM_IDENTITY_TESTING,
@@ -13,20 +13,24 @@ import art.arcane.iris.pack.datapack.v263.DataFixerV263;
//https://minecraft.wiki/w/Pack_format
@Getter
public enum DataVersion {
UNSUPPORTED("0.0.0", 0, () -> null),
V1_21_1("1.21.1", 48, DataFixerV1206::new),
V26_1_2("26.1.2", 101, DataFixerV1217::new),
V26_2("26.2", 107, DataFixerV1217::new),
V26_3("26.3", 121, DataFixerV263::new);
UNSUPPORTED("0.0.0", 0, 0, 0, () -> null),
V1_21_1("1.21.1", 48, 0, 3955, DataFixerV1206::new),
V26_1_2("26.1.2", 101, 1, 4790, DataFixerV1217::new),
V26_2("26.2", 107, 1, 4903, DataFixerV1217::new),
V26_3("26.3", 121, 0, 5023, DataFixerV263::new);
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
@Getter(AccessLevel.NONE)
private final Supplier<IDataFixer> constructor;
private final String version;
private final int packFormat;
private final int packFormatMinor;
private final int worldDataVersion;
DataVersion(String version, int packFormat, Supplier<IDataFixer> constructor) {
DataVersion(String version, int packFormat, int packFormatMinor, int worldDataVersion, Supplier<IDataFixer> constructor) {
this.constructor = constructor;
this.packFormat = packFormat;
this.packFormatMinor = packFormatMinor;
this.worldDataVersion = worldDataVersion;
this.version = version;
}
@@ -0,0 +1,99 @@
package art.arcane.iris.pack.datapack;
import art.arcane.iris.pack.datapack.DatapackIngestService.Entry;
import art.arcane.iris.pack.datapack.ModrinthResolver.ResolvedDatapack;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.io.IO;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.LinkOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
public final class DatapackExportArchives {
private static final String DIRECT_VERSION_PREFIX = "direct-";
private DatapackExportArchives() {
}
public static Path resolve(String source, DataVersion target) throws IOException {
String normalized = DatapackImportSources.normalizeConfiguredSource(Objects.requireNonNull(source, "datapack import source"));
URI uri = DatapackStartupValidation.parseSourceUri(normalized);
if (uri == null) {
throw new IOException("Empty datapack import source");
}
if ("file".equalsIgnoreCase(uri.getScheme())) {
return DatapackArchive.requireLocalDatapackPath(uri);
}
IrisPlatform platform = IrisPlatforms.get();
File root = platform.dataFolder("datapacks");
File cacheDir = new File(root, "cache");
boolean running = DataVersion.forMinecraftVersion(platform.minecraftVersion()) == target;
DatapackTransactions.TRANSACTION_LOCK.lock();
try {
Entry entry = manifestEntry(root, normalized);
Path cached = ingestCache(cacheDir, entry, running);
if (cached != null) {
return cached;
}
return download(cacheDir, normalized, entry, target);
} finally {
DatapackTransactions.TRANSACTION_LOCK.unlock();
}
}
private static Entry manifestEntry(File root, String source) {
try {
return DatapackManifestStore.readCommittedManifest(root).find(source);
} catch (IOException exception) {
IrisLogging.reportError("The datapack manifest under " + root.getPath() + " could not be read; '" + source
+ "' is resolved again for the export", exception);
return null;
}
}
private static Path ingestCache(File cacheDir, Entry entry, boolean running) throws IOException {
if (entry == null || entry.id == null || entry.sha1 == null) {
return null;
}
boolean direct = entry.versionId != null && entry.versionId.startsWith(DIRECT_VERSION_PREFIX);
if (!direct && !running) {
return null;
}
File archive = new File(cacheDir, entry.id + "-" + DatapackSupport.safeFile(entry.versionId) + ".zip");
return matches(archive, entry.sha1) ? archive.toPath() : null;
}
private static Path download(File cacheDir, String source, Entry entry, DataVersion target) throws IOException {
ResolvedDatapack resolved = ModrinthResolver.resolve(source, target.getVersion());
String id = entry == null || entry.id == null ? DatapackArchive.deriveId(resolved) : entry.id;
File archive = new File(cacheDir, id + "-" + DatapackSupport.safeFile(resolved.getVersionId()) + ".zip");
String expected = resolved.getSha1();
if (expected != null && !expected.isBlank() && matches(archive, expected)) {
return archive.toPath();
}
DatapackSupport.ensureScratchDirectory(cacheDir, "datapack download cache");
DatapackArchive.download(resolved.getDownloadUrl(), archive, null, null);
if (expected != null && !expected.isBlank()) {
String checksum = DatapackArchive.sha1(archive);
if (!expected.equalsIgnoreCase(checksum)) {
IO.delete(archive);
throw new IOException("Checksum mismatch for " + id + " (expected " + expected + ", got " + checksum + ")");
}
}
return archive.toPath();
}
private static boolean matches(File archive, String sha1) throws IOException {
Path path = archive.toPath();
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
return sha1.equalsIgnoreCase(DatapackArchive.sha1(archive));
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.structure.export;
package art.arcane.iris.pack.datapack.export;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
@@ -14,25 +14,26 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java.util.stream.Stream;
final class AtomicDatapackPublisher {
Publication publish(
Map<String, byte[]> resources,
Path output,
VanillaJigsawExportFormat format,
boolean replaceExisting
public final class AtomicDatapackPublisher {
public Publication publish(
Map<String, byte[]> files,
Path destination,
DatapackExportFormat format,
boolean replace,
String stagingPrefix
) throws IOException {
Path parent = output.getParent();
Path parent = destination.getParent();
if (parent == null) {
throw new IOException("Export output has no parent directory: " + output);
throw new IOException("Export output has no parent directory: " + destination);
}
Files.createDirectories(parent);
Path staging = Files.createTempDirectory(parent, ".iris-jigsaw-export-");
Path staging = Files.createTempDirectory(parent, stagingPrefix);
List<String> cleanupWarnings = new ArrayList<>();
try {
Path artifact = format == VanillaJigsawExportFormat.DIRECTORY
? createDirectoryArtifact(staging, resources)
: createZipArtifact(staging, resources);
replaceAtomically(artifact, output, replaceExisting, cleanupWarnings);
Path artifact = format == DatapackExportFormat.DIRECTORY
? createDirectoryArtifact(staging, files)
: createZipArtifact(staging, files);
replaceAtomically(artifact, destination, replace, cleanupWarnings);
} finally {
try {
deleteTree(staging);
@@ -152,8 +153,8 @@ final class AtomicDatapackPublisher {
}
}
record Publication(List<String> cleanupWarnings) {
Publication {
public record Publication(List<String> cleanupWarnings) {
public Publication {
cleanupWarnings = List.copyOf(cleanupWarnings);
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.pack.datapack.export;
public record ContentTrim(int filesBefore, long bytesBefore, int filesAfter, long bytesAfter) {
public static final ContentTrim NONE = new ContentTrim(0, 0L, 0, 0L);
public ContentTrim {
if (filesBefore < 0 || bytesBefore < 0L || filesAfter < 0 || bytesAfter < 0L) {
throw new IllegalArgumentException("Content trim counts cannot be negative");
}
if (filesAfter > filesBefore || bytesAfter > bytesBefore) {
throw new IllegalArgumentException("A content trim cannot add files or bytes");
}
}
public int filesRemoved() {
return filesBefore - filesAfter;
}
public long bytesRemoved() {
return bytesBefore - bytesAfter;
}
}
@@ -0,0 +1,53 @@
package art.arcane.iris.pack.datapack.export;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public final class DatapackExportDestinations {
private static final Pattern OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
private static final Set<Path> CLAIMED = ConcurrentHashMap.newKeySet();
private DatapackExportDestinations() {
}
public static DatapackExportFormat parseFormat(String raw) {
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
case "zip" -> DatapackExportFormat.ZIP;
case "directory", "dir", "folder" -> DatapackExportFormat.DIRECTORY;
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
};
}
public static Path resolve(Path exportRoot, String output, DatapackExportFormat format) {
Path root = Objects.requireNonNull(exportRoot, "Datapack export root").toAbsolutePath().normalize();
String requested = Objects.requireNonNull(output, "Datapack export output name");
String name = requested.trim();
if (!name.equals(requested) || !OUTPUT_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
}
String fileName = format == DatapackExportFormat.ZIP && !name.endsWith(".zip") ? name + ".zip" : name;
Path destination = root.resolve(fileName).normalize();
if (destination.equals(root) || !root.equals(destination.getParent())) {
throw new IllegalArgumentException("Export output must be one artifact inside the exports folder.");
}
return destination;
}
public static boolean claim(Path destination) {
return CLAIMED.add(normalized(destination));
}
public static void release(Path destination) {
CLAIMED.remove(normalized(destination));
}
private static Path normalized(Path destination) {
return Objects.requireNonNull(destination, "Datapack export destination").toAbsolutePath().normalize();
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.pack.datapack.export;
public enum DatapackExportFormat {
ZIP,
DIRECTORY
}
@@ -0,0 +1,83 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
public record GenerationCost(Map<GenerationCost.Root, GenerationCost.RootCost> roots, long densityFunctionBytes) {
public static final GenerationCost NONE = new GenerationCost(Map.of(), 0L);
private static final String DATA_PREFIX = "data/";
private static final String DENSITY_FUNCTION_FOLDER = "/worldgen/density_function/";
private static final String JSON_SUFFIX = ".json";
public GenerationCost {
EnumMap<Root, RootCost> copy = new EnumMap<>(Root.class);
copy.putAll(roots);
roots = Collections.unmodifiableMap(copy);
if (densityFunctionBytes < 0L) {
throw new IllegalArgumentException("Density function JSON size cannot be negative, got " + densityFunctionBytes);
}
}
public static long densityFunctionBytes(Map<String, byte[]> files) {
long bytes = 0L;
for (Map.Entry<String, byte[]> file : files.entrySet()) {
String path = file.getKey();
if (path.startsWith(DATA_PREFIX) && path.contains(DENSITY_FUNCTION_FOLDER) && path.endsWith(JSON_SUFFIX)) {
bytes += file.getValue().length;
}
}
return bytes;
}
public Optional<RootCost> root(Root root) {
return Optional.ofNullable(roots.get(root));
}
public GenerationCost withDensityFunctionBytes(long bytes) {
return new GenerationCost(roots, bytes);
}
public enum Root {
TEMPERATURE("temperature", true),
VEGETATION("vegetation", true),
CONTINENTS("continents", true),
EROSION("erosion", true),
DEPTH("depth", true),
RIDGES("ridges", true),
CHUNK_SURFACE_LEVEL("chunk_surface_level", false),
FINAL_DENSITY("final_density", false),
AQUIFER_BARRIER("aquifer.barrier", false),
AQUIFER_FLOODEDNESS("aquifer.fluid_level_floodedness", false),
AQUIFER_SPREAD("aquifer.fluid_level_spread", false),
AQUIFER_LAVA("aquifer.lava", false),
AQUIFER_EXCLUSION("aquifer.exclusion", false),
AQUIFER_SURFACE_LEVEL("aquifer.surface_level", false);
private final String id;
private final boolean climate;
Root(String id, boolean climate) {
this.id = id;
this.climate = climate;
}
public String id() {
return id;
}
public boolean climate() {
return climate;
}
}
public record RootCost(double worstCaseNodes, double expandedNodes, long distinctNodes) {
public RootCost {
if (!(worstCaseNodes >= 0.0) || !(expandedNodes >= 0.0) || distinctNodes < 0L) {
throw new IllegalArgumentException("Generation cost counts cannot be negative, got " + worstCaseNodes + " worst case, "
+ expandedNodes + " expanded and " + distinctNodes + " distinct");
}
}
}
}
@@ -0,0 +1,186 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.Vector3i;
import art.arcane.volmlib.util.nbt.io.NBTUtil;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.IntTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.GZIPOutputStream;
public final class StructureTemplateEncoder {
private static final Comparator<TemplateBlock> BLOCK_ORDER = Comparator
.comparingInt(TemplateBlock::y)
.thenComparingInt(TemplateBlock::x)
.thenComparingInt(TemplateBlock::z);
public static TemplateVolume volume(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) {
List<TemplateBlock> blocks = new ArrayList<>();
Vector3i center = object.getCenter();
int droppedTiles = 0;
String firstDropped = null;
int outside = 0;
String firstOutside = null;
Set<String> customBlocks = new TreeSet<>();
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : object.getBlocks()) {
IrisBlockVector signed = entry.getKey();
int x = signed.getBlockX() + center.getX();
int y = signed.getBlockY() + center.getY();
int z = signed.getBlockZ() + center.getZ();
if (x < 0 || y < 0 || z < 0 || x >= object.getW() || y >= object.getH() || z >= object.getD()) {
outside++;
if (firstOutside == null) {
firstOutside = x + "," + y + "," + z;
}
continue;
}
VanillaBlockState state;
try {
state = VanillaBlockState.parse(entry.getValue().key());
} catch (IllegalArgumentException exception) {
customBlocks.add(entry.getValue().key());
continue;
}
TileData tile = object.getStates().get(signed);
CompoundTag nbt = null;
if (tile != null) {
Optional<CompoundTag> converted = TileEntityNbt.toNbt(tile.getMaterialKey(), tile.getProperties());
if (converted.isPresent()) {
nbt = converted.get();
} else {
droppedTiles++;
if (firstDropped == null) {
firstDropped = state.name() + " at " + x + "," + y + "," + z;
}
}
}
blocks.add(new TemplateBlock(x, y, z, state, nbt));
}
if (!customBlocks.isEmpty()) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.CUSTOM_BLOCK, resource,
"Blocks outside Minecraft are left out of the template: " + String.join(", ", customBlocks)));
}
if (outside > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.OBJECT_BOUNDS, resource,
outside + " blocks outside the " + object.getW() + "x" + object.getH() + "x" + object.getD()
+ " object size are left out of the template, first at " + firstOutside));
}
if (droppedTiles > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.TILE_ENTITY, resource,
droppedTiles + " tile entities have no vanilla block entity form and export as plain blocks, first "
+ firstDropped));
}
return new TemplateVolume(object.getW(), object.getH(), object.getD(), blocks);
}
public byte[] encode(IrisObject object, List<TemplateBlock> extraBlocks, DataVersion target) throws IOException {
List<ExportDiagnostic> lossy = new ArrayList<>();
TemplateVolume volume = volume(object, object.getLoadKey(), lossy);
if (!lossy.isEmpty()) {
throw new IllegalArgumentException("Object '" + object.getLoadKey() + "' cannot be encoded: " + lossy.getFirst().message());
}
Map<BlockPosition, TemplateBlock> blocks = new LinkedHashMap<>();
for (TemplateBlock block : volume.blocks()) {
blocks.put(new BlockPosition(block.x(), block.y(), block.z()), block);
}
for (TemplateBlock extra : extraBlocks) {
blocks.put(new BlockPosition(extra.x(), extra.y(), extra.z()), extra);
}
return encode(new TemplateVolume(volume.sizeX(), volume.sizeY(), volume.sizeZ(), new ArrayList<>(blocks.values())), target);
}
public byte[] encode(TemplateVolume volume, DataVersion target) throws IOException {
MCABlockStateCodecSupport.Format format = paletteFormat(target);
List<TemplateBlock> orderedBlocks = new ArrayList<>(volume.blocks());
orderedBlocks.sort(BLOCK_ORDER);
for (int index = 1; index < orderedBlocks.size(); index++) {
if (BLOCK_ORDER.compare(orderedBlocks.get(index - 1), orderedBlocks.get(index)) == 0) {
TemplateBlock duplicate = orderedBlocks.get(index);
throw new IllegalArgumentException("Template has two blocks at " + duplicate.x() + "," + duplicate.y() + "," + duplicate.z());
}
}
Map<String, Integer> paletteIndexes = new LinkedHashMap<>();
List<VanillaBlockState> paletteStates = new ArrayList<>();
for (TemplateBlock block : orderedBlocks) {
String key = block.state().canonical();
if (!paletteIndexes.containsKey(key)) {
paletteIndexes.put(key, paletteStates.size());
paletteStates.add(block.state());
}
}
CompoundTag root = new CompoundTag();
root.put("size", intList(volume.sizeX(), volume.sizeY(), volume.sizeZ()));
root.put("palette", palette(paletteStates, format));
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
root.put("entities", new ListTag<>(CompoundTag.class));
root.putInt("DataVersion", target.getWorldDataVersion());
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(output)) {
NBTUtil.write(new NamedTag("", root), gzip, false);
}
return output.toByteArray();
}
private static MCABlockStateCodecSupport.Format paletteFormat(DataVersion target) {
return switch (target) {
case V26_3 -> MCABlockStateCodecSupport.Format.LOWERCASE;
case V1_21_1, V26_1_2, V26_2 -> MCABlockStateCodecSupport.Format.CAPITALIZED;
case UNSUPPORTED -> throw new IllegalArgumentException("Structure templates need a supported target version");
};
}
private ListTag<CompoundTag> palette(List<VanillaBlockState> states, MCABlockStateCodecSupport.Format format) {
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
for (VanillaBlockState state : states) {
palette.add(state.toNbt(format));
}
return palette;
}
private ListTag<CompoundTag> blocks(List<TemplateBlock> entries, Map<String, Integer> paletteIndexes) {
ListTag<CompoundTag> blocks = new ListTag<>(CompoundTag.class);
for (TemplateBlock entry : entries) {
CompoundTag block = new CompoundTag();
block.put("pos", intList(entry.x(), entry.y(), entry.z()));
block.putInt("state", paletteIndexes.get(entry.state().canonical()));
if (entry.nbt() != null) {
block.put("nbt", entry.nbt());
}
blocks.add(block);
}
return blocks;
}
private ListTag<IntTag> intList(int... values) {
ListTag<IntTag> list = new ListTag<>(IntTag.class);
for (int value : values) {
list.add(new IntTag(value));
}
return list;
}
private record BlockPosition(int x, int y, int z) {
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import java.util.Objects;
public record TemplateBlock(int x, int y, int z, VanillaBlockState state, CompoundTag nbt) {
public TemplateBlock {
Objects.requireNonNull(state, "Template block state");
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.pack.datapack.export;
import java.util.List;
public record TemplateVolume(int sizeX, int sizeY, int sizeZ, List<TemplateBlock> blocks) {
public TemplateVolume {
if (sizeX < 0 || sizeY < 0 || sizeZ < 0) {
throw new IllegalArgumentException("Template size must not be negative, got " + sizeX + "x" + sizeY + "x" + sizeZ);
}
blocks = List.copyOf(blocks);
for (TemplateBlock block : blocks) {
if (block.x() < 0 || block.y() < 0 || block.z() < 0 || block.x() >= sizeX || block.y() >= sizeY || block.z() >= sizeZ) {
throw new IllegalArgumentException("Template block at " + block.x() + "," + block.y() + "," + block.z()
+ " lies outside the " + sizeX + "x" + sizeY + "x" + sizeZ + " template");
}
}
}
public int footprint() {
return Math.max(sizeX, sizeZ);
}
}
@@ -0,0 +1,324 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.volmlib.util.nbt.io.SNBTDeserializer;
import art.arcane.volmlib.util.nbt.tag.ByteTag;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.DoubleTag;
import art.arcane.volmlib.util.nbt.tag.EndTag;
import art.arcane.volmlib.util.nbt.tag.FloatTag;
import art.arcane.volmlib.util.nbt.tag.IntArrayTag;
import art.arcane.volmlib.util.nbt.tag.IntTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import art.arcane.volmlib.util.nbt.tag.LongTag;
import art.arcane.volmlib.util.nbt.tag.ShortTag;
import art.arcane.volmlib.util.nbt.tag.StringTag;
import art.arcane.volmlib.util.nbt.tag.Tag;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
public final class TileEntityNbt {
private static final String SNBT_PROPERTY = "nbt";
private static final String ID = "id";
private static final String PROFILE = "profile";
private static final String UUID = "UUID";
private static final int MAX_DEPTH = 64;
private static final Set<String> POSITION_KEYS = Set.of("x", "y", "z");
private static final Map<String, String> BLOCK_ENTITIES = Map.ofEntries(
Map.entry("minecraft:chest", "minecraft:chest"),
Map.entry("minecraft:trapped_chest", "minecraft:trapped_chest"),
Map.entry("minecraft:ender_chest", "minecraft:ender_chest"),
Map.entry("minecraft:barrel", "minecraft:barrel"),
Map.entry("minecraft:furnace", "minecraft:furnace"),
Map.entry("minecraft:smoker", "minecraft:smoker"),
Map.entry("minecraft:blast_furnace", "minecraft:blast_furnace"),
Map.entry("minecraft:dispenser", "minecraft:dispenser"),
Map.entry("minecraft:dropper", "minecraft:dropper"),
Map.entry("minecraft:hopper", "minecraft:hopper"),
Map.entry("minecraft:brewing_stand", "minecraft:brewing_stand"),
Map.entry("minecraft:beacon", "minecraft:beacon"),
Map.entry("minecraft:lectern", "minecraft:lectern"),
Map.entry("minecraft:jukebox", "minecraft:jukebox"),
Map.entry("minecraft:enchanting_table", "minecraft:enchanting_table"),
Map.entry("minecraft:conduit", "minecraft:conduit"),
Map.entry("minecraft:bell", "minecraft:bell"),
Map.entry("minecraft:crafter", "minecraft:crafter"),
Map.entry("minecraft:daylight_detector", "minecraft:daylight_detector"),
Map.entry("minecraft:comparator", "minecraft:comparator"),
Map.entry("minecraft:end_gateway", "minecraft:end_gateway"),
Map.entry("minecraft:end_portal", "minecraft:end_portal"),
Map.entry("minecraft:sculk_sensor", "minecraft:sculk_sensor"),
Map.entry("minecraft:calibrated_sculk_sensor", "minecraft:calibrated_sculk_sensor"),
Map.entry("minecraft:sculk_catalyst", "minecraft:sculk_catalyst"),
Map.entry("minecraft:sculk_shrieker", "minecraft:sculk_shrieker"),
Map.entry("minecraft:chiseled_bookshelf", "minecraft:chiseled_bookshelf"),
Map.entry("minecraft:decorated_pot", "minecraft:decorated_pot"),
Map.entry("minecraft:creaking_heart", "minecraft:creaking_heart"),
Map.entry("minecraft:trial_spawner", "minecraft:trial_spawner"),
Map.entry("minecraft:vault", "minecraft:vault"),
Map.entry("minecraft:beehive", "minecraft:beehive"),
Map.entry("minecraft:bee_nest", "minecraft:beehive"),
Map.entry("minecraft:spawner", "minecraft:mob_spawner"),
Map.entry("minecraft:campfire", "minecraft:campfire"),
Map.entry("minecraft:soul_campfire", "minecraft:campfire"),
Map.entry("minecraft:command_block", "minecraft:command_block"),
Map.entry("minecraft:chain_command_block", "minecraft:command_block"),
Map.entry("minecraft:repeating_command_block", "minecraft:command_block"),
Map.entry("minecraft:suspicious_sand", "minecraft:brushable_block"),
Map.entry("minecraft:suspicious_gravel", "minecraft:brushable_block"));
private static final List<Map.Entry<String, String>> SUFFIX_BLOCK_ENTITIES = List.of(
Map.entry("_hanging_sign", "minecraft:hanging_sign"),
Map.entry("_sign", "minecraft:sign"),
Map.entry("_banner", "minecraft:banner"),
Map.entry("_head", "minecraft:skull"),
Map.entry("_skull", "minecraft:skull"),
Map.entry("_bed", "minecraft:bed"),
Map.entry("shulker_box", "minecraft:shulker_box"),
Map.entry("_shelf", "minecraft:shelf"),
Map.entry("copper_golem_statue", "minecraft:copper_golem_statue"));
private static final Set<String> BYTE_FIELDS = Set.of(
"Slot", "Count", "has_glowing_text", "is_waxed", "auto", "conditionMet", "powered", "TrackOutput",
"UpdateLastExecution", "OnGround", "Invulnerable", "Silent", "NoGravity", "Glowing",
"PersistenceRequired", "CanPickUpLoot", "LeftHanded", "NoAI", "ExactTeleport", "show_particles",
"show_icon", "ambient");
private static final Set<String> SHORT_FIELDS = Set.of(
"Delay", "MinSpawnDelay", "MaxSpawnDelay", "SpawnCount", "MaxNearbyEntities", "RequiredPlayerRange",
"SpawnRange", "BurnTime", "CookTime", "CookTimeTotal", "lit_time_remaining", "lit_total_time",
"cooking_time_spent", "cooking_total_time", "Fire", "Air", "HurtTime", "DeathTime");
private static final Set<String> LONG_FIELDS = Set.of("LootTableSeed", "Age", "LastExecution", "ticks_since_song_started");
private static final Set<String> FLOAT_FIELDS = Set.of("Health", "AbsorptionAmount", "Rotation", "HandDropChances", "ArmorDropChances");
private TileEntityNbt() {
}
public static Optional<String> blockEntityId(String blockKey) {
String key = normalizeBlockKey(blockKey);
if (key.isEmpty()) {
return Optional.empty();
}
String exact = BLOCK_ENTITIES.get(key);
if (exact != null) {
return Optional.of(exact);
}
if (!key.startsWith("minecraft:")) {
return Optional.empty();
}
for (Map.Entry<String, String> suffix : SUFFIX_BLOCK_ENTITIES) {
if (key.endsWith(suffix.getKey())) {
return Optional.of(suffix.getValue());
}
}
return Optional.empty();
}
public static Optional<CompoundTag> toNbt(String blockKey, Map<String, Object> properties) {
Optional<String> blockEntity = blockEntityId(blockKey);
if (blockEntity.isEmpty() || properties == null) {
return Optional.empty();
}
CompoundTag nbt = parseSnbt(properties.get(SNBT_PROPERTY));
if (nbt == null) {
nbt = compound(properties, "", 0);
}
for (String key : POSITION_KEYS) {
nbt.remove(key);
}
nbt.remove(SNBT_PROPERTY);
if (nbt.size() == 0) {
return Optional.empty();
}
if (!nbt.containsKey(ID)) {
nbt.putString(ID, blockEntity.get());
}
return Optional.of(nbt);
}
private static String normalizeBlockKey(String blockKey) {
if (blockKey == null) {
return "";
}
String trimmed = blockKey.trim().toLowerCase(Locale.ROOT);
int properties = trimmed.indexOf('[');
String base = properties < 0 ? trimmed : trimmed.substring(0, properties);
if (base.isEmpty()) {
return "";
}
return base.indexOf(':') < 0 ? "minecraft:" + base : base;
}
private static CompoundTag parseSnbt(Object snbt) {
if (!(snbt instanceof String text) || text.isBlank()) {
return null;
}
try {
Tag<?> parsed = new SNBTDeserializer().fromString(text);
return parsed instanceof CompoundTag compound ? compound : null;
} catch (IOException | RuntimeException exception) {
return null;
}
}
private static CompoundTag compound(Map<?, ?> values, String parent, int depth) {
CompoundTag compound = new CompoundTag();
if (depth > MAX_DEPTH) {
return compound;
}
Map<String, Object> sorted = new TreeMap<>();
for (Map.Entry<?, ?> entry : values.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
sorted.put(String.valueOf(entry.getKey()), entry.getValue());
}
}
if (depth == 0) {
sorted.remove(SNBT_PROPERTY);
}
for (Map.Entry<String, Object> entry : sorted.entrySet()) {
Tag<?> tag = tag(entry.getKey(), parent, entry.getValue(), depth + 1);
if (tag != null) {
compound.put(entry.getKey(), tag);
}
}
return compound;
}
private static Tag<?> tag(String key, String parent, Object value, int depth) {
if (value instanceof Map<?, ?> map) {
return compound(map, key, depth);
}
if (value instanceof List<?> list) {
return list(key, parent, list, depth);
}
if (value instanceof Boolean bool) {
return new ByteTag(bool);
}
if (value instanceof Number number) {
return number(numberKind(key, number), number);
}
return new StringTag(String.valueOf(value));
}
private static Tag<?> list(String key, String parent, List<?> values, int depth) {
if (intArray(key, parent) && allNumbers(values)) {
int[] ints = new int[values.size()];
for (int index = 0; index < ints.length; index++) {
ints[index] = ((Number) values.get(index)).intValue();
}
return new IntArrayTag(ints);
}
NumberKind listKind = allNumbers(values) ? listNumberKind(key, values) : null;
ListTag<?> list = ListTag.createUnchecked(EndTag.class);
Class<?> elementType = null;
for (Object value : values) {
if (value == null) {
continue;
}
Tag<?> element = listKind == null ? tag(key, parent, value, depth + 1) : number(listKind, (Number) value);
if (element == null) {
continue;
}
if (elementType == null) {
elementType = element.getClass();
}
if (elementType != element.getClass()) {
return stringList(values);
}
list.addUnchecked(element);
}
return list;
}
private static ListTag<?> stringList(List<?> values) {
ListTag<?> list = ListTag.createUnchecked(EndTag.class);
for (Object value : values) {
if (value != null) {
list.addUnchecked(new StringTag(String.valueOf(value)));
}
}
return list;
}
private static boolean intArray(String key, String parent) {
return UUID.equals(key) || (ID.equals(key) && PROFILE.equals(parent));
}
private static boolean allNumbers(List<?> values) {
if (values.isEmpty()) {
return false;
}
for (Object value : values) {
if (!(value instanceof Number)) {
return false;
}
}
return true;
}
private static NumberKind listNumberKind(String key, List<?> values) {
NumberKind declared = declaredKind(key);
if (declared != null) {
return declared;
}
NumberKind widest = NumberKind.INT;
for (Object value : values) {
NumberKind kind = inferredKind((Number) value);
if (kind.ordinal() > widest.ordinal()) {
widest = kind;
}
}
return widest;
}
private static NumberKind numberKind(String key, Number number) {
NumberKind declared = declaredKind(key);
return declared == null ? inferredKind(number) : declared;
}
private static NumberKind declaredKind(String key) {
if (BYTE_FIELDS.contains(key)) {
return NumberKind.BYTE;
}
if (SHORT_FIELDS.contains(key)) {
return NumberKind.SHORT;
}
if (LONG_FIELDS.contains(key)) {
return NumberKind.LONG;
}
if (FLOAT_FIELDS.contains(key)) {
return NumberKind.FLOAT;
}
return null;
}
private static NumberKind inferredKind(Number number) {
if (number instanceof Double || number instanceof Float) {
return NumberKind.DOUBLE;
}
long value = number.longValue();
return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE ? NumberKind.INT : NumberKind.LONG;
}
private static Tag<?> number(NumberKind kind, Number number) {
return switch (kind) {
case BYTE -> new ByteTag(number.byteValue());
case SHORT -> new ShortTag(number.shortValue());
case INT -> new IntTag(number.intValue());
case LONG -> new LongTag(number.longValue());
case FLOAT -> new FloatTag(number.floatValue());
case DOUBLE -> new DoubleTag(number.doubleValue());
};
}
private enum NumberKind {
BYTE,
SHORT,
INT,
LONG,
FLOAT,
DOUBLE
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.structure.export;
package art.arcane.iris.pack.datapack.export;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
@@ -9,15 +9,15 @@ import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
record VanillaBlockState(String name, Map<String, String> properties) {
public record VanillaBlockState(String name, Map<String, String> properties) {
private static final Pattern PROPERTY_NAME = Pattern.compile("[a-z0-9_.-]+");
private static final Pattern PROPERTY_VALUE = Pattern.compile("[a-z0-9_.-]+");
VanillaBlockState {
public VanillaBlockState {
properties = Collections.unmodifiableMap(new LinkedHashMap<>(properties));
}
static VanillaBlockState parse(String value) {
public static VanillaBlockState parse(String value) {
if (value == null) {
throw new IllegalArgumentException("Block state must not be null");
}
@@ -58,7 +58,7 @@ record VanillaBlockState(String name, Map<String, String> properties) {
return new VanillaBlockState(name, properties);
}
String canonical() {
public String canonical() {
if (properties.isEmpty()) {
return name;
}
@@ -74,7 +74,7 @@ record VanillaBlockState(String name, Map<String, String> properties) {
return builder.append(']').toString();
}
CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
public CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
CompoundTag state = new CompoundTag();
state.putString(format.nameKey(), name);
if (!properties.isEmpty()) {
@@ -0,0 +1,104 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Locale;
import java.util.StringJoiner;
import java.util.regex.Pattern;
public final class VanillaResourceIdentifier {
private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+");
private static final Pattern PATH = Pattern.compile("[a-z0-9/._-]+");
private static final Pattern INVALID_PATH_CHARACTER = Pattern.compile("[^a-z0-9._-]");
private VanillaResourceIdentifier() {
}
public static boolean validNamespace(String value) {
return value != null && NAMESPACE.matcher(value).matches();
}
public static boolean validPath(String value) {
if (value == null
|| !PATH.matcher(value).matches()
|| value.startsWith("/")
|| value.endsWith("/")
|| value.contains("//")) {
return false;
}
for (String segment : value.split("/")) {
if (segment.equals(".") || segment.equals("..")) {
return false;
}
}
return true;
}
public static boolean validIdentifier(String value) {
if (value == null) {
return false;
}
int separator = value.indexOf(':');
if (separator <= 0 || separator != value.lastIndexOf(':')) {
return false;
}
return validNamespace(value.substring(0, separator)) && validPath(value.substring(separator + 1));
}
public static String requireNamespace(String value, String role) {
if (!validNamespace(value)) {
throw invalid(value, role);
}
return value;
}
public static String requirePath(String value, String role) {
if (!validPath(value)) {
throw invalid(value, role);
}
return value;
}
public static String requireIdentifier(String value, String role) {
if (!validIdentifier(value)) {
throw invalid(value, role);
}
return value;
}
public static String sanitizePath(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Cannot derive a resource path from '" + value + "'");
}
String lowered = value.trim().replace('\\', '/').toLowerCase(Locale.ROOT);
StringJoiner path = new StringJoiner("/");
for (String segment : lowered.split("/")) {
if (segment.isEmpty()) {
continue;
}
String cleaned = INVALID_PATH_CHARACTER.matcher(segment).replaceAll("_");
path.add(cleaned.equals(".") || cleaned.equals("..") ? cleaned.replace('.', '_') : cleaned);
}
String result = path.toString();
if (result.isEmpty()) {
throw new IllegalArgumentException("Cannot derive a resource path from '" + value + "'");
}
return result;
}
public static String normalizeConnectorIdentifier(String value) {
if (value == null || value.isBlank()) {
return "minecraft:empty";
}
String normalized = value.trim();
if (normalized.indexOf(':') < 0) {
normalized = "minecraft:" + normalized;
}
if (!validIdentifier(normalized)) {
throw new IllegalArgumentException("Invalid resource identifier '" + value + "'");
}
return normalized;
}
private static IllegalArgumentException invalid(String value, String role) {
return new IllegalArgumentException("Invalid " + role + " '" + value + "'");
}
}
@@ -0,0 +1,170 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseStyleCompiler;
import art.arcane.iris.pack.datapack.export.emit.UnusedResourcePruner;
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.pack.datapack.export.lower.GenerationBudget;
import art.arcane.iris.pack.datapack.export.lower.GenerationCostCounter;
import art.arcane.iris.pack.datapack.export.lower.VanillaBounds;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisLogging;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.function.Function;
import java.util.regex.Pattern;
public final class WorldgenDatapackExporter implements WorldgenExportService {
private static final String STAGING_PREFIX = ".iris-worldgen-export-";
private static final Pattern UNSAFE_OUTPUT_CHARACTER = Pattern.compile("[^A-Za-z0-9._-]");
private final Function<DataVersion, WorldgenJsonWriter> writers;
private final PackCompiler compiler;
public WorldgenDatapackExporter(Function<DataVersion, WorldgenJsonWriter> writers, PackCompiler compiler) {
this.writers = Objects.requireNonNull(writers);
this.compiler = Objects.requireNonNull(compiler);
}
public static WorldgenDatapackExporter create() {
return new WorldgenDatapackExporter(WorldgenJsonWriter::forTarget, PackCompiler.create());
}
@Override
public WorldgenExportResult export(WorldgenExportRequest request, WorldgenExportProgress progress) {
Objects.requireNonNull(request);
Objects.requireNonNull(progress);
ExportPlan plan;
try {
plan = plan(request);
} catch (IllegalArgumentException exception) {
return WorldgenExportResult.rejected(null, exception.getMessage(), List.of());
}
Path destination = plan.destination();
if (Files.exists(destination) && !request.replace()) {
return rejected(destination, "Export output already exists: " + destination + ". Pass replace=true to overwrite it.");
}
if (!DatapackExportDestinations.claim(destination)) {
return rejected(destination, "An export to " + destination + " is already running.");
}
try {
return exportClaimed(request, plan, progress);
} finally {
DatapackExportDestinations.release(destination);
}
}
private ExportPlan plan(WorldgenExportRequest request) {
DataVersion target = WorldgenExportTargets.resolve(request.version());
WorldgenExportMode mode = WorldgenExportMode.parse(request.mode());
DatapackExportFormat format = DatapackExportDestinations.parseFormat(request.format());
String namespace = VanillaResourceIdentifier.requireNamespace(request.namespace(), "export namespace");
String output = request.output().isEmpty() ? defaultOutput(request.dimension(), target) : request.output();
return new ExportPlan(target, mode, format, namespace, DatapackExportDestinations.resolve(request.exportRoot(), output, format));
}
private WorldgenExportResult exportClaimed(WorldgenExportRequest request, ExportPlan plan, WorldgenExportProgress progress) {
Path destination = plan.destination();
File pack = request.pack().toFile();
try {
progress.stage("validate");
PackValidationResult validation = PackValidator.validateForPackaging(pack);
if (!validation.isLoadable()) {
return rejected(destination, "Pack '" + validation.getPackName() + "' failed validation: "
+ String.join("; ", validation.getBlockingErrors()));
}
progress.stage("load");
IrisData data = IrisData.openRuntime(pack);
try {
IrisDimension dimension = data.getDimensionLoader().load(request.dimension(), false);
if (dimension == null) {
return rejected(destination, "Dimension '" + request.dimension() + "' was not found in pack '" + pack.getName() + "'.");
}
PackCompileContext context = compileContext(data, dimension, plan, request.objects());
ExportIr ir = compiler.compile(context, progress);
return publish(request, plan, ir, context.biomeCodes().loadKeyByBiomeId(), progress);
} finally {
data.close();
}
} catch (IOException exception) {
IrisLogging.reportError("Worldgen datapack publication to " + destination + " failed", exception);
return WorldgenExportResult.failed(destination, "Datapack publication failed: " + exception.getMessage(), List.of());
} catch (RuntimeException exception) {
IrisLogging.reportError("Worldgen datapack export of dimension '" + request.dimension() + "' failed", exception);
return WorldgenExportResult.failed(destination, "Datapack export failed: " + exception.getMessage(), List.of());
}
}
private WorldgenExportResult publish(WorldgenExportRequest request, ExportPlan plan, ExportIr ir, Map<String, String> loadKeyByBiomeId,
WorldgenExportProgress progress) throws IOException {
progress.stage("write");
GenerationCost routerCost = GenerationCostCounter.count(ir.router(), VanillaBounds.of(ir.namespace(), ir.noises()), ir.target());
SortedMap<String, byte[]> files = writers.apply(plan.target()).write(ir);
progress.resources(files.size(), files.size());
progress.stage("prune");
UnusedResourcePruner.Pruned pruned = UnusedResourcePruner.prune(files);
progress.trimmed(pruned.trim());
GenerationCost cost = routerCost.withDensityFunctionBytes(GenerationCost.densityFunctionBytes(pruned.files()));
progress.stage("publish");
AtomicDatapackPublisher.Publication publication = new AtomicDatapackPublisher().publish(
pruned.files(),
plan.destination(),
plan.format(),
request.replace(),
STAGING_PREFIX);
List<ExportDiagnostic> costDiagnostics = GenerationBudget.check(cost, ir.dimension().name());
List<ExportDiagnostic> diagnostics = new ArrayList<>(ir.diagnostics().size() + pruned.diagnostics().size() + costDiagnostics.size());
diagnostics.addAll(ir.diagnostics());
diagnostics.addAll(costDiagnostics);
diagnostics.addAll(pruned.diagnostics());
return new WorldgenExportResult(
WorldgenExportResult.Status.EXPORTED,
plan.destination(),
String.join("; ", publication.cleanupWarnings()),
diagnostics,
loadKeyByBiomeId,
pruned.trim(),
cost);
}
private static PackCompileContext compileContext(IrisData data, IrisDimension dimension, ExportPlan plan, boolean objects) {
List<ExportDiagnostic> diagnostics = new ArrayList<>();
return PackCompileContext.builder()
.data(data)
.dimension(dimension)
.capabilities(ExportCapabilities.forTarget(plan.target()))
.namespace(plan.namespace())
.mode(plan.mode())
.objects(objects)
.diagnostics(diagnostics)
.styles(NoiseStyleCompiler.create(diagnostics))
.build();
}
private static String defaultOutput(String dimension, DataVersion target) {
return UNSAFE_OUTPUT_CHARACTER.matcher(dimension.replace('/', '-')).replaceAll("-") + "-" + target.getVersion();
}
private static WorldgenExportResult rejected(Path destination, String message) {
return WorldgenExportResult.rejected(destination, message, List.of());
}
private record ExportPlan(DataVersion target, WorldgenExportMode mode, DatapackExportFormat format, String namespace, Path destination) {
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Locale;
public enum WorldgenExportMode {
PRESET,
DIMENSION,
OVERWORLD;
public static WorldgenExportMode parse(String raw) {
if (raw == null || raw.isBlank()) {
return OVERWORLD;
}
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
case "preset" -> PRESET;
case "dimension" -> DIMENSION;
case "overworld" -> OVERWORLD;
default -> throw new IllegalArgumentException("Unknown export mode '" + raw + "'. Expected: preset, dimension, overworld");
};
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.pack.datapack.export;
public interface WorldgenExportProgress {
WorldgenExportProgress NONE = new WorldgenExportProgress() {
@Override
public void stage(String stage) {
}
@Override
public void resources(int done, int total) {
}
};
void stage(String stage);
void resources(int done, int total);
default void trimmed(ContentTrim trim) {
}
}
@@ -0,0 +1,112 @@
package art.arcane.iris.pack.datapack.export;
import java.nio.file.Path;
import java.util.Objects;
public record WorldgenExportRequest(
Path pack,
String dimension,
String version,
String mode,
String namespace,
String output,
String format,
boolean objects,
boolean replace,
Path exportRoot
) {
public static final String DEFAULT_MODE = "overworld";
public static final String DEFAULT_NAMESPACE = "iris_export";
public static final String DEFAULT_FORMAT = "zip";
public WorldgenExportRequest {
if (pack == null) {
throw new IllegalArgumentException("Worldgen export request requires a pack directory");
}
if (dimension == null || dimension.isBlank()) {
throw new IllegalArgumentException("Worldgen export request requires a dimension");
}
if (exportRoot == null) {
throw new IllegalArgumentException("Worldgen export request requires an export root");
}
Objects.requireNonNull(version);
Objects.requireNonNull(mode);
Objects.requireNonNull(namespace);
Objects.requireNonNull(output);
Objects.requireNonNull(format);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private Path pack;
private String dimension = "";
private String version = "";
private String mode = DEFAULT_MODE;
private String namespace = DEFAULT_NAMESPACE;
private String output = "";
private String format = DEFAULT_FORMAT;
private boolean objects = true;
private boolean replace;
private Path exportRoot;
private Builder() {
}
public Builder pack(Path value) {
pack = Objects.requireNonNull(value);
return this;
}
public Builder dimension(String value) {
dimension = Objects.requireNonNull(value).trim();
return this;
}
public Builder version(String value) {
version = Objects.requireNonNull(value).trim();
return this;
}
public Builder mode(String value) {
mode = Objects.requireNonNull(value).trim();
return this;
}
public Builder namespace(String value) {
namespace = Objects.requireNonNull(value).trim();
return this;
}
public Builder output(String value) {
output = Objects.requireNonNull(value).trim();
return this;
}
public Builder format(String value) {
format = Objects.requireNonNull(value).trim();
return this;
}
public Builder objects(boolean value) {
objects = value;
return this;
}
public Builder replace(boolean value) {
replace = value;
return this;
}
public Builder exportRoot(Path value) {
exportRoot = Objects.requireNonNull(value);
return this;
}
public WorldgenExportRequest build() {
return new WorldgenExportRequest(pack, dimension, version, mode, namespace, output, format, objects, replace, exportRoot);
}
}
}
@@ -0,0 +1,48 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
public record WorldgenExportResult(Status status, Path output, String message, List<ExportDiagnostic> diagnostics,
Map<String, String> loadKeyByBiomeId, ContentTrim trim, GenerationCost cost) {
public WorldgenExportResult {
Objects.requireNonNull(status);
if (status == Status.EXPORTED) {
Objects.requireNonNull(output);
}
message = message == null ? "" : message;
diagnostics = List.copyOf(diagnostics);
loadKeyByBiomeId = Collections.unmodifiableSortedMap(new TreeMap<>(loadKeyByBiomeId));
Objects.requireNonNull(trim);
Objects.requireNonNull(cost);
}
public static WorldgenExportResult exported(Path output, List<ExportDiagnostic> diagnostics, Map<String, String> loadKeyByBiomeId,
ContentTrim trim, GenerationCost cost) {
return new WorldgenExportResult(Status.EXPORTED, output, "", diagnostics, loadKeyByBiomeId, trim, cost);
}
public static WorldgenExportResult rejected(Path output, String message, List<ExportDiagnostic> diagnostics) {
return new WorldgenExportResult(Status.REJECTED, output, message, diagnostics, Map.of(), ContentTrim.NONE, GenerationCost.NONE);
}
public static WorldgenExportResult failed(Path output, String message, List<ExportDiagnostic> diagnostics) {
return new WorldgenExportResult(Status.FAILED, output, message, diagnostics, Map.of(), ContentTrim.NONE, GenerationCost.NONE);
}
public boolean isSuccess() {
return status == Status.EXPORTED;
}
public enum Status {
EXPORTED,
REJECTED,
FAILED
}
}
@@ -0,0 +1,5 @@
package art.arcane.iris.pack.datapack.export;
public interface WorldgenExportService {
WorldgenExportResult export(WorldgenExportRequest request, WorldgenExportProgress progress);
}
@@ -0,0 +1,23 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class WorldgenExportServices {
private static final AtomicReference<WorldgenExportService> SERVICE = new AtomicReference<>();
private WorldgenExportServices() {
}
public static void install(WorldgenExportService service) {
SERVICE.set(Objects.requireNonNull(service, "Datapack export service"));
}
public static WorldgenExportService get() {
WorldgenExportService service = SERVICE.get();
if (service != null) {
return service;
}
return SERVICE.updateAndGet(current -> current == null ? WorldgenDatapackExporter.create() : current);
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.pack.datapack.DataVersion;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
public final class WorldgenExportTargets {
private static final DataVersion OLDEST = DataVersion.V26_3;
public static final List<DataVersion> SUPPORTED = supported();
private WorldgenExportTargets() {
}
public static DataVersion resolve(String raw) {
if (raw == null || raw.isBlank()) {
return SUPPORTED.getLast();
}
DataVersion version = DataVersion.forMinecraftVersion(raw.trim());
if (!SUPPORTED.contains(version)) {
throw unsupported(raw);
}
return version;
}
public static IllegalArgumentException unsupported(String raw) {
StringJoiner supported = new StringJoiner(", ");
for (DataVersion version : SUPPORTED) {
supported.add(version.getVersion());
}
return new IllegalArgumentException("Unsupported datapack export version '" + raw + "'. Supported: " + supported);
}
private static List<DataVersion> supported() {
List<DataVersion> versions = new ArrayList<>();
for (DataVersion version : DataVersion.values()) {
if (version.ordinal() >= OLDEST.ordinal()) {
versions.add(version);
}
}
return List.copyOf(versions);
}
}
@@ -0,0 +1,128 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.pack.ContentKeyValidator;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.validation.ContentGate;
import art.arcane.iris.pack.validation.LegacyBlockRenames;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class BlockStateResolver {
private final IrisData data;
private final PlatformRegistries registries;
private final Set<String> knownBlocks;
private final List<ExportDiagnostic> diagnostics;
private final Map<String, Resolution> resolutions = new HashMap<>();
public BlockStateResolver(IrisData data, PlatformRegistries registries, List<ExportDiagnostic> diagnostics) {
this.data = Objects.requireNonNull(data);
this.registries = registries;
this.knownBlocks = registries == null ? Set.of() : knownBlocks(registries.blockTypeKeys());
this.diagnostics = Objects.requireNonNull(diagnostics);
}
public static BlockStateResolver forContext(PackCompileContext context) {
PlatformRegistries registries = IrisPlatforms.isBound() ? IrisPlatforms.get().registries() : null;
return new BlockStateResolver(context.data(), registries, context.diagnostics());
}
public Optional<VanillaBlockState> resolve(IrisBlockData block, String resource) {
if (block == null || block.getBlock() == null || block.getBlock().isBlank()) {
return Optional.empty();
}
return resolve(stateKey(block), resource);
}
public Optional<VanillaBlockState> resolve(String rawState, String resource) {
String normalized = ContentGate.normalizeState(rawState);
if (normalized == null) {
return Optional.empty();
}
Resolution resolution = resolutions.computeIfAbsent(normalized, this::parse);
if (resolution.problem() != null) {
ExportDiagnostic diagnostic = new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.BLOCK_STATE, resource, resolution.problem());
if (!diagnostics.contains(diagnostic)) {
diagnostics.add(diagnostic);
}
}
return Optional.ofNullable(resolution.state());
}
private String stateKey(IrisBlockData block) {
IrisBlockData custom = data.getBlockLoader() == null ? null : data.getBlockLoader().load(block.getBlock(), false);
if (custom == null || custom.getBlock() == null || custom.getBlock().equals(block.getBlock())) {
return block.stateKey();
}
IrisBlockData merged = IrisBlockData.from(custom.stateKey());
merged.getData().putAll(block.getData());
return merged.stateKey();
}
private Resolution parse(String normalized) {
String renamed = renamed(normalized);
if (!renamed.startsWith("minecraft:")) {
return new Resolution(null, "Block '" + renamed + "' is not a Minecraft block and is not exported");
}
String problem = null;
String validated = renamed;
List<String> propertyProblems = registries == null ? List.of() : ContentKeyValidator.validateBlockStateProperties(registries, List.of(renamed));
if (!propertyProblems.isEmpty()) {
validated = ContentGate.baseKey(renamed);
problem = String.join("; ", propertyProblems) + "; the block exports in its default state";
}
try {
return new Resolution(VanillaBlockState.parse(validated), problem);
} catch (IllegalArgumentException exception) {
return new Resolution(null, "Block state '" + renamed + "' is not exported: " + exception.getMessage());
}
}
private String renamed(String normalized) {
if (knownBlocks.isEmpty() || knownBlocks.contains(ContentGate.baseKey(normalized))) {
return normalized;
}
String properties = normalized.substring(ContentGate.baseKey(normalized).length());
String current = normalized;
for (int hop = 0; hop < LegacyBlockRenames.MAX_HOPS; hop++) {
String supplement = LegacyBlockRenames.supplementFor(current);
if (supplement == null) {
return normalized;
}
String next = ContentGate.normalizeState(supplement);
if (next == null) {
return normalized;
}
if (knownBlocks.contains(ContentGate.baseKey(next))) {
return ContentGate.baseKey(next) + properties;
}
current = next;
}
return normalized;
}
private static Set<String> knownBlocks(List<String> keys) {
Set<String> known = new HashSet<>(keys.size());
for (String key : keys) {
String normalized = ContentGate.normalizeState(key);
if (normalized != null) {
known.add(ContentGate.baseKey(normalized));
}
}
return known;
}
private record Resolution(VanillaBlockState state, String problem) {
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.pack.datapack.export.compile;
public interface CompileStage {
String name();
void apply(PackCompileContext context);
}
@@ -0,0 +1,75 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionType;
import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.List;
import java.util.Locale;
public final class DimensionStage implements CompileStage {
private static final String DEFAULT_ROCK = "minecraft:stone";
private static final String DEFAULT_FLUID = "minecraft:water";
private static final float FULLBRIGHT_AMBIENT_LIGHT = 1.0f;
@Override
public String name() {
return "dimension";
}
@Override
public void apply(PackCompileContext context) {
IrisDimension dimension = context.dimension();
int minY = dimension.getMinHeight();
DimensionDef aligned = DimensionDef.aligned(
VanillaResourceIdentifier.sanitizePath(dimension.getLoadKey()),
minY,
dimension.getMaxHeight() - minY,
dimension.getLogicalHeight(),
dimension.getFluidHeight() + minY,
paletteBlock(dimension.getRockPalette(), DEFAULT_ROCK, "rock"),
paletteBlock(dimension.getFluidPalette(), DEFAULT_FLUID, "fluid"),
new JsonObject(),
context.diagnostics());
IrisDimensionType type = new IrisDimensionType(
dimension.getBaseDimension(),
typeOptions(dimension),
aligned.logicalHeight(),
aligned.height(),
aligned.minY());
JsonObject typeJson = JsonParser.parseString(type.toJson(context.capabilities().target().get())).getAsJsonObject();
context.setDimensionDef(new DimensionDef(
aligned.name(),
aligned.minY(),
aligned.height(),
aligned.logicalHeight(),
aligned.fluidHeight(),
aligned.defaultBlock(),
aligned.defaultFluid(),
typeJson));
}
private static IrisDimensionTypeOptions typeOptions(IrisDimension dimension) {
IrisDimensionTypeOptions options = dimension.getDimensionOptions();
return dimension.isFullbright() ? options.copy().ambientLight(FULLBRIGHT_AMBIENT_LIGHT) : options;
}
private static String paletteBlock(IrisMaterialPalette palette, String fallback, String role) {
List<IrisBlockData> entries = palette == null ? null : palette.getPalette();
if (entries == null || entries.isEmpty() || entries.getFirst() == null) {
return fallback;
}
String raw = entries.getFirst().getBlock();
String block = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT);
int properties = block.indexOf('[');
String id = properties < 0 ? block : block.substring(0, properties);
String namespaced = id.indexOf(':') < 0 ? "minecraft:" + id : id;
return VanillaResourceIdentifier.requireIdentifier(namespaced, "dimension " + role + " block");
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class ExportedBiomes {
private final SortedMap<String, IrisBiome> byId;
private final Map<String, String> loadKeys;
private final Map<String, Integer> codeOrder;
private final Map<String, IrisRegion> regionByLoadKey;
private ExportedBiomes(SortedMap<String, IrisBiome> byId, Map<String, String> loadKeys, Map<String, Integer> codeOrder,
Map<String, IrisRegion> regionByLoadKey) {
this.byId = Collections.unmodifiableSortedMap(byId);
this.loadKeys = Map.copyOf(loadKeys);
this.codeOrder = Map.copyOf(codeOrder);
this.regionByLoadKey = Map.copyOf(regionByLoadKey);
}
public static ExportedBiomes load(PackCompileContext context) {
SortedMap<String, IrisBiome> byId = new TreeMap<>();
Map<String, String> loadKeys = new HashMap<>();
for (Map.Entry<String, String> entry : context.biomeCodes().loadKeyByBiomeId().entrySet()) {
IrisBiome biome = Objects.requireNonNull(context.data().getBiomeLoader().load(entry.getValue(), false),
"Biome '" + entry.getValue() + "' could not be loaded");
byId.put(entry.getKey(), biome);
loadKeys.put(entry.getKey(), entry.getValue());
}
return new ExportedBiomes(byId, loadKeys, codeOrder(context.biomeCodes().layout(), byId.keySet()), regions(context));
}
public SortedMap<String, IrisBiome> byId() {
return byId;
}
public SortedSet<String> ids() {
return new TreeSet<>(byId.keySet());
}
public SortedSet<String> idsFor(Collection<String> loadKeys) {
SortedSet<String> ids = new TreeSet<>();
for (Map.Entry<String, String> entry : this.loadKeys.entrySet()) {
if (loadKeys.contains(entry.getValue())) {
ids.add(entry.getKey());
}
}
return ids;
}
public String loadKey(String biomeId) {
return loadKeys.get(biomeId);
}
public String resource(String biomeId) {
return "biomes/" + loadKeys.get(biomeId);
}
public IrisRegion region(String biomeId) {
return regionByLoadKey.get(loadKeys.get(biomeId));
}
public List<String> inCodeOrder(Iterable<String> biomeIds) {
List<String> ordered = new ArrayList<>();
for (String biomeId : biomeIds) {
ordered.add(biomeId);
}
ordered.sort(Comparator.comparingInt((String id) -> codeOrder.getOrDefault(id, Integer.MAX_VALUE)).thenComparing(Comparator.naturalOrder()));
return ordered;
}
private static Map<String, Integer> codeOrder(ClimateLayout layout, Iterable<String> biomeIds) {
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>(layout.entries());
entries.sort(Comparator.comparingInt((ClimateLayout.ClimateEntry entry) -> entry.axis().ordinal())
.thenComparingDouble(ClimateLayout.ClimateEntry::min)
.thenComparing(ClimateLayout.ClimateEntry::biomeId));
Map<String, Integer> order = new LinkedHashMap<>();
for (ClimateLayout.ClimateEntry entry : entries) {
order.putIfAbsent(entry.biomeId(), order.size());
}
for (String biomeId : biomeIds) {
order.putIfAbsent(biomeId, order.size());
}
return order;
}
private static Map<String, IrisRegion> regions(PackCompileContext context) {
Map<String, IrisRegion> regions = new HashMap<>();
for (IrisRegion region : context.dimension().getAllRegions(() -> context.data())) {
if (region == null) {
continue;
}
for (IrisBiome biome : region.getAllBiomes(() -> context.data())) {
if (biome != null && biome.getLoadKey() != null) {
regions.putIfAbsent(biome.getLoadKey(), region);
}
}
}
return regions;
}
}
@@ -0,0 +1,117 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class ImageMapStage implements CompileStage {
private static final String DIMENSION_MESSAGE = "Image maps are not exported; the dimension generates from its procedural layers";
private static final String STYLE_MESSAGE = "Generator style image maps are not exported; the base noise style is used";
@Override
public String name() {
return "image-maps";
}
@Override
public void apply(PackCompileContext context) {
IrisDimension dimension = context.dimension();
IrisData data = context.data();
Gson gson = data.getGson();
if (usesImageMaps(gson.toJsonTree(dimension))) {
report(context, "dimensions/" + dimension.getLoadKey(), DIMENSION_MESSAGE);
}
dimension.setImageMaps(new KList<>());
SortedMap<String, IrisRegion> regions = new TreeMap<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
regions.put(region.getLoadKey(), region);
}
reportStyles(context, gson, "regions/", regions);
SortedMap<String, IrisBiome> biomes = new TreeMap<>();
SortedSet<String> generatorKeys = new TreeSet<>();
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
biomes.put(biome.getLoadKey(), biome);
collectGenerators(biome.getGenerators(), generatorKeys);
}
reportStyles(context, gson, "biomes/", biomes);
SortedMap<String, IrisGenerator> generators = new TreeMap<>();
for (String key : generatorKeys) {
IrisGenerator generator = data.getGeneratorLoader().load(key, false);
if (generator != null) {
generators.put(key, generator);
}
}
reportStyles(context, gson, "generators/", generators);
}
private static void collectGenerators(List<IrisBiomeGeneratorLink> links, SortedSet<String> generatorKeys) {
if (links == null) {
return;
}
for (IrisBiomeGeneratorLink link : links) {
if (link != null && link.getGenerator() != null && !link.getGenerator().isBlank()) {
generatorKeys.add(link.getGenerator().trim());
}
}
}
private static void reportStyles(PackCompileContext context, Gson gson, String folder, SortedMap<String, ?> resources) {
for (Map.Entry<String, ?> entry : resources.entrySet()) {
if (usesImageMaps(gson.toJsonTree(entry.getValue()))) {
report(context, folder + entry.getKey(), STYLE_MESSAGE);
}
}
}
private static void report(PackCompileContext context, String resource, String message) {
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.IMAGE_MAP, resource, message));
}
private static boolean usesImageMaps(JsonElement element) {
if (element.isJsonArray()) {
for (JsonElement child : element.getAsJsonArray()) {
if (usesImageMaps(child)) {
return true;
}
}
return false;
}
if (!element.isJsonObject()) {
return false;
}
JsonObject object = element.getAsJsonObject();
if (namesImageMap(object.get("imageMap")) || bindsImageMaps(object.get("imageMaps"))) {
return true;
}
for (Map.Entry<String, JsonElement> member : object.entrySet()) {
if (usesImageMaps(member.getValue())) {
return true;
}
}
return false;
}
private static boolean namesImageMap(JsonElement value) {
return value != null && value.isJsonPrimitive() && !value.getAsString().isBlank();
}
private static boolean bindsImageMaps(JsonElement value) {
return value != null && value.isJsonArray() && !value.getAsJsonArray().isEmpty();
}
}
@@ -0,0 +1,390 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeDecision;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeIdSanitizer;
import art.arcane.iris.pack.datapack.export.compile.decoration.BiomeFeatureLists;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.BinaryResource;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.validation.ContentGate;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public final class PackCompileContext {
private static final Field NEUTRAL_VOLUME = new Field.Const(0.0);
private static final Field NO_CAVES = new Field.Const(1.0e6);
private static final Field ZERO_CODE = new Field.Const(0.0);
private static final double CLIMATE_SPAN = 1.0;
private static final List<String> LEGACY_BIOME_KEYS = List.of("surface_builder", "depth", "scale", "precipitation", "category", "starts");
private static final StyleCompiler NO_STYLE_COMPILER = (style, seedSalt, registry, capabilities) -> {
throw new IllegalStateException("No style compiler installed");
};
private final IrisData data;
private final IrisDimension dimension;
private final ExportCapabilities capabilities;
private final String namespace;
private final WorldgenExportMode mode;
private final boolean objects;
private final StyleCompiler styles;
private final NoiseRegistry noises;
private final List<ExportDiagnostic> diagnostics;
private final BiomeFeatureLists features = new BiomeFeatureLists();
private final List<JsonResource> jsonResources = new ArrayList<>();
private final List<BinaryResource> binaryResources = new ArrayList<>();
private DimensionDef dimensionDef;
private BiomeCodes biomeCodes;
private BiomeDecision biomeDecision;
private BiomeCodes boundBiomeCodes;
private Field boundHeight;
private List<BiomeDef> biomes;
private Field height;
private Field carvedHeight;
private Field climateHeight;
private Field surfaceEstimate;
private Field volumetric;
private Field caves;
private Field.Shared caveField;
private double wallShellBand;
private Field.Shared deepCaves;
private AquiferDef aquifer;
private MaterialProgram material;
private DefaultBiome defaultBiome;
private PackCompileContext(Builder builder) {
data = Objects.requireNonNull(builder.data, "Pack compile context requires pack data");
dimension = Objects.requireNonNull(builder.dimension, "Pack compile context requires a dimension");
capabilities = Objects.requireNonNull(builder.capabilities, "Pack compile context requires export capabilities");
namespace = VanillaResourceIdentifier.requireNamespace(builder.namespace, "export namespace");
mode = Objects.requireNonNull(builder.mode, "Pack compile context requires an export mode");
objects = builder.objects;
styles = builder.styles;
diagnostics = builder.diagnostics;
noises = new NoiseRegistry(namespace);
}
public static Builder builder() {
return new Builder();
}
public IrisData data() {
return data;
}
public IrisDimension dimension() {
return dimension;
}
public ExportCapabilities capabilities() {
return capabilities;
}
public String namespace() {
return namespace;
}
public WorldgenExportMode mode() {
return mode;
}
public boolean objects() {
return objects;
}
public StyleCompiler styles() {
return styles;
}
public NoiseRegistry noises() {
return noises;
}
public List<ExportDiagnostic> diagnostics() {
return diagnostics;
}
public BiomeFeatureLists features() {
return features;
}
public List<JsonResource> jsonResources() {
return jsonResources;
}
public List<BinaryResource> binaryResources() {
return binaryResources;
}
public DimensionDef dimensionDef() {
if (dimensionDef == null) {
throw new IllegalStateException("Dimension definition for '" + dimension.getLoadKey() + "' is read before the dimension stage ran");
}
return dimensionDef;
}
public void setDimensionDef(DimensionDef value) {
dimensionDef = Objects.requireNonNull(value);
}
public BiomeCodes biomeCodes() {
if (biomeDecision != null) {
return height == null ? biomeDecision.base() : heightBoundBiomeCodes();
}
return biomeCodes == null ? defaultBiome().codes() : biomeCodes;
}
public BiomeCodes baseBiomeCodes() {
if (biomeDecision != null) {
return biomeDecision.base();
}
return biomeCodes == null ? defaultBiome().codes() : biomeCodes;
}
public void setBiomeCodes(BiomeCodes value) {
biomeCodes = Objects.requireNonNull(value);
biomeDecision = null;
}
public void setBiomeDecision(BiomeDecision value) {
biomeDecision = Objects.requireNonNull(value);
biomeCodes = null;
boundBiomeCodes = null;
boundHeight = null;
}
public List<BiomeDef> biomes() {
return biomes == null ? defaultBiome().biomes() : biomes;
}
public void setBiomes(List<BiomeDef> value) {
biomes = List.copyOf(value);
}
public Field height() {
return carvedHeight == null ? naturalHeight() : carvedHeight;
}
public Field naturalHeight() {
return height == null ? new Field.Const(dimensionDef().fluidHeight()) : height;
}
public void setHeight(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "height");
height = value;
carvedHeight = null;
climateHeight = null;
surfaceEstimate = null;
}
public Field climateHeight() {
return climateHeight == null ? naturalHeight() : climateHeight;
}
public void setClimateHeight(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "climate height");
climateHeight = value;
}
public void setCarvedHeight(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "carved height");
carvedHeight = value;
}
public Field surfaceEstimate() {
return surfaceEstimate == null ? climateHeight() : surfaceEstimate;
}
public void setSurfaceEstimate(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "surface estimate");
surfaceEstimate = value;
}
public Field volumetric() {
return volumetric == null ? NEUTRAL_VOLUME : volumetric;
}
public void setVolumetric(Field value) {
volumetric = Objects.requireNonNull(value);
}
public Field caves() {
return caves == null ? NO_CAVES : caves;
}
public void setCaves(Field value) {
caves = Objects.requireNonNull(value);
}
public Optional<Field.Shared> caveField() {
return Optional.ofNullable(caveField);
}
public void setCaveField(Field.Shared value, double wallBand) {
if (!(wallBand >= 0.0) || Double.isInfinite(wallBand)) {
throw new IllegalArgumentException("Wall shell band must be finite and non-negative, got " + wallBand);
}
caveField = Objects.requireNonNull(value);
wallShellBand = wallBand;
}
public double wallShellBand() {
return wallShellBand;
}
public Optional<Field.Shared> deepCaves() {
return Optional.ofNullable(deepCaves);
}
public void setDeepCaves(Field.Shared value) {
deepCaves = Objects.requireNonNull(value);
}
public AquiferDef aquifer() {
return aquifer == null ? AquiferDef.dry(surfaceEstimate()) : aquifer;
}
public void setAquifer(AquiferDef value) {
aquifer = Objects.requireNonNull(value);
}
public MaterialProgram material() {
return material == null ? MaterialProgram.rock(dimensionDef().defaultBlock()) : material;
}
public void setMaterial(MaterialProgram value) {
material = Objects.requireNonNull(value);
}
private BiomeCodes heightBoundBiomeCodes() {
Field bindingHeight = climateHeight();
if (boundHeight != bindingHeight) {
boundBiomeCodes = biomeDecision.bind(bindingHeight);
boundHeight = bindingHeight;
}
return boundBiomeCodes;
}
private DefaultBiome defaultBiome() {
if (defaultBiome == null) {
defaultBiome = createDefaultBiome();
}
return defaultBiome;
}
private DefaultBiome createDefaultBiome() {
String loadKey = firstLandBiome();
String id = BiomeIdSanitizer.sanitize(namespace, loadKey, new HashSet<>());
String body = new IrisBiomeCustom().generateJson(capabilities.target().get(), new ContentGate(null, Map.of(), null));
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
for (String key : LEGACY_BIOME_KEYS) {
json.remove(key);
}
BiomeDef biome = new BiomeDef(id.substring(namespace.length() + 1), json);
ClimateLayout layout = new ClimateLayout(List.of(
new ClimateLayout.ClimateEntry(id, ClimateLayout.ClimateAxis.TEMPERATURE, -CLIMATE_SPAN, CLIMATE_SPAN)));
BiomeCodes codes = new BiomeCodes(ZERO_CODE, ZERO_CODE, ZERO_CODE, layout, Map.of(id, loadKey));
return new DefaultBiome(List.of(biome), codes);
}
private String firstLandBiome() {
for (String regionKey : dimension.getRegions()) {
IrisRegion region = data.getRegionLoader().load(regionKey, false);
if (region == null || region.isCompatExcluded()) {
continue;
}
for (String biomeKey : region.getLandBiomes()) {
IrisBiome biome = biomeKey == null ? null : data.getBiomeLoader().load(biomeKey, false);
if (biome != null && !biome.isCompatExcluded()) {
return biome.getLoadKey();
}
}
}
throw new IllegalStateException("Dimension '" + dimension.getLoadKey() + "' has no exportable land biome in any of its regions");
}
private record DefaultBiome(List<BiomeDef> biomes, BiomeCodes codes) {
}
public static final class Builder {
private IrisData data;
private IrisDimension dimension;
private ExportCapabilities capabilities;
private String namespace;
private WorldgenExportMode mode = WorldgenExportMode.OVERWORLD;
private boolean objects = true;
private StyleCompiler styles = NO_STYLE_COMPILER;
private List<ExportDiagnostic> diagnostics = new ArrayList<>();
private Builder() {
}
public Builder data(IrisData value) {
data = Objects.requireNonNull(value);
return this;
}
public Builder dimension(IrisDimension value) {
dimension = Objects.requireNonNull(value);
return this;
}
public Builder capabilities(ExportCapabilities value) {
capabilities = Objects.requireNonNull(value);
return this;
}
public Builder namespace(String value) {
namespace = Objects.requireNonNull(value);
return this;
}
public Builder mode(WorldgenExportMode value) {
mode = Objects.requireNonNull(value);
return this;
}
public Builder objects(boolean value) {
objects = value;
return this;
}
public Builder styles(StyleCompiler value) {
styles = Objects.requireNonNull(value);
return this;
}
public Builder diagnostics(List<ExportDiagnostic> value) {
diagnostics = Objects.requireNonNull(value);
return this;
}
public PackCompileContext build() {
return new PackCompileContext(this);
}
}
}
@@ -0,0 +1,73 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeStage;
import art.arcane.iris.pack.datapack.export.compile.cave.CaveStage;
import art.arcane.iris.pack.datapack.export.compile.decoration.DecorationStage;
import art.arcane.iris.pack.datapack.export.compile.object.ObjectStage;
import art.arcane.iris.pack.datapack.export.compile.structure.StructureStage;
import art.arcane.iris.pack.datapack.export.compile.subterrain.SubterrainStage;
import art.arcane.iris.pack.datapack.export.compile.surface.SurfaceStage;
import art.arcane.iris.pack.datapack.export.compile.terrain.RiverStage;
import art.arcane.iris.pack.datapack.export.compile.terrain.TerrainStage;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
import java.util.List;
public final class PackCompiler {
private static final String ROUTER_STAGE = "router";
private final List<CompileStage> stages;
public PackCompiler(List<CompileStage> stages) {
this.stages = List.copyOf(stages);
}
public static PackCompiler create() {
return new PackCompiler(List.of(
new DimensionStage(),
new ImageMapStage(),
new BiomeStage(),
new TerrainStage(),
new RiverStage(),
new CaveStage(),
new SurfaceStage(),
new DecorationStage(),
new StructureStage(),
new SubterrainStage(),
new ObjectStage()));
}
public ExportIr compile(PackCompileContext context, WorldgenExportProgress progress) {
for (CompileStage stage : stages) {
progress.stage(stage.name());
stage.apply(context);
}
progress.stage(ROUTER_STAGE);
DimensionDef dimension = context.dimensionDef();
BiomeCodes codes = context.biomeCodes();
RouterDef router = RouterCompiler.compile(
context.height(),
context.volumetric(),
context.caves(),
context.aquifer(),
codes,
dimension);
return new ExportIr(
context.namespace(),
context.capabilities().target(),
context.mode(),
dimension,
context.noises().definitions(),
router,
codes.layout(),
context.features().apply(context.biomes(), context.namespace()),
context.material(),
context.diagnostics(),
context.jsonResources(),
context.binaryResources());
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
public final class RouterCompiler {
private static final Field DENSITY_PER_BLOCK = new Field.Const(1.0);
private static final Field UNUSED_CLIMATE = new Field.Const(0.0);
private static final double HALF_BLOCK = 0.5;
private static final int VOLUME_CELL_XZ = 4;
private static final int VOLUME_CELL_Y = 8;
private static final int SURFACE_CELL_XZ = 16;
private static final int SURFACE_CELL_Y = 1;
private RouterCompiler() {
}
public static RouterDef compile(Field height, Field volumetric, Field caves, AquiferDef aquifer, BiomeCodes codes, DimensionDef dimension) {
FieldValidator.requireTwoDimensional(height, "height");
Field ramp = new Field.Binary(Field.BinaryOp.MUL, DENSITY_PER_BLOCK, new Field.Binary(Field.BinaryOp.SUB, height, blockCenterY(dimension)));
Field terrain = new Field.Binary(Field.BinaryOp.ADD, ramp, new Field.Grid(volumetric, VOLUME_CELL_XZ, VOLUME_CELL_Y));
return new RouterDef(
codes.surfaceCode(),
codes.caveCode(),
UNUSED_CLIMATE,
UNUSED_CLIMATE,
codes.class3d(),
UNUSED_CLIMATE,
new Field.Grid(height, SURFACE_CELL_XZ, SURFACE_CELL_Y),
new Field.Binary(Field.BinaryOp.MIN, terrain, caves),
aquifer);
}
private static Field blockCenterY(DimensionDef dimension) {
double bottom = dimension.minY();
double top = dimension.minY() + dimension.height();
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, bottom, top, bottom - HALF_BLOCK, top - HALF_BLOCK);
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import java.util.Objects;
public record WeightedState(VanillaBlockState state, int weight) {
public WeightedState {
Objects.requireNonNull(state);
if (weight < 1) {
throw new IllegalArgumentException("Block state weight must be positive, got " + weight + " for " + state.canonical());
}
}
}
@@ -0,0 +1,76 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
public final class WorldgenSyntax {
private final DataVersion target;
private WorldgenSyntax(DataVersion target) {
this.target = target;
}
public static WorldgenSyntax forTarget(DataVersion target) {
if (!WorldgenExportTargets.SUPPORTED.contains(target)) {
throw WorldgenExportTargets.unsupported(target.getVersion());
}
return new WorldgenSyntax(target);
}
public DataVersion target() {
return target;
}
public String featureFolder() {
return "worldgen/feature";
}
public JsonObject blockState(VanillaBlockState state) {
JsonObject json = new JsonObject();
json.addProperty("id", state.name());
if (!state.properties().isEmpty()) {
JsonObject properties = new JsonObject();
for (Map.Entry<String, String> property : state.properties().entrySet()) {
properties.addProperty(property.getKey(), property.getValue());
}
json.add("properties", properties);
}
return json;
}
public JsonObject provider(List<WeightedState> entries) {
if (entries.isEmpty()) {
throw new IllegalArgumentException("A block state provider needs at least one state");
}
if (entries.size() == 1) {
return blockState(entries.getFirst().state());
}
JsonArray weighted = new JsonArray(entries.size());
for (WeightedState entry : entries) {
JsonObject item = new JsonObject();
item.add("data", blockState(entry.state()));
item.addProperty("weight", entry.weight());
weighted.add(item);
}
JsonObject provider = new JsonObject();
provider.addProperty("type", "minecraft:weighted");
provider.add("entries", weighted);
return provider;
}
public JsonObject feature(String type, JsonObject config) {
JsonObject feature = new JsonObject();
feature.addProperty("type", type);
for (Map.Entry<String, JsonElement> field : config.entrySet()) {
feature.add(field.getKey(), field.getValue().deepCopy());
}
return feature;
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
public record BiomeCodes(Field surfaceCode, Field caveCode, Field class3d, ClimateLayout layout, Map<String, String> loadKeyByBiomeId) {
public static final String BASE_CODE = "biome/base_code";
public BiomeCodes {
Objects.requireNonNull(surfaceCode);
Objects.requireNonNull(caveCode);
Objects.requireNonNull(class3d);
Objects.requireNonNull(layout);
FieldValidator.requireTwoDimensional(surfaceCode, "surface code");
FieldValidator.requireTwoDimensional(caveCode, "cave code");
SortedMap<String, String> loadKeys = new TreeMap<>(loadKeyByBiomeId);
for (Map.Entry<String, String> entry : loadKeys.entrySet()) {
VanillaResourceIdentifier.requireIdentifier(entry.getKey(), "biome id");
Objects.requireNonNull(entry.getValue());
}
loadKeyByBiomeId = Collections.unmodifiableSortedMap(loadKeys);
}
}
@@ -0,0 +1,141 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class BiomeDecision {
static final int LAND = 0;
static final int SEA = 1;
static final int SHORE = 2;
static final int SURFACE_SLOT = 0;
private static final double UNDERGROUND_DEPTH = 8.0;
private static final int BRIDGE_WEIGHT = 8;
private static final int LEVEL_WEIGHT = 2;
private static final int ZONE_WEIGHT = 4;
private static final int DEPTH_WEIGHT = 2;
private static final Field UNUSED_CODE = new Field.Const(0.0);
private final BiomeCodes base;
private final Parts parts;
BiomeDecision(BiomeCodes base, Parts parts) {
this.base = Objects.requireNonNull(base);
this.parts = Objects.requireNonNull(parts);
}
public BiomeCodes base() {
return base;
}
public BiomeCodes bind(Field height) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(height), "height");
return new BiomeCodes(surfaceCode(height), base.caveCode(), class3d(height), base.layout(), base.loadKeyByBiomeId());
}
private Field surfaceCode(Field height) {
double fluid = parts.fluid();
double fluidTop = Math.nextUp((float) fluid);
IndexedField levels = IndexedField.partition(height, List.of(fluid - 1.0, fluid, fluidTop), List.of(0, LEVEL_WEIGHT, 2 * LEVEL_WEIGHT, 3 * LEVEL_WEIGHT));
IndexedField above = IndexedField.partition(new Field.Binary(Field.BinaryOp.SUB, height, parts.shoreHeight()), List.of(fluidTop), List.of(0, 1));
IndexedField key = IndexedField.sum(List.of(parts.bridge().remap(bridge -> bridge * BRIDGE_WEIGHT), levels, above));
IndexedField list = key.remap(BiomeDecision::listFor);
List<Field> codes = new ArrayList<>(parts.chains().size());
for (IndexedField chain : parts.chains()) {
codes.add(chain.lookup(index -> parts.surfaceCodes()[index]));
}
if (list.isConstant()) {
return codes.get(list.values().getFirst());
}
List<Field> cases = new ArrayList<>(list.values().getLast() + 1);
for (int value = 0; value <= list.values().getLast(); value++) {
cases.add(list.values().contains(value) ? codes.get(value) : UNUSED_CODE);
}
return new Field.Mux(list.field(), cases);
}
private Field class3d(Field height) {
if (parts.caveSlot().equals(IndexedField.constant(SURFACE_SLOT)) && parts.carvingSlots().isEmpty()) {
return new Field.Const(0.0);
}
IndexedField zones = parts.zoneKeys();
Field caveExtra = new Field.Binary(Field.BinaryOp.SUB, parts.requiredDepth(), new Field.Const(UNDERGROUND_DEPTH));
Field caveZone = zones.lookup(key -> key / ZONE_WEIGHT == CarvingZones.CAVE_ZONE ? 1.0 : 0.0);
Field depthGate = new Field.Binary(Field.BinaryOp.ADD, new Field.Const(UNDERGROUND_DEPTH), new Field.Gate(caveZone, caveExtra));
Field clearance = new Field.Binary(Field.BinaryOp.SUB, new Field.Binary(Field.BinaryOp.SUB, height, parts.y()), depthGate);
IndexedField deepEnough = IndexedField.partition(clearance, List.of(0.0), List.of(0, 1));
IndexedField key = IndexedField.sum(List.of(zones.remap(zone -> zone / ZONE_WEIGHT * DEPTH_WEIGHT), deepEnough));
IndexedField classIndex = key.remap(BiomeDecision::classFor);
List<IndexedField> cases = new ArrayList<>(parts.carvingSlots().size() + 2);
cases.add(IndexedField.constant(SURFACE_SLOT));
cases.add(parts.caveSlot());
cases.addAll(parts.carvingSlots());
IndexedField slot = IndexedField.select(classIndex, cases);
return slot.lookup(value -> value == SURFACE_SLOT ? 0.0 : parts.undergroundCodes()[value - 1]);
}
static int listFor(int key) {
boolean landBase = key / BRIDGE_WEIGHT == LAND;
int level = (key % BRIDGE_WEIGHT) / LEVEL_WEIGHT;
boolean aboveShoreBand = key % LEVEL_WEIGHT == 1;
boolean nearFluid = level >= 1;
boolean belowFluid = level <= 1;
boolean atFluid = level == 2;
int base = landBase ? LAND : SEA;
if (nearFluid) {
if (!aboveShoreBand) {
return SHORE;
}
if (!landBase) {
return LAND;
}
} else if (!landBase && aboveShoreBand) {
return LAND;
}
if (belowFluid && landBase) {
return SEA;
}
if (atFluid) {
return SHORE;
}
return base;
}
static int classFor(int key) {
int zone = key / DEPTH_WEIGHT;
boolean deepEnough = key % DEPTH_WEIGHT == 1;
if (zone == CarvingZones.SHALLOW_ZONE || !deepEnough) {
return 0;
}
return zone == CarvingZones.CAVE_ZONE ? 1 : zone;
}
record Parts(
double fluid,
IndexedField bridge,
Field shoreHeight,
List<IndexedField> chains,
double[] surfaceCodes,
Field y,
IndexedField zoneKeys,
IndexedField caveSlot,
Field requiredDepth,
List<IndexedField> carvingSlots,
double[] undergroundCodes) {
Parts {
Objects.requireNonNull(bridge);
Objects.requireNonNull(shoreHeight);
chains = List.copyOf(chains);
Objects.requireNonNull(surfaceCodes);
Objects.requireNonNull(y);
Objects.requireNonNull(zoneKeys);
Objects.requireNonNull(caveSlot);
Objects.requireNonNull(requiredDepth);
carvingSlots = List.copyOf(carvingSlots);
Objects.requireNonNull(undergroundCodes);
}
}
}
@@ -0,0 +1,299 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.loading.IrisData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
public final class BiomeDecisionCompiler {
private static final long REGION_SALT = 883L;
private static final long CONTINENT_SALT = 234_234_565L;
private static final long PICK_SALT = 1_000L;
private static final long CHILD_SALT = 123L;
private static final long SHORE_SALT = 3_458_612L;
private static final double SURFACE_SPAN = 1.0;
private static final double UNDERGROUND_MIN = 1.25;
private static final double UNDERGROUND_MAX = 2.0;
private static final double MINIMUM_CAVE_DEPTH = 8.0;
private static final double NO_CAVE_DEPTH = 100_000.0;
private static final int DEEP_FLOOR_Y = -8;
private static final int DEEP_ABOVE_MIN_Y = 40;
private final StyleCompiler styles;
public BiomeDecisionCompiler(StyleCompiler styles) {
this.styles = Objects.requireNonNull(styles);
}
public BiomeDecision compile(PackCompileContext context) {
IrisDimension dimension = context.dimension();
IrisData data = context.data();
DimensionDef dimensionDef = context.dimensionDef();
BiomePools pools = BiomePools.load(dimension, data);
CarvingZones carving = CarvingZones.plan(dimension, data, Math.max(DEEP_FLOOR_Y, dimensionDef.minY() + DEEP_ABOVE_MIN_Y));
List<String> surfaceKeys = List.copyOf(pools.surface());
SortedSet<String> undergroundSet = new TreeSet<>(pools.cave());
undergroundSet.addAll(carving.biomes());
List<String> undergroundKeys = List.copyOf(undergroundSet);
Map<String, Integer> surfaceIndex = indexOf(surfaceKeys);
Map<String, Integer> undergroundIndex = indexOf(undergroundKeys);
Map<String, String> ids = biomeIds(context.namespace(), surfaceKeys, undergroundKeys);
double[] surfaceCodes = codes(-SURFACE_SPAN, SURFACE_SPAN, surfaceKeys.size());
double[] undergroundCodes = codes(UNDERGROUND_MIN, UNDERGROUND_MAX, undergroundKeys.size());
Build build = new Build(pools, surfaceKeys, surfaceIndex,
new SelectorCompiler(styles.selectors(), new SelectorCompiler.Sources(data, context.noises(), context.capabilities())));
IndexedField region = build.region(dimension);
IndexedField bridge = build.selectors.scaled(dimension.getContinentalStyle(), 1.0 / dimension.getContinentZoom(), CONTINENT_SALT)
.partition(List.of(dimension.getLandChance()), List.of(BiomeDecision.LAND, BiomeDecision.SEA))
.shared("biome/bridge");
RegionPicks picks = build.regionPicks(dimension, bridge, undergroundIndex, context.diagnostics());
List<IndexedField> chains = List.of(
build.implode("biome/land", IndexedField.select(region, picks.land()).shared("biome/land/pick")),
build.implode("biome/sea", IndexedField.select(region, picks.sea()).shared("biome/sea/pick")),
build.implode("biome/shore", IndexedField.select(region, picks.shore()).shared("biome/shore/pick")));
IndexedField baseSurface = IndexedField.select(bridge, chains.subList(0, 2));
IndexedField caveSlot = IndexedField.select(region, picks.caves()).shared("biome/cave");
Field caveCode = caveSlot.lookup(value -> value == BiomeDecision.SURFACE_SLOT ? 0.0 : undergroundCodes[value - 1]);
Field requiredDepth = caveSlot.lookup(value -> value == BiomeDecision.SURFACE_SLOT
? NO_CAVE_DEPTH
: Math.max(MINIMUM_CAVE_DEPTH, pools.biome(undergroundKeys.get(value - 1)).getCaveMinDepthBelowSurface()));
Field y = blockY(dimensionDef);
List<IndexedField> carvingSlots = carving.slots(build.selectors, data, key -> undergroundIndex.get(key) + 1);
BiomeCodes base = new BiomeCodes(
new Field.Shared(BiomeCodes.BASE_CODE, baseSurface.lookup(index -> surfaceCodes[index])),
caveCode,
new Field.Const(0.0),
layout(ids, surfaceKeys, surfaceCodes, undergroundKeys, undergroundCodes),
loadKeys(ids));
BiomeDecision.Parts parts = new BiomeDecision.Parts(
dimensionDef.fluidHeight(),
bridge,
shoreHeight(region, picks.shoreHeights()),
chains,
surfaceCodes,
y,
carving.zoneKeys(y),
caveSlot,
requiredDepth,
carvingSlots,
undergroundCodes);
return new BiomeDecision(base, parts);
}
private static Field shoreHeight(IndexedField region, List<Field> heights) {
if (region.isConstant()) {
return heights.get(region.values().getFirst());
}
if (new HashSet<>(heights).size() == 1) {
return heights.getFirst();
}
return new Field.Shared("biome/shore_height", new Field.Mux(region.field(), heights));
}
private static SortedMap<String, String> loadKeys(Map<String, String> ids) {
SortedMap<String, String> loadKeys = new TreeMap<>();
for (Map.Entry<String, String> entry : ids.entrySet()) {
loadKeys.put(entry.getValue(), entry.getKey());
}
return loadKeys;
}
private static Map<String, Integer> indexOf(List<String> keys) {
Map<String, Integer> index = new HashMap<>(keys.size() * 2);
for (int position = 0; position < keys.size(); position++) {
index.put(keys.get(position), position);
}
return index;
}
private static Map<String, String> biomeIds(String namespace, List<String> surfaceKeys, List<String> undergroundKeys) {
SortedSet<String> all = new TreeSet<>(surfaceKeys);
all.addAll(undergroundKeys);
Set<String> taken = new HashSet<>();
Map<String, String> ids = new TreeMap<>();
for (String key : all) {
ids.put(key, BiomeIdSanitizer.sanitize(namespace, key, taken));
}
return ids;
}
private static double[] codes(double min, double max, int count) {
double[] codes = new double[count];
double half = (max - min) / (2.0 * Math.max(1, count));
for (int index = 0; index < count; index++) {
codes[index] = min + (2 * index + 1) * half;
}
return codes;
}
private static ClimateLayout layout(Map<String, String> ids, List<String> surfaceKeys, double[] surfaceCodes, List<String> undergroundKeys, double[] undergroundCodes) {
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>(surfaceKeys.size() + undergroundKeys.size());
double surfaceHalf = SURFACE_SPAN / Math.max(1, surfaceKeys.size());
for (int index = 0; index < surfaceKeys.size(); index++) {
entries.add(new ClimateLayout.ClimateEntry(ids.get(surfaceKeys.get(index)), ClimateLayout.ClimateAxis.TEMPERATURE,
surfaceCodes[index] - surfaceHalf, surfaceCodes[index] + surfaceHalf));
}
double undergroundHalf = (UNDERGROUND_MAX - UNDERGROUND_MIN) / (2.0 * Math.max(1, undergroundKeys.size()));
for (int index = 0; index < undergroundKeys.size(); index++) {
entries.add(new ClimateLayout.ClimateEntry(ids.get(undergroundKeys.get(index)), ClimateLayout.ClimateAxis.DEPTH,
undergroundCodes[index] - undergroundHalf, Math.min(UNDERGROUND_MAX, undergroundCodes[index] + undergroundHalf)));
}
return new ClimateLayout(entries);
}
private static Field blockY(DimensionDef dimension) {
double bottom = dimension.minY();
double top = dimension.minY() + dimension.height();
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, bottom, top, bottom, top);
}
private static final class Build {
private final BiomePools pools;
private final List<String> surfaceKeys;
private final Map<String, Integer> surfaceIndex;
private final SelectorCompiler selectors;
private final Map<String, IndexedField> hops = new HashMap<>();
private Build(BiomePools pools, List<String> surfaceKeys, Map<String, Integer> surfaceIndex, SelectorCompiler selectors) {
this.pools = pools;
this.surfaceKeys = surfaceKeys;
this.surfaceIndex = surfaceIndex;
this.selectors = selectors;
}
private IndexedField region(IrisDimension dimension) {
List<IrisRegion> regions = new ArrayList<>(pools.regions().size());
List<Integer> leaves = new ArrayList<>(pools.regions().size());
for (BiomePools.RegionPools regionPools : pools.regions()) {
leaves.add(regions.size());
regions.add(regionPools.region());
}
if (regions.size() == 1) {
return IndexedField.constant(0);
}
return selectors.zoomed(dimension.getRegionStyle(), dimension.getRegionZoom(), REGION_SALT)
.partition(BiomePools.rarityCuts(regions), leaves)
.shared("biome/region");
}
private RegionPicks regionPicks(IrisDimension dimension, IndexedField bridge, Map<String, Integer> undergroundIndex, List<ExportDiagnostic> diagnostics) {
int count = pools.regions().size();
RegionPicks picks = new RegionPicks(new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count));
for (int position = 0; position < count; position++) {
BiomePools.RegionPools regionPools = pools.regions().get(position);
IrisRegion region = regionPools.region();
IndexedField landPick = pick(dimension, regionPools, InferredType.LAND, surfaceIndex::get, 0);
IndexedField seaPick = pick(dimension, regionPools, InferredType.SEA, surfaceIndex::get, 0);
IndexedField land = landPick == null ? seaPick : landPick;
IndexedField sea = seaPick == null ? landPick : seaPick;
IndexedField shore = pick(dimension, regionPools, InferredType.SHORE, surfaceIndex::get, 0);
IndexedField cave = pick(dimension, regionPools, InferredType.CAVE, undergroundIndex::get, 1);
picks.land().add(land);
picks.sea().add(sea);
picks.shore().add(shore == null ? IndexedField.select(bridge, List.of(land, sea)) : shore);
picks.caves().add(cave == null ? IndexedField.constant(BiomeDecision.SURFACE_SLOT) : cave);
picks.shoreHeights().add(shoreHeight(region, position));
if (region.getShoreMinimumWidth() > 0.0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.SHORE_WIDTH, "regions/" + region.getLoadKey(),
"Shore widening by slope is not exported; the shore band follows the shore height only"));
}
}
return picks;
}
private IndexedField pick(IrisDimension dimension, BiomePools.RegionPools regionPools, InferredType type, Function<String, Integer> index, int offset) {
List<IrisBiome> pool = regionPools.of(type);
if (pool.isEmpty()) {
return null;
}
List<Integer> leaves = new ArrayList<>(pool.size());
for (IrisBiome biome : pool) {
leaves.add(index.apply(biome.getLoadKey()) + offset);
}
if (pool.size() == 1) {
return IndexedField.constant(leaves.getFirst());
}
IrisRegion region = regionPools.region();
double zoom = switch (type) {
case LAND -> dimension.getBiomeZoom() * dimension.getLandZoom() * region.getLandBiomeZoom();
case SEA -> dimension.getBiomeZoom() * dimension.getSeaZoom() * region.getSeaBiomeZoom();
case SHORE -> dimension.getBiomeZoom() * region.getShoreBiomeZoom();
case CAVE -> dimension.getBiomeZoom() * region.getCaveBiomeZoom();
};
IrisGeneratorStyle style = switch (type) {
case LAND -> dimension.getLandBiomeStyle();
case SEA -> dimension.getSeaBiomeStyle();
case SHORE -> dimension.getShoreBiomeStyle();
case CAVE -> dimension.getCaveBiomeStyle();
};
return selectors.zoomed(style, zoom, PICK_SALT + type.ordinal()).partition(BiomePools.rarityCuts(pool), leaves);
}
private Field shoreHeight(IrisRegion region, int position) {
if (region.getShoreHeightMin() == region.getShoreHeightMax()) {
return new Field.Const(region.getShoreHeightMin());
}
return selectors.sampled(NoiseStyle.IRIS.style(), region.getShoreHeightZoom(), SHORE_SALT + position, region::getShoreHeight).curve();
}
private IndexedField implode(String name, IndexedField start) {
IndexedField current = start;
for (int hop = 0; hop < BiomePools.MAX_CHILD_HOPS; hop++) {
List<Integer> movers = new ArrayList<>();
for (int value : current.values()) {
BiomePools.ChildPlan plan = pools.plan(surfaceKeys.get(value));
if (plan != null && plan.moves()) {
movers.add(value);
}
}
if (movers.isEmpty()) {
return current;
}
Map<Integer, IndexedField> moved = new HashMap<>(movers.size() * 2);
for (int mover : movers) {
moved.put(mover, hops.computeIfAbsent(surfaceKeys.get(mover), this::hop));
}
current = IndexedField.advance(current, moved).shared(name + "/hop" + (hop + 1));
}
return current;
}
private IndexedField hop(String parent) {
IrisBiome biome = pools.biome(parent);
BiomePools.ChildPlan plan = pools.plan(parent);
List<Integer> leaves = new ArrayList<>(plan.choiceByIndex().size());
for (String choice : plan.choiceByIndex()) {
leaves.add(surfaceIndex.get(choice));
}
return selectors.scaled(biome.getChildStyle(), biome.getChildShrinkFactor(), CHILD_SALT)
.partition(BiomePools.selectionCuts(plan.choiceByIndex().size()), leaves);
}
}
private record RegionPicks(List<IndexedField> land, List<IndexedField> sea, List<IndexedField> shore, List<IndexedField> caves, List<Field> shoreHeights) {
}
}
@@ -0,0 +1,299 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisVanillaBiomeTags;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.validation.ContentGate;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.VanillaDataSource;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class BiomeDefinitionCompiler {
private static final String VANILLA_NAMESPACE = "minecraft";
private static final String BIOME_FOLDER = "/worldgen/biome/";
private static final String TAG_FOLDER = "/tags/worldgen/biome";
private static final String JSON_SUFFIX = ".json";
private static final String SPAWNS_ATTRIBUTE = "minecraft:gameplay/natural_mob_spawns";
private static final String SPAWN_PROBABILITY_ATTRIBUTE = "minecraft:gameplay/creature_world_gen_spawn_probability";
private static final String SPAWN_PROBABILITY = "creature_spawn_probability";
private static final List<String> CLIMATE_KEYS = List.of("has_precipitation", "temperature", "downfall", "temperature_modifier");
private static final List<String> CUSTOM_KEYS = List.of("has_precipitation", "temperature", "downfall", "temperature_modifier",
"effects", "attributes", "spawners", "spawn_costs", SPAWN_PROBABILITY);
private final Optional<VanillaDataSource> vanilla;
public BiomeDefinitionCompiler(Optional<VanillaDataSource> vanilla) {
this.vanilla = Objects.requireNonNull(vanilla);
}
public static BiomeDefinitionCompiler forTarget(DataVersion target) {
if (!IrisPlatforms.isBound()) {
return new BiomeDefinitionCompiler(Optional.empty());
}
IrisPlatform platform = IrisPlatforms.get();
boolean running = DataVersion.forMinecraftVersion(platform.minecraftVersion()) == target;
return new BiomeDefinitionCompiler(running ? Optional.ofNullable(platform.vanillaData()) : Optional.empty());
}
public Definitions compile(PackCompileContext context, Map<String, String> loadKeyByBiomeId) {
DataVersion target = context.capabilities().target();
ContentGate gate = vanilla.isPresent() ? ContentGate.forData(context.data()) : new ContentGate(null, Map.of(), null);
if (vanilla.isEmpty()) {
diagnose(context, "dimensions/" + context.dimension().getLoadKey(), "Vanilla biome data for Minecraft " + target.getVersion()
+ " is not available on this server; biomes are built from their Iris fields without vanilla music, ambience, spawns or structure tags");
}
Map<String, List<String>> vanillaTags = vanilla.map(BiomeDefinitionCompiler::directTags).orElse(Map.of());
SortedMap<String, SortedSet<String>> members = new TreeMap<>();
List<BiomeDef> biomes = new ArrayList<>(loadKeyByBiomeId.size());
for (Map.Entry<String, String> entry : new TreeMap<>(loadKeyByBiomeId).entrySet()) {
String id = entry.getKey();
IrisBiome biome = Objects.requireNonNull(context.data().getBiomeLoader().load(entry.getValue(), false),
"Biome '" + entry.getValue() + "' could not be loaded");
IrisBiomeCustom custom = custom(context, biome);
String visualKey = biome.getBiomeScatter().isEmpty() ? biome.getDerivativeKey() : namespaced(biome.getBiomeScatter().getFirst());
Optional<JsonObject> base = vanilla.flatMap(source -> read(source, visualKey));
if (vanilla.isPresent() && base.isEmpty()) {
diagnose(context, "biomes/" + biome.getLoadKey(), "Vanilla biome '" + visualKey + "' has no data for Minecraft " + target.getVersion()
+ "; the biome is built from its Iris fields");
}
JsonObject overlay = custom == null ? null : customJson(custom, target, gate);
JsonObject json = base.isPresent()
? merge(base.get(), overlay, custom)
: retained(overlay == null ? customJson(new IrisBiomeCustom(), target, gate) : overlay);
json.add("carvers", new JsonArray());
json.add("features", new JsonArray());
biomes.add(new BiomeDef(id.substring(id.indexOf(':') + 1), json));
for (String tag : tags(biome, custom, vanillaTags)) {
members.computeIfAbsent(tag, ignored -> new TreeSet<>()).add(id);
}
}
List<JsonResource> tags = new ArrayList<>(members.size());
for (Map.Entry<String, SortedSet<String>> tag : members.entrySet()) {
JsonObject json = new JsonObject();
json.addProperty("replace", false);
JsonArray values = new JsonArray(tag.getValue().size());
for (String member : tag.getValue()) {
values.add(member);
}
json.add("values", values);
int separator = tag.getKey().indexOf(':');
tags.add(new JsonResource("data/" + tag.getKey().substring(0, separator) + TAG_FOLDER + "/" + tag.getKey().substring(separator + 1) + JSON_SUFFIX, json));
}
return new Definitions(biomes, tags);
}
private static IrisBiomeCustom custom(PackCompileContext context, IrisBiome biome) {
boolean customs = biome.getCustomDerivitives() != null && biome.getCustomDerivitives().size() > 1;
if (customs || biome.getBiomeScatter().size() > 1) {
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.BIOME_VARIANT, "biomes/" + biome.getLoadKey(),
"Biomes with several custom derivatives or scatter biomes export as one biome using the first entry"));
}
return biome.isCustom() ? biome.getCustomDerivitives().getFirst() : null;
}
private static JsonObject customJson(IrisBiomeCustom custom, DataVersion target, ContentGate gate) {
return JsonParser.parseString(custom.generateJson(target.get(), gate)).getAsJsonObject();
}
private static JsonObject merge(JsonObject base, JsonObject overlay, IrisBiomeCustom custom) {
JsonObject merged = base.deepCopy();
if (overlay == null) {
return merged;
}
for (String key : CLIMATE_KEYS) {
if (overlay.has(key)) {
merged.add(key, overlay.get(key).deepCopy());
}
}
boolean spawnRarity = custom.getSpawnRarity() > 0;
putAll(object(merged, "effects"), overlay.getAsJsonObject("effects"));
JsonObject overlayAttributes = overlay.getAsJsonObject("attributes");
if (overlayAttributes != null) {
JsonObject attributes = object(merged, "attributes");
for (Map.Entry<String, JsonElement> attribute : overlayAttributes.entrySet()) {
switch (attribute.getKey()) {
case SPAWNS_ATTRIBUTE -> mergeSpawnAttribute(attributes, attribute.getValue());
case SPAWN_PROBABILITY_ATTRIBUTE -> {
if (spawnRarity) {
attributes.add(attribute.getKey(), attribute.getValue().deepCopy());
}
}
default -> attributes.add(attribute.getKey(), attribute.getValue().deepCopy());
}
}
}
if (overlay.has("spawners")) {
mergeSpawns(merged, overlay);
}
if (spawnRarity && overlay.has(SPAWN_PROBABILITY)) {
merged.add(SPAWN_PROBABILITY, overlay.get(SPAWN_PROBABILITY).deepCopy());
}
return merged;
}
private static void mergeSpawnAttribute(JsonObject attributes, JsonElement overlay) {
JsonElement existing = attributes.get(SPAWNS_ATTRIBUTE);
if (existing == null || !existing.isJsonObject() || !overlay.isJsonObject()) {
attributes.add(SPAWNS_ATTRIBUTE, overlay.deepCopy());
return;
}
JsonObject target = spawnHolder(existing.getAsJsonObject());
JsonObject source = spawnHolder(overlay.getAsJsonObject());
appendCategories(object(target, "spawns_by_category"), source.getAsJsonObject("spawns_by_category"));
putAll(object(target, "spawn_costs"), source.getAsJsonObject("spawn_costs"));
}
private static void mergeSpawns(JsonObject merged, JsonObject overlay) {
appendCategories(object(merged, "spawners"), overlay.getAsJsonObject("spawners"));
putAll(object(merged, "spawn_costs"), overlay.getAsJsonObject("spawn_costs"));
}
private static JsonObject spawnHolder(JsonObject attribute) {
JsonElement argument = attribute.get("argument");
return argument != null && argument.isJsonObject() ? argument.getAsJsonObject() : attribute;
}
private static void appendCategories(JsonObject target, JsonObject source) {
if (source == null) {
return;
}
for (Map.Entry<String, JsonElement> category : source.entrySet()) {
JsonElement existing = target.get(category.getKey());
JsonArray spawns = existing != null && existing.isJsonArray() ? existing.getAsJsonArray() : new JsonArray();
spawns.addAll(category.getValue().deepCopy().getAsJsonArray());
target.add(category.getKey(), spawns);
}
}
private static void putAll(JsonObject target, JsonObject source) {
if (source == null) {
return;
}
for (Map.Entry<String, JsonElement> entry : source.entrySet()) {
target.add(entry.getKey(), entry.getValue().deepCopy());
}
}
private static JsonObject object(JsonObject parent, String key) {
JsonElement existing = parent.get(key);
if (existing != null && existing.isJsonObject()) {
return existing.getAsJsonObject();
}
JsonObject created = new JsonObject();
parent.add(key, created);
return created;
}
private static JsonObject retained(JsonObject custom) {
JsonObject retained = new JsonObject();
for (String key : CUSTOM_KEYS) {
if (custom.has(key)) {
retained.add(key, custom.get(key).deepCopy());
}
}
return retained;
}
private static SortedSet<String> tags(IrisBiome biome, IrisBiomeCustom custom, Map<String, List<String>> vanillaTags) {
SortedSet<String> tags = new TreeSet<>();
if (custom != null && custom.getTags() != null) {
for (String tag : custom.getTags()) {
String normalized = tagId(tag);
if (normalized != null) {
tags.add(normalized);
}
}
}
String structureKey = biome.getVanillaDerivativeKey();
tags.addAll(vanillaTags.isEmpty() ? IrisVanillaBiomeTags.tagsFor(structureKey) : vanillaTags.getOrDefault(structureKey, List.of()));
return tags;
}
private static Map<String, List<String>> directTags(VanillaDataSource source) {
String root = "data/" + VANILLA_NAMESPACE + TAG_FOLDER;
Map<String, List<String>> tags = new HashMap<>();
for (String path : source.list(root)) {
if (!path.endsWith(JSON_SUFFIX)) {
continue;
}
String tag = VANILLA_NAMESPACE + ":" + path.substring(root.length() + 1, path.length() - JSON_SUFFIX.length());
Optional<JsonObject> json = read(source, path);
JsonArray values = json.isEmpty() ? null : json.get().getAsJsonArray("values");
if (values == null) {
continue;
}
for (JsonElement value : values) {
String member = value.isJsonObject() ? value.getAsJsonObject().get("id").getAsString() : value.getAsString();
if (!member.startsWith("#")) {
tags.computeIfAbsent(namespaced(member), ignored -> new ArrayList<>()).add(tag);
}
}
}
return tags;
}
private static Optional<JsonObject> read(VanillaDataSource source, String biomeKeyOrPath) {
String path = biomeKeyOrPath.startsWith("data/") ? biomeKeyOrPath : biomePath(biomeKeyOrPath);
if (path == null) {
return Optional.empty();
}
return source.read(path).map(bytes -> JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject());
}
private static String biomePath(String biomeKey) {
if (biomeKey == null || !VanillaResourceIdentifier.validIdentifier(biomeKey)) {
return null;
}
int separator = biomeKey.indexOf(':');
return "data/" + biomeKey.substring(0, separator) + BIOME_FOLDER + biomeKey.substring(separator + 1) + JSON_SUFFIX;
}
private static String namespaced(String key) {
String trimmed = key.trim().toLowerCase(Locale.ROOT);
return trimmed.indexOf(':') < 0 ? VANILLA_NAMESPACE + ":" + trimmed : trimmed;
}
private static String tagId(String tag) {
if (tag == null || tag.isBlank()) {
return null;
}
String trimmed = tag.trim();
String normalized = namespaced(trimmed.startsWith("#") ? trimmed.substring(1) : trimmed);
return VanillaResourceIdentifier.validIdentifier(normalized) ? normalized : null;
}
private static void diagnose(PackCompileContext context, String resource, String message) {
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.VANILLA_BIOME_DATA, resource, message));
}
public record Definitions(List<BiomeDef> biomes, List<JsonResource> tags) {
public Definitions {
biomes = List.copyOf(biomes);
tags = List.copyOf(tags);
}
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import java.util.Objects;
import java.util.Set;
public final class BiomeIdSanitizer {
private static final int FIRST_SUFFIX = 2;
private BiomeIdSanitizer() {
}
public static String sanitize(String namespace, String loadKey, Set<String> taken) {
VanillaResourceIdentifier.requireNamespace(namespace, "biome namespace");
Objects.requireNonNull(taken);
String path = VanillaResourceIdentifier.sanitizePath(loadKey);
String id = namespace + ":" + path;
int suffix = FIRST_SUFFIX;
while (taken.contains(id)) {
id = namespace + ":" + path + "_" + suffix;
suffix++;
}
taken.add(id);
return id;
}
}
@@ -0,0 +1,192 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRaritySelection;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Rarity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
final class BiomePools {
static final int MAX_CHILD_HOPS = 4;
static final List<InferredType> SURFACE_TYPES = List.of(InferredType.LAND, InferredType.SEA, InferredType.SHORE);
private final List<RegionPools> regions;
private final SortedMap<String, IrisBiome> biomes;
private final SortedSet<String> surface;
private final SortedSet<String> cave;
private final Map<String, ChildPlan> plans;
private BiomePools(List<RegionPools> regions, SortedMap<String, IrisBiome> biomes, SortedSet<String> surface, SortedSet<String> cave, Map<String, ChildPlan> plans) {
this.regions = regions;
this.biomes = biomes;
this.surface = surface;
this.cave = cave;
this.plans = plans;
}
static BiomePools load(IrisDimension dimension, IrisData data) {
List<RegionPools> regions = new ArrayList<>();
SortedMap<String, IrisBiome> biomes = new TreeMap<>();
for (IrisRegion region : data.getRegionLoader().loadAll(dimension.getRegions())) {
if (region.isCompatExcluded()) {
continue;
}
RegionPools pools = new RegionPools(
region,
pool(data, region.getLandBiomes(), biomes),
pool(data, region.getSeaBiomes(), biomes),
pool(data, region.getShoreBiomes(), biomes),
pool(data, region.getCaveBiomes(), biomes));
if (pools.land().isEmpty() && pools.sea().isEmpty()) {
throw new IllegalStateException("Region '" + region.getLoadKey() + "' has no land or sea biome to export");
}
regions.add(pools);
}
if (regions.isEmpty()) {
throw new IllegalStateException("Dimension '" + dimension.getLoadKey() + "' has no exportable region");
}
SortedSet<String> cave = new TreeSet<>();
SortedSet<String> frontier = new TreeSet<>();
for (RegionPools pools : regions) {
for (InferredType type : SURFACE_TYPES) {
for (IrisBiome biome : pools.of(type)) {
frontier.add(biome.getLoadKey());
}
}
for (IrisBiome biome : pools.cave()) {
cave.add(biome.getLoadKey());
}
}
Map<String, ChildPlan> plans = new HashMap<>();
SortedSet<String> surface = new TreeSet<>(frontier);
for (int hop = 0; hop < MAX_CHILD_HOPS && !frontier.isEmpty(); hop++) {
SortedSet<String> next = new TreeSet<>();
for (String key : frontier) {
ChildPlan plan = plans.computeIfAbsent(key, ignored -> ChildPlan.of(biomes.get(key), data, biomes));
for (String choice : plan.choices()) {
if (surface.add(choice)) {
next.add(choice);
}
}
}
frontier = next;
}
return new BiomePools(List.copyOf(regions), biomes, surface, cave, plans);
}
static List<Double> rarityCuts(List<? extends Rarity> options) {
double total = 0.0;
for (Rarity option : options) {
total += 1.0 / Rarity.get(option);
}
List<Double> cuts = new ArrayList<>(Math.max(0, options.size() - 1));
double cumulative = 0.0;
for (int index = 0; index + 1 < options.size(); index++) {
cumulative += 1.0 / Rarity.get(options.get(index));
cuts.add(cumulative / total);
}
return cuts;
}
static List<Double> selectionCuts(int size) {
int maximum = size - 1;
List<Double> cuts = new ArrayList<>(Math.max(0, maximum));
for (int index = 0; index < maximum; index++) {
cuts.add((index + 0.5) / maximum);
}
return cuts;
}
List<RegionPools> regions() {
return regions;
}
IrisBiome biome(String loadKey) {
return Objects.requireNonNull(biomes.get(loadKey), "Biome '" + loadKey + "' was not loaded");
}
SortedSet<String> surface() {
return surface;
}
SortedSet<String> cave() {
return cave;
}
ChildPlan plan(String loadKey) {
return plans.get(loadKey);
}
private static List<IrisBiome> pool(IrisData data, KList<String> keys, SortedMap<String, IrisBiome> biomes) {
List<IrisBiome> pool = new ArrayList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
if (biome.isCompatExcluded()) {
continue;
}
biomes.putIfAbsent(biome.getLoadKey(), biome);
pool.add(biome);
}
return List.copyOf(pool);
}
record RegionPools(IrisRegion region, List<IrisBiome> land, List<IrisBiome> sea, List<IrisBiome> shore, List<IrisBiome> cave) {
List<IrisBiome> of(InferredType type) {
return switch (type) {
case LAND -> land;
case SEA -> sea;
case SHORE -> shore;
case CAVE -> cave;
};
}
}
record ChildPlan(String parent, List<String> choiceByIndex) {
static ChildPlan of(IrisBiome parent, IrisData data, SortedMap<String, IrisBiome> biomes) {
if (parent.getChildren().isEmpty()) {
return new ChildPlan(parent.getLoadKey(), List.of(parent.getLoadKey()));
}
KList<IrisBiome> options = new KList<>();
for (IrisBiome child : parent.getRealChildren(() -> data)) {
if (child != null) {
biomes.putIfAbsent(child.getLoadKey(), child);
options.add(child);
}
}
options.add(parent);
IrisRaritySelection<IrisBiome> selection = IrisRaritySelection.create(options);
long size = selection.size();
List<String> choices = new ArrayList<>((int) size);
for (long index = 0; index < size; index++) {
choices.add(selection.get(index).getLoadKey());
}
return new ChildPlan(parent.getLoadKey(), List.copyOf(choices));
}
boolean moves() {
for (String choice : choiceByIndex) {
if (!choice.equals(parent)) {
return true;
}
}
return false;
}
SortedSet<String> choices() {
return new TreeSet<>(choiceByIndex);
}
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
public final class BiomeStage implements CompileStage {
@Override
public String name() {
return "biomes";
}
@Override
public void apply(PackCompileContext context) {
BiomeDecision decision = new BiomeDecisionCompiler(context.styles()).compile(context);
context.setBiomeDecision(decision);
BiomeDefinitionCompiler.Definitions definitions = BiomeDefinitionCompiler.forTarget(context.capabilities().target())
.compile(context, decision.base().loadKeyByBiomeId());
context.setBiomes(definitions.biomes());
context.jsonResources().addAll(definitions.tags());
}
}
@@ -0,0 +1,239 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.pack.value.IrisRaritySelection;
import art.arcane.volmlib.util.math.Rarity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.ToIntFunction;
final class CarvingZones {
static final int SHALLOW_ZONE = 0;
static final int CAVE_ZONE = 1;
static final int FIRST_CARVING_ZONE = 2;
private static final int MAX_CHILD_DEPTH = 32;
private static final double MINIMUM_CHILD_SHRINK_FACTOR = 0.0001;
private static final long CARVING_SALT = 0x0CA4_7E00L;
private final List<Integer> cuts;
private final List<Integer> zoneByInterval;
private final List<Machine> machines;
private final SortedSet<String> biomes;
private CarvingZones(List<Integer> cuts, List<Integer> zoneByInterval, List<Machine> machines, SortedSet<String> biomes) {
this.cuts = cuts;
this.zoneByInterval = zoneByInterval;
this.machines = machines;
this.biomes = biomes;
}
static CarvingZones plan(IrisDimension dimension, IrisData data, int deepTop) {
List<IrisDimensionCarvingEntry> entries = new ArrayList<>();
if (dimension.getCarving() != null) {
for (IrisDimensionCarvingEntry entry : dimension.getCarving()) {
if (entry != null && entry.isEnabled() && entry.getRealBiome(data) != null) {
entries.add(entry);
}
}
}
SortedSet<Integer> cutSet = new TreeSet<>();
cutSet.add(deepTop + 1);
for (IrisDimensionCarvingEntry entry : entries) {
IrisRange range = entry.getWorldYRange();
if (range != null && range.getMin() <= range.getMax()) {
cutSet.add((int) Math.ceil(range.getMin()));
cutSet.add((int) Math.floor(range.getMax()) + 1);
}
}
List<Integer> cuts = List.copyOf(cutSet);
Map<String, IrisDimensionCarvingEntry> byId = dimension.getCarvingEntryIndex();
Map<ZoneKey, Integer> zones = new LinkedHashMap<>();
List<Machine> machines = new ArrayList<>();
SortedSet<String> biomes = new TreeSet<>();
List<Integer> zoneByInterval = new ArrayList<>(cuts.size() + 1);
for (int interval = 0; interval <= cuts.size(); interval++) {
int y = interval == 0 ? cuts.getFirst() - 1 : cuts.get(interval - 1);
if (y > deepTop) {
zoneByInterval.add(SHALLOW_ZONE);
continue;
}
IrisDimensionCarvingEntry root = null;
List<Integer> inRange = new ArrayList<>();
for (int position = 0; position < entries.size(); position++) {
IrisDimensionCarvingEntry entry = entries.get(position);
if (contains(entry, y)) {
root = entry;
inRange.add(position);
}
}
if (root == null) {
zoneByInterval.add(CAVE_ZONE);
continue;
}
ZoneKey key = new ZoneKey(entries.indexOf(root), List.copyOf(inRange));
Integer zone = zones.get(key);
if (zone == null) {
Machine machine = Machine.build(root, y, byId, data);
zone = FIRST_CARVING_ZONE + machines.size();
zones.put(key, zone);
machines.add(machine);
for (State state : machine.states()) {
biomes.add(state.entry().getRealBiome(data).getLoadKey());
}
}
zoneByInterval.add(zone);
}
return new CarvingZones(cuts, List.copyOf(zoneByInterval), List.copyOf(machines), biomes);
}
SortedSet<String> biomes() {
return biomes;
}
IndexedField zoneKeys(Field y) {
List<Double> thresholds = new ArrayList<>(cuts.size());
for (int cut : cuts) {
thresholds.add((double) cut);
}
List<Integer> leaves = new ArrayList<>(zoneByInterval.size());
for (int zone : zoneByInterval) {
leaves.add(zone * 4);
}
return IndexedField.partition(y, thresholds, leaves);
}
List<IndexedField> slots(SelectorCompiler selectors, IrisData data, ToIntFunction<String> undergroundSlot) {
List<IndexedField> slots = new ArrayList<>(machines.size());
for (int zone = 0; zone < machines.size(); zone++) {
slots.add(machines.get(zone).slot("biome/carving/" + zone, selectors, data, undergroundSlot));
}
return slots;
}
private static boolean contains(IrisDimensionCarvingEntry entry, int y) {
IrisRange range = entry.getWorldYRange();
return range == null || range.contains(y);
}
private static int clampDepth(int depth) {
return depth <= 0 ? 0 : Math.min(depth, MAX_CHILD_DEPTH);
}
private record ZoneKey(int root, List<Integer> inRange) {
}
private record State(IrisDimensionCarvingEntry entry, int depth) {
}
private record Move(IrisDimensionCarvingEntry entry, List<Integer> nextByIndex) {
}
private record CarvingOption(IrisDimensionCarvingEntry entry, int rarity) implements Rarity {
@Override
public int getRarity() {
return rarity;
}
}
private record Machine(List<State> states, Map<Integer, Move> moves, int hops) {
static Machine build(IrisDimensionCarvingEntry root, int y, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
List<State> states = new ArrayList<>();
Map<State, Integer> ids = new HashMap<>();
Map<Integer, Move> moves = new HashMap<>();
int hops = clampDepth(root.getChildRecursionDepth());
State start = new State(root, terminal(root, byId, data) ? 0 : hops);
states.add(start);
ids.put(start, 0);
for (int cursor = 0; cursor < states.size(); cursor++) {
State state = states.get(cursor);
if (state.depth() <= 0) {
continue;
}
IrisRaritySelection<CarvingOption> selection = selection(state.entry(), byId, data);
if (selection.size() <= 1L) {
continue;
}
List<Integer> nextByIndex = new ArrayList<>((int) selection.size());
for (long index = 0; index < selection.size(); index++) {
IrisDimensionCarvingEntry chosen = selection.get(index).entry();
State next = chosen == state.entry() || !contains(chosen, y)
? new State(state.entry(), 0)
: new State(chosen, Math.min(state.depth() - 1, clampDepth(chosen.getChildRecursionDepth())));
Integer id = ids.get(next);
if (id == null) {
id = states.size();
ids.put(next, id);
states.add(next);
}
nextByIndex.add(id);
}
moves.put(cursor, new Move(state.entry(), List.copyOf(nextByIndex)));
}
return new Machine(List.copyOf(states), Map.copyOf(moves), hops);
}
IndexedField slot(String name, SelectorCompiler selectors, IrisData data, ToIntFunction<String> undergroundSlot) {
IndexedField current = IndexedField.constant(0);
for (int hop = 0; hop < hops; hop++) {
List<Integer> movers = new ArrayList<>();
for (int state : current.values()) {
if (moves.containsKey(state)) {
movers.add(state);
}
}
if (movers.isEmpty()) {
break;
}
Map<Integer, IndexedField> moved = new HashMap<>(movers.size() * 2);
for (int state : movers) {
Move move = moves.get(state);
IrisDimensionCarvingEntry entry = move.entry();
SelectorCompiler.Selector selector = selectors.scaled(
entry.getChildStyle(),
Math.max(MINIMUM_CHILD_SHRINK_FACTOR, entry.getChildShrinkFactor()),
CARVING_SALT + Objects.toString(entry.getId(), "").trim().hashCode());
moved.put(state, selector.partition(BiomePools.selectionCuts(move.nextByIndex().size()), move.nextByIndex()));
}
current = IndexedField.advance(current, moved).shared(name + "/hop" + (hop + 1));
}
return current.remap(state -> undergroundSlot.applyAsInt(states.get(state).entry().getRealBiome(data).getLoadKey()));
}
private static boolean terminal(IrisDimensionCarvingEntry entry, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
return entry.getChildren() == null || entry.getChildren().isEmpty() || selection(entry, byId, data).size() <= 1L;
}
private static IrisRaritySelection<CarvingOption> selection(IrisDimensionCarvingEntry parent, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
List<CarvingOption> options = new ArrayList<>();
if (parent.getChildren() != null) {
for (String childId : parent.getChildren()) {
if (childId == null || childId.isBlank()) {
continue;
}
IrisDimensionCarvingEntry child = byId.get(childId.trim());
if (child == null || !child.isEnabled()) {
continue;
}
IrisBiome childBiome = child.getRealBiome(data);
if (childBiome != null) {
options.add(new CarvingOption(child, Math.max(childBiome.getRarity(), 1)));
}
}
}
options.add(new CarvingOption(parent, Math.max(parent.getRealBiome(data).getRarity(), 1)));
return IrisRaritySelection.create(options);
}
}
}
@@ -0,0 +1,162 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.IntToDoubleFunction;
import java.util.function.IntUnaryOperator;
record IndexedField(Field field, List<Integer> values) {
private static final Field UNUSED_CASE = new Field.Const(0.0);
IndexedField {
Objects.requireNonNull(field);
values = List.copyOf(new TreeSet<>(values));
if (values.isEmpty()) {
throw new IllegalArgumentException("An indexed field needs at least one value");
}
}
static IndexedField constant(int value) {
return new IndexedField(new Field.Const(value), List.of(value));
}
static IndexedField partition(Field input, List<Double> thresholds, List<Integer> leaves) {
if (leaves.size() != thresholds.size() + 1) {
throw new IllegalArgumentException("Partition needs " + (thresholds.size() + 1) + " leaves, got " + leaves.size());
}
List<Double> kept = new ArrayList<>(thresholds.size());
List<Integer> keptLeaves = new ArrayList<>(leaves.size());
keptLeaves.add(leaves.getFirst());
for (int index = 0; index < thresholds.size(); index++) {
int next = leaves.get(index + 1);
if (next == keptLeaves.getLast()) {
continue;
}
kept.add(thresholds.get(index));
keptLeaves.add(next);
}
if (keptLeaves.size() == 1) {
return constant(keptLeaves.getFirst());
}
List<Field> constants = new ArrayList<>(keptLeaves.size());
for (int leaf : keptLeaves) {
constants.add(new Field.Const(leaf));
}
return new IndexedField(new Field.Partition(input, kept, constants), keptLeaves);
}
static IndexedField select(IndexedField index, List<IndexedField> cases) {
if (index.isConstant()) {
return cases.get(index.values().getFirst());
}
IndexedField first = cases.get(index.values().getFirst());
boolean uniform = true;
SortedSet<Integer> reachable = new TreeSet<>();
for (int value : index.values()) {
IndexedField selected = cases.get(value);
uniform &= selected.equals(first);
reachable.addAll(selected.values());
}
if (uniform) {
return first;
}
int count = index.values().getLast() + 1;
List<Field> muxCases = new ArrayList<>(count);
for (int value = 0; value < count; value++) {
muxCases.add(index.values().contains(value) ? cases.get(value).field() : UNUSED_CASE);
}
return new IndexedField(new Field.Mux(index.field(), muxCases), List.copyOf(reachable));
}
static IndexedField advance(IndexedField current, Map<Integer, IndexedField> moved) {
int last = current.values().getLast();
List<IndexedField> cases = new ArrayList<>(last + 1);
for (int value = 0; value <= last; value++) {
IndexedField next = moved.get(value);
cases.add(next == null ? constant(value) : next);
}
return select(current, cases);
}
static IndexedField sum(List<IndexedField> terms) {
SortedSet<Integer> values = new TreeSet<>(List.of(0));
int offset = 0;
Field field = null;
for (IndexedField term : terms) {
SortedSet<Integer> combined = new TreeSet<>();
for (int value : values) {
for (int addend : term.values()) {
combined.add(value + addend);
}
}
values = combined;
if (term.isConstant()) {
offset += term.values().getFirst();
} else {
field = field == null ? term.field() : new Field.Binary(Field.BinaryOp.ADD, field, term.field());
}
}
if (field == null) {
return constant(offset);
}
Field total = offset == 0 ? field : new Field.Binary(Field.BinaryOp.ADD, field, new Field.Const(offset));
return new IndexedField(total, List.copyOf(values));
}
static List<Double> midpoints(List<Integer> sorted) {
List<Double> thresholds = new ArrayList<>(sorted.size() - 1);
for (int index = 0; index + 1 < sorted.size(); index++) {
thresholds.add((sorted.get(index) + sorted.get(index + 1)) / 2.0);
}
return thresholds;
}
boolean isConstant() {
return values.size() == 1;
}
IndexedField remap(IntUnaryOperator mapping) {
List<Integer> mapped = new ArrayList<>(values.size());
for (int value : values) {
mapped.add(mapping.applyAsInt(value));
}
if (isConstant()) {
return constant(mapped.getFirst());
}
return partition(field, midpoints(values), mapped);
}
Field lookup(IntToDoubleFunction table) {
if (isConstant()) {
return new Field.Const(table.applyAsDouble(values.getFirst()));
}
List<Double> thresholds = midpoints(values);
List<Double> keptThresholds = new ArrayList<>(thresholds.size());
List<Field> leaves = new ArrayList<>(values.size());
double previous = table.applyAsDouble(values.getFirst());
leaves.add(new Field.Const(previous));
for (int index = 1; index < values.size(); index++) {
double next = table.applyAsDouble(values.get(index));
if (next == previous) {
continue;
}
keptThresholds.add(thresholds.get(index - 1));
leaves.add(new Field.Const(next));
previous = next;
}
if (leaves.size() == 1) {
return leaves.getFirst();
}
return new Field.Partition(field, keptThresholds, leaves);
}
IndexedField shared(String name) {
return isConstant() ? this : new IndexedField(new Field.Shared(name, field), values);
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SplittableRandom;
import java.util.function.DoubleBinaryOperator;
public final class RarityCalibrator {
private static final int SAMPLES = 8192;
private static final double WINDOW_FEATURES = 96.0;
private static final double MAX_WINDOW_BLOCKS = 4_000_000.0;
private static final long POSITION_SEED = 0x5EED_B10EL;
private static final long WORLD_SEED = 0x1215_B10EL;
private final NoiseRegistry registry;
private final DataVersion target;
public RarityCalibrator(NoiseRegistry registry, DataVersion target) {
this.registry = Objects.requireNonNull(registry);
this.target = Objects.requireNonNull(target);
}
public Calibration calibrate(DoubleBinaryOperator iris, Field compiled, double featureBlocks, long salt) {
Objects.requireNonNull(iris);
Objects.requireNonNull(compiled);
double window = Math.min(MAX_WINDOW_BLOCKS, Math.max(1.0, featureBlocks) * WINDOW_FEATURES);
Map<String, NoiseDef> noises = noiseDefinitions();
SplittableRandom positions = new SplittableRandom(POSITION_SEED ^ salt);
double[] irisSamples = new double[SAMPLES];
double[] compiledSamples = new double[SAMPLES];
for (int index = 0; index < SAMPLES; index++) {
int x = (int) Math.floor((positions.nextDouble() - 0.5) * window);
int z = (int) Math.floor((positions.nextDouble() - 0.5) * window);
irisSamples[index] = iris.applyAsDouble(x, z);
compiledSamples[index] = compiled instanceof Field.Const constant
? constant.value()
: FieldEvaluator.sample(compiled, noises, WORLD_SEED, target, x, 0, z);
}
Arrays.sort(irisSamples);
Arrays.sort(compiledSamples);
return new Calibration(irisSamples, compiledSamples);
}
private Map<String, NoiseDef> noiseDefinitions() {
List<NoiseDef> definitions = registry.definitions();
Map<String, NoiseDef> noises = new HashMap<>(definitions.size() * 2);
for (NoiseDef definition : definitions) {
noises.put(registry.namespace() + ":" + definition.id(), definition);
}
return noises;
}
public static final class Calibration {
private final double[] iris;
private final double[] compiled;
private Calibration(double[] iris, double[] compiled) {
this.iris = iris;
this.compiled = compiled;
}
public double cumulative(double irisValue) {
int low = 0;
int high = iris.length;
while (low < high) {
int middle = (low + high) >>> 1;
if (iris[middle] < irisValue) {
low = middle + 1;
} else {
high = middle;
}
}
return low / (double) iris.length;
}
public double threshold(double probability) {
return quantile(compiled, probability);
}
public double irisQuantile(double probability) {
return quantile(iris, probability);
}
public List<Double> thresholds(List<Double> irisCuts) {
List<Double> thresholds = new ArrayList<>(irisCuts.size());
for (double cut : irisCuts) {
thresholds.add(threshold(cumulative(cut)));
}
return thresholds;
}
private static double quantile(double[] sorted, double probability) {
double position = Math.clamp(probability, 0.0, 1.0) * sorted.length - 0.5;
if (position <= 0.0) {
return sorted[0];
}
if (position >= sorted.length - 1) {
return sorted[sorted.length - 1];
}
int lower = (int) Math.floor(position);
double fraction = position - lower;
return sorted[lower] + fraction * (sorted[lower + 1] - sorted[lower]);
}
}
}
@@ -0,0 +1,184 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.DoubleBinaryOperator;
public final class SelectorCompiler {
private static final double COHERENT_BLOCKS = 64.0;
private static final long CALIBRATION_SEED = 0x1215_5E1EL;
private static final int CURVE_KNOTS = 9;
private static final String SHARED_PREFIX = "biome/selector/";
private final StyleCompiler styles;
private final IrisData data;
private final NoiseRegistry registry;
private final ExportCapabilities capabilities;
private final RarityCalibrator calibrator;
private final Map<SelectorKey, Selector> selectors = new HashMap<>();
private int shared;
public SelectorCompiler(StyleCompiler styles, Sources sources) {
this.styles = Objects.requireNonNull(styles);
Objects.requireNonNull(sources);
data = sources.data();
registry = sources.registry();
capabilities = sources.capabilities();
calibrator = new RarityCalibrator(registry, capabilities.target());
}
public Selector zoomed(IrisGeneratorStyle style, double zoom, long salt) {
return selectors.computeIfAbsent(new SelectorKey(style, false, zoom, salt), this::create);
}
public Selector scaled(IrisGeneratorStyle style, double scale, long salt) {
return selectors.computeIfAbsent(new SelectorKey(style, true, scale, salt), this::create);
}
public Selector sampled(IrisGeneratorStyle style, double zoom, long salt, DoubleBinaryOperator iris) {
IrisGeneratorStyle exported = exportable(style);
exported.setZoom(exported.getZoom() * zoom);
return compile(exported, salt, iris, COHERENT_BLOCKS * exported.getZoom());
}
private Selector create(SelectorKey key) {
IrisGeneratorStyle exported = exportable(key.style());
RNG seed = new RNG(CALIBRATION_SEED).nextParallelRNG(key.salt());
if (key.scaled()) {
CNG iris = exported.createScaledGenerator(seed, data, key.factor());
IrisGeneratorStyle compiled = scaledCopy(exported, key.factor());
return compile(compiled, key.salt(), iris::noiseFast2D, COHERENT_BLOCKS * compiled.getZoom());
}
CNG iris = exported.create(seed, data);
double zoom = key.factor();
IrisGeneratorStyle compiled = copy(exported);
compiled.setZoom(exported.getZoom() * zoom);
return compile(compiled, key.salt(), (x, z) -> iris.noise(x / zoom, z / zoom), COHERENT_BLOCKS * compiled.getZoom());
}
private Selector compile(IrisGeneratorStyle style, long salt, DoubleBinaryOperator iris, double featureBlocks) {
Field field = styles.compile(style, salt, registry, capabilities);
FieldValidator.requireTwoDimensional(field, "biome selector");
RarityCalibrator.Calibration calibration = calibrator.calibrate(iris, field, featureBlocks, salt);
Field selector = field instanceof Field.Const ? field : new Field.Shared(SHARED_PREFIX + shared++, field);
return new Selector(selector, calibration);
}
private static IrisGeneratorStyle exportable(IrisGeneratorStyle style) {
IrisGeneratorStyle copy = copy(style);
for (IrisGeneratorStyle current = copy; current != null; current = current.getFracture()) {
current.setImageMap(null);
current.setCacheSize(0);
}
return copy;
}
private static IrisGeneratorStyle scaledCopy(IrisGeneratorStyle style, double scale) {
IrisGeneratorStyle copy = copy(style);
copy.setZoom(style.getZoom() / scale);
IrisGeneratorStyle fracture = copy.getFracture();
if (fracture != null) {
fracture.setZoom(fracture.getZoom() * scale);
fracture.setMultiplier(fracture.getMultiplier() * scale);
}
return copy;
}
private static IrisGeneratorStyle copy(IrisGeneratorStyle style) {
IrisGeneratorStyle copy = new IrisGeneratorStyle(style.getStyle());
copy.setCellularFrequency(style.getCellularFrequency());
copy.setCellularZoom(style.getCellularZoom());
copy.setZoom(style.getZoom());
copy.setExpression(style.getExpression());
copy.setImageMap(style.getImageMap());
copy.setMultiplier(style.getMultiplier());
copy.setExponent(style.getExponent());
copy.setCacheSize(style.getCacheSize());
copy.setFracture(style.getFracture() == null ? null : copy(style.getFracture()));
return copy;
}
public record Sources(IrisData data, NoiseRegistry registry, ExportCapabilities capabilities) {
public Sources {
Objects.requireNonNull(data);
Objects.requireNonNull(registry);
Objects.requireNonNull(capabilities);
}
}
public record Selector(Field field, RarityCalibrator.Calibration calibration) {
public Selector {
Objects.requireNonNull(field);
Objects.requireNonNull(calibration);
}
IndexedField partition(List<Double> irisCuts, List<Integer> leaves) {
if (leaves.size() != irisCuts.size() + 1) {
throw new IllegalArgumentException("Selector partition needs " + (irisCuts.size() + 1) + " leaves, got " + leaves.size());
}
List<Double> upper = new ArrayList<>(leaves.size());
List<Integer> kept = new ArrayList<>(leaves.size());
double lower = 0.0;
for (int index = 0; index < leaves.size(); index++) {
double bound = index == irisCuts.size() ? 1.0 : Math.max(lower, calibration.cumulative(irisCuts.get(index)));
if (bound > lower) {
upper.add(bound);
kept.add(leaves.get(index));
}
lower = bound;
}
if (kept.isEmpty()) {
return IndexedField.constant(leaves.getLast());
}
List<Double> thresholds = new ArrayList<>(kept.size() - 1);
for (int index = 0; index + 1 < kept.size(); index++) {
thresholds.add(calibration.threshold(upper.get(index)));
}
return IndexedField.partition(field, thresholds, kept);
}
Field curve() {
List<Field.Knot> knots = new ArrayList<>(CURVE_KNOTS);
double previousLocation = Double.NEGATIVE_INFINITY;
List<double[]> points = new ArrayList<>(CURVE_KNOTS);
for (int index = 0; index < CURVE_KNOTS; index++) {
double probability = (index + 0.5) / CURVE_KNOTS;
double location = calibration.threshold(probability);
if (location <= previousLocation) {
continue;
}
points.add(new double[]{location, calibration.irisQuantile(probability)});
previousLocation = location;
}
if (points.size() == 1) {
return new Field.Const(points.getFirst()[1]);
}
for (int index = 0; index < points.size(); index++) {
double derivative = 0.0;
if (index > 0 && index + 1 < points.size()) {
double[] before = points.get(index - 1);
double[] after = points.get(index + 1);
derivative = (after[1] - before[1]) / (after[0] - before[0]);
}
knots.add(new Field.Knot(points.get(index)[0], new Field.Const(points.get(index)[1]), derivative));
}
return new Field.Curve(field, knots);
}
}
private record SelectorKey(IrisGeneratorStyle style, boolean scaled, double factor, long salt) {
}
}
@@ -0,0 +1,119 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
public final class AquiferCompiler {
private static final int LAVA_LIMIT_Y = -10;
private static final int LAVA_CENTER_MARGIN = 12;
private static final int FLUID_BAND_HEIGHT = 40;
private static final int FLUID_BAND_CENTER = 20;
private static final double SPREAD_LEVEL_SCALE = 10.0;
private static final double LEVEL_ROUNDING_MARGIN = 0.5;
private static final Field DRY = new Field.Const(-1.0);
private static final Field PARTIAL = new Field.Const(0.6);
private static final Field LOW_BARRIER = new Field.Const(-1.0);
private static final Field NO_EXCLUSION = new Field.Const(-1.0);
private static final Field LAVA = new Field.Const(1.0);
private static final Field NO_LAVA = new Field.Const(0.0);
private static final Field OUTSIDE_BAND = new Field.Const(0.0);
private static final Field INSIDE_BAND = new Field.Const(1.0);
private final CaveInputs inputs;
private final Field blockY;
private final int lowFluidBand;
private final int highFluidBand;
public AquiferCompiler(CaveInputs inputs) {
this.inputs = Objects.requireNonNull(inputs);
this.blockY = inputs.blockY();
this.lowFluidBand = Math.floorDiv(inputs.minY(), FLUID_BAND_HEIGHT) - 1;
this.highFluidBand = Math.floorDiv(inputs.maxY(), FLUID_BAND_HEIGHT) + 1;
}
public AquiferDef compile(CaveSelection selection) {
AquiferDef dry = AquiferDef.dry(inputs.aquiferSurface());
boolean lavaWanted = any(selection, IrisCaveProfile::isAllowLava);
boolean lavaExported = lavaWanted && inputs.lavaY() <= LAVA_LIMIT_Y;
if (lavaWanted && !lavaExported) {
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.LAVA_HEIGHT, inputs.resource(),
"Cave lava height Y " + inputs.lavaY() + " is above Y " + LAVA_LIMIT_Y + "; caves export without lava"));
}
boolean wet = any(selection, IrisCaveProfile::isAllowFluid);
if (!lavaExported && !wet) {
return dry;
}
Field spread = lavaExported ? lavaSpread(wet, dry.spread()) : dry.spread();
Field lava = lavaExported ? lavaBands() : NO_LAVA;
return new AquiferDef(LOW_BARRIER, floodedness(selection, lavaExported), spread, lava, NO_EXCLUSION, inputs.aquiferSurface());
}
private Field floodedness(CaveSelection selection, boolean lavaExported) {
Field flood = selection.profiles().isEmpty() ? DRY : selection.select(source -> flood(source, lavaExported), DRY);
for (CaveSource band : selection.bands()) {
Field banded = flood(band, lavaExported);
if (banded.equals(flood)) {
continue;
}
Field inside = new Field.Partition(blockY, List.of((double) band.minY(), band.maxY() + 1.0), List.of(OUTSIDE_BAND, INSIDE_BAND, OUTSIDE_BAND));
flood = new Field.Mux(inside, List.of(flood, banded));
}
return flood;
}
private Field flood(CaveSource source, boolean lavaExported) {
IrisCaveProfile profile = source.profile();
int top = Integer.MIN_VALUE;
if (lavaExported && profile.isAllowLava()) {
top = inputs.lavaY() + LAVA_CENTER_MARGIN;
}
if (profile.isAllowFluid()) {
top = Math.max(top, inputs.fluidHeight());
}
if (top < inputs.minY()) {
return DRY;
}
if (top >= inputs.maxY()) {
return PARTIAL;
}
return new Field.RangeIf(blockY, inputs.minY(), top + 1.0, PARTIAL, DRY);
}
private Field lavaSpread(boolean wet, Field vanillaSpread) {
Field levels = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, lowFluidBand, highFluidBand, spreadAt(lowFluidBand), spreadAt(highFluidBand));
return wet ? new Field.RangeIf(fluidBand(), lowFluidBand, lavaBand() + 1.0, levels, vanillaSpread) : levels;
}
private Field lavaBands() {
return new Field.RangeIf(fluidBand(), lowFluidBand, lavaBand() + 1.0, LAVA, NO_LAVA);
}
private Field fluidBand() {
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, lowFluidBand, highFluidBand, lowFluidBand, highFluidBand);
}
private int lavaBand() {
return Math.floorDiv(inputs.lavaY() + LAVA_CENTER_MARGIN, FLUID_BAND_HEIGHT);
}
private double spreadAt(int band) {
return (inputs.lavaY() - FLUID_BAND_CENTER + LEVEL_ROUNDING_MARGIN) / SPREAD_LEVEL_SCALE - band * (FLUID_BAND_HEIGHT / SPREAD_LEVEL_SCALE);
}
private static boolean any(CaveSelection selection, Predicate<IrisCaveProfile> property) {
for (CaveSource source : selection.sources()) {
if (property.test(source.profile())) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class CaveFieldCompiler {
static final int CELL_XZ = 4;
static final int CELL_Y = 4;
static final double WALL_BAND = 0.03125;
private static final double MIN_GAIN = 1.0 / 64.0;
private static final double MAX_GAIN = 64.0;
private static final Field UNCOVERED = new Field.Const(CaveProfileCompiler.SOLID);
private final CaveProfileCompiler profiles;
private final CaveInputs inputs;
public CaveFieldCompiler(StyleCompiler styles, CaveInputs inputs) {
this.profiles = new CaveProfileCompiler(styles, inputs);
this.inputs = inputs;
}
public Optional<Field> compile(CaveSelection selection) {
return union(selection).map(union -> new Field.Grid(union, CELL_XZ, CELL_Y));
}
private Optional<Field> union(CaveSelection selection) {
List<Field> parts = new ArrayList<>(selection.bands().size() + 1);
if (!selection.profiles().isEmpty()) {
parts.add(selection.select(this::normalized, UNCOVERED));
}
for (CaveSource band : selection.bands()) {
parts.add(normalized(band));
}
if (parts.isEmpty()) {
return Optional.empty();
}
Field union = parts.getFirst();
for (int index = 1; index < parts.size(); index++) {
union = new Field.Binary(Field.BinaryOp.MIN, union, parts.get(index));
}
return Optional.of(union);
}
private Field normalized(CaveSource source) {
return profiles.compile(source, gain(source));
}
private double gain(CaveSource source) {
Optional<CaveInterior> interior = profiles.interior(source);
if (interior.isEmpty()) {
return 1.0;
}
double band = WallShellCalibrator.band(interior.get(), inputs.noises().byReference(), inputs.capabilities().target());
return band > 0.0 ? Math.clamp(WALL_BAND / band, MIN_GAIN, MAX_GAIN) : 1.0;
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import java.util.List;
import java.util.Objects;
public record CaveInputs(
String resource,
int minY,
int maxY,
int fluidHeight,
int lavaY,
Field surface,
Field aquiferSurface,
NoiseRegistry noises,
ExportCapabilities capabilities,
List<ExportDiagnostic> diagnostics
) {
public CaveInputs {
Objects.requireNonNull(resource);
Objects.requireNonNull(surface);
Objects.requireNonNull(aquiferSurface);
Objects.requireNonNull(noises);
Objects.requireNonNull(capabilities);
Objects.requireNonNull(diagnostics);
if (maxY <= minY) {
throw new IllegalArgumentException("Cave bounds need maxY above minY, got " + minY + ".." + maxY);
}
FieldValidator.requireTwoDimensional(surface, "cave surface");
FieldValidator.requireTwoDimensional(aquiferSurface, "aquifer surface");
}
public static CaveInputs of(PackCompileContext context) {
IrisDimension dimension = context.dimension();
int minY = dimension.getMinHeight();
return new CaveInputs(
"dimensions/" + dimension.getLoadKey(),
minY,
dimension.getMaxHeight() - 1,
context.dimensionDef().fluidHeight(),
minY + dimension.getCaveLavaHeight(),
context.height(),
context.surfaceEstimate(),
context.noises(),
context.capabilities(),
context.diagnostics());
}
public Field blockY() {
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, minY, maxY + 1, minY, maxY + 1);
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.Objects;
record CaveInterior(Field field, int minY, int maxY) {
CaveInterior {
Objects.requireNonNull(field);
if (maxY < minY) {
throw new IllegalArgumentException("Cave interior needs maxY at or above minY, got " + minY + ".." + maxY);
}
}
}
@@ -0,0 +1,294 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.value.IrisRange;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class CaveProfileCompiler {
static final double SOLID = 1.0;
private static final long BASE_SALT = 934_447L;
private static final long DETAIL_SALT = 612_991L;
private static final long THRESHOLD_SALT = 489_112L;
private static final long SURFACE_BREAK_SALT = 341_219L;
private static final long MODULE_SALT = 1_000_003L;
private static final long MODULE_SALT_STEP = 65_537L;
private static final double LOW_WEIGHT = 0.1;
private static final int CEILING_FADE_DEPTH = 12;
private static final double CLOSURE_EPSILON = 1.0e-6;
private static final double SEALED_MARGIN = 1.0;
private static final Set<NoiseStyle> CELLULAR_STYLES = cellularStyles();
private final StyleCompiler styles;
private final CaveInputs inputs;
private final Field blockY;
public CaveProfileCompiler(StyleCompiler styles, CaveInputs inputs) {
this.styles = Objects.requireNonNull(styles);
this.inputs = Objects.requireNonNull(inputs);
this.blockY = inputs.blockY();
}
public Field compile(CaveSource source, double gain) {
if (!(gain > 0.0) || Double.isInfinite(gain)) {
throw new IllegalArgumentException("Cave field gain must be finite and positive, got " + gain);
}
IrisCaveProfile profile = source.profile();
IrisRange range = profile.getVerticalRange();
int floor = floor(inputs, source);
int ceiling = Math.min(source.maxY(), Math.min(inputs.maxY(), inputs.minY() + (int) Math.ceil(range.getMax())));
if (ceiling < floor) {
return new Field.Const(SOLID);
}
double normalization = normalization(profile) / gain;
List<Field> terms = densityTerms(source, normalization, floor, ceiling);
edgeFade(profile, floor, ceiling, gain).ifPresent(terms::add);
terms.add(surface(source, closure(profile, normalization), gain));
return bounded(sum(terms), floor, ceiling);
}
Optional<CaveInterior> interior(CaveSource source) {
IrisCaveProfile profile = source.profile();
int floor = floor(inputs, source);
int ceiling = Math.min(source.maxY(), Math.min(inputs.maxY(), inputs.minY() + (int) Math.ceil(profile.getVerticalRange().getMax())));
if (ceiling < floor) {
return Optional.empty();
}
List<Field> terms = densityTerms(source, normalization(profile), floor, ceiling);
terms.add(negativeThreshold(profile, 1.0));
return Optional.of(new CaveInterior(sum(terms), floor, ceiling));
}
static int floor(CaveInputs inputs, CaveSource source) {
return Math.max(source.minY(), inputs.minY() + Math.max(0, (int) Math.floor(source.profile().getVerticalRange().getMin())));
}
static boolean cellular(IrisGeneratorStyle style) {
if (style.getExpression() != null || style.getImageMap() != null) {
return false;
}
if (style.getCellularFrequency() > 0.0 || CELLULAR_STYLES.contains(style.getStyle())) {
return true;
}
return style.getFracture() != null && cellular(style.getFracture());
}
private List<Field> densityTerms(CaveSource source, double normalization, int floor, int ceiling) {
IrisCaveProfile profile = source.profile();
List<Field> terms = new ArrayList<>();
addTerm(terms, source, new Term(profile.getBaseDensityStyle(), BASE_SALT, profile.getBaseWeight(), 0.0, false), normalization);
addTerm(terms, source, new Term(profile.getDetailDensityStyle(), DETAIL_SALT, profile.getDetailWeight(), 0.0, false), normalization);
List<IrisCaveFieldModule> modules = profile.getModules();
for (int index = 0; index < modules.size(); index++) {
IrisCaveFieldModule module = modules.get(index);
IrisRange range = module.getVerticalRange();
int low = inputs.minY() + (int) Math.floor(range.getMin());
int high = inputs.minY() + (int) Math.ceil(range.getMax());
if (module.getWeight() == 0.0 || high < floor || low > ceiling) {
continue;
}
Field term = term(source, new Term(module.getStyle(), MODULE_SALT + index * MODULE_SALT_STEP, module.getWeight(), module.getThreshold(), module.isInvert()), normalization);
terms.add(low <= floor && high >= ceiling ? term : new Field.Gate(window(low, high), term));
}
return terms;
}
private void addTerm(List<Field> terms, CaveSource source, Term term, double normalization) {
if (term.weight() != 0.0) {
terms.add(term(source, term, normalization));
}
}
private Field term(CaveSource source, Term term, double normalization) {
double sign = term.invert() ? -1.0 : 1.0;
double scale = sign * term.weight() / normalization;
Field unit = volume(source, term.style(), term.salt(), Math.abs(term.weight()) < LOW_WEIGHT);
return affine(unit, 2.0 * scale, -scale * (1.0 + term.threshold()));
}
private Field volume(CaveSource source, IrisGeneratorStyle style, long salt, boolean lowWeight) {
boolean cellularStyle = cellular(style);
if (cellularStyle && lowWeight) {
return VolumeLift.lift(styles.compile(substitute(style), salt, inputs.noises(), inputs.capabilities()));
}
Field compiled = styles.compile(style, salt, inputs.noises(), inputs.capabilities());
if (compiled.axes().contains(Axis.Y)) {
return compiled;
}
if (!cellularStyle) {
return VolumeLift.lift(compiled);
}
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.NOISE_STYLE, source.resource(),
"Cellular cave style " + style.getStyle() + " exports as smooth noise"));
return VolumeLift.lift(styles.compile(substitute(style), salt, inputs.noises(), inputs.capabilities()));
}
private Field planar(IrisGeneratorStyle style, long salt) {
Field compiled = styles.compile(style, salt, inputs.noises(), inputs.capabilities());
return compiled.axes().contains(Axis.Y) ? new Field.Slice(compiled, Axis.Y, 0) : compiled;
}
private Field surface(CaveSource source, double closure, double gain) {
IrisCaveProfile profile = source.profile();
Field negativeThreshold = negativeThreshold(profile, gain);
Field depthBelowClearance = new Field.Binary(Field.BinaryOp.SUB,
new Field.Binary(Field.BinaryOp.ADD, inputs.surface(), new Field.Const(-Math.max(0, profile.getSurfaceClearance()))),
blockY);
Field sealed = new Field.Curve(depthBelowClearance, List.of(
new Field.Knot(-1.0, new Field.Const(SEALED_MARGIN * gain - closure), 0.0),
new Field.Knot(0.0, new Field.Const(-closure), 0.0),
new Field.Knot(CEILING_FADE_DEPTH, negativeThreshold, 0.0)));
if (!profile.isAllowSurfaceBreak()) {
return sealed;
}
Field breaking = new Field.Binary(Field.BinaryOp.ADD, breakSteps(profile, closure, gain), negativeThreshold);
Field breakNoise = planar(profile.getSurfaceBreakStyle(), SURFACE_BREAK_SALT);
Field cutoff = new Field.Const((profile.getSurfaceBreakNoiseThreshold() + 1.0) / 2.0);
return new Field.Binary(Field.BinaryOp.ADD,
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, cutoff, breakNoise), sealed),
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, breakNoise, cutoff), breaking));
}
private Field breakSteps(IrisCaveProfile profile, double closure, double gain) {
int depth = Math.max(0, profile.getSurfaceBreakDepth());
double boost = Math.max(0.0, profile.getSurfaceBreakThresholdBoost()) * gain;
Field belowSurface = new Field.Binary(Field.BinaryOp.SUB, inputs.surface(), blockY);
List<Field.Knot> knots = new ArrayList<>();
knots.add(new Field.Knot(-1.0, new Field.Const((thresholdCeiling(profile) + SEALED_MARGIN) * gain - closure + boost), 0.0));
knots.add(new Field.Knot(0.0, new Field.Const(-boost), 0.0));
if (boost > 0.0) {
if (depth > 0) {
knots.add(new Field.Knot(depth, new Field.Const(-boost), 0.0));
}
knots.add(new Field.Knot(depth + 1.0, new Field.Const(0.0), 0.0));
}
return new Field.Curve(belowSurface, knots);
}
private Field negativeThreshold(IrisCaveProfile profile, double gain) {
IrisStyledRange range = profile.getDensityThreshold();
double bias = profile.getThresholdBias();
if (range.getMin() == range.getMax()) {
return new Field.Const(gain * (bias - range.getMin()));
}
if (range.getStyle().isFlat()) {
return new Field.Const(gain * (bias - (range.getMin() + 0.5 * (range.getMax() - range.getMin()))));
}
return affine(planar(range.getStyle(), THRESHOLD_SALT), gain * (range.getMin() - range.getMax()), gain * (bias - range.getMin()));
}
private Optional<Field> edgeFade(IrisCaveProfile profile, int floor, int ceiling, double gain) {
int fade = Math.max(0, profile.getVerticalEdgeFade());
double strength = Math.max(0.0, profile.getVerticalEdgeFadeStrength()) * gain;
if (fade <= 0 || strength <= 0.0 || ceiling <= floor) {
return Optional.empty();
}
List<Field.Knot> knots = new ArrayList<>();
knots.add(new Field.Knot(floor, new Field.Const(strength), 0.0));
if (floor + fade <= ceiling - fade) {
knots.add(new Field.Knot(floor + fade, new Field.Const(0.0), 0.0));
if (ceiling - fade > floor + fade) {
knots.add(new Field.Knot(ceiling - fade, new Field.Const(0.0), 0.0));
}
} else {
double middle = floor + (ceiling - floor) / 2.0;
double progress = Math.min(middle - floor, ceiling - middle) / fade;
knots.add(new Field.Knot(middle, new Field.Const((1.0 - smoothstep(progress)) * strength), 0.0));
}
knots.add(new Field.Knot(ceiling, new Field.Const(strength), 0.0));
return Optional.of(new Field.Curve(blockY, knots));
}
private Field bounded(Field core, int floor, int ceiling) {
if (floor <= inputs.minY() && ceiling >= inputs.maxY()) {
return core;
}
Field shifted = new Field.Binary(Field.BinaryOp.ADD, core, new Field.Const(-SOLID));
return new Field.Binary(Field.BinaryOp.ADD, new Field.Gate(window(floor, ceiling), shifted), new Field.Const(SOLID));
}
private Field window(int low, int high) {
return new Field.RangeIf(blockY, low, high + 1.0, new Field.Const(1.0), new Field.Const(0.0));
}
private static Field affine(Field input, double scale, double offset) {
if (input instanceof Field.Const constant) {
return new Field.Const(scale * constant.value() + offset);
}
if (scale == 0.0) {
return new Field.Const(offset);
}
return new Field.Curve(input, List.of(
new Field.Knot(0.0, new Field.Const(offset), scale),
new Field.Knot(1.0, new Field.Const(scale + offset), scale)));
}
private static Field sum(List<Field> terms) {
Field total = terms.getFirst();
for (int index = 1; index < terms.size(); index++) {
total = new Field.Binary(Field.BinaryOp.ADD, total, terms.get(index));
}
return total;
}
private static double normalization(IrisCaveProfile profile) {
double weight = Math.abs(profile.getBaseWeight()) + Math.abs(profile.getDetailWeight());
for (IrisCaveFieldModule module : profile.getModules()) {
weight += Math.abs(module.getWeight());
}
return weight <= 0.0 ? 1.0 : weight;
}
private static double closure(IrisCaveProfile profile, double normalization) {
double minimum = -Math.abs(profile.getBaseWeight()) - Math.abs(profile.getDetailWeight());
for (IrisCaveFieldModule module : profile.getModules()) {
double threshold = module.getThreshold();
double low = module.isInvert() ? threshold - 1.0 : -1.0 - threshold;
double high = module.isInvert() ? threshold + 1.0 : 1.0 - threshold;
minimum += Math.min(low * module.getWeight(), high * module.getWeight());
}
return minimum / normalization - CLOSURE_EPSILON;
}
private static double thresholdCeiling(IrisCaveProfile profile) {
IrisStyledRange range = profile.getDensityThreshold();
return Math.max(range.getMin(), range.getMax()) - profile.getThresholdBias();
}
private static double smoothstep(double progress) {
double clamped = Math.max(0.0, Math.min(1.0, progress));
return clamped * clamped * (3.0 - 2.0 * clamped);
}
private static IrisGeneratorStyle substitute(IrisGeneratorStyle style) {
return new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(style.getZoom());
}
private static Set<NoiseStyle> cellularStyles() {
Set<NoiseStyle> cellular = EnumSet.noneOf(NoiseStyle.class);
for (NoiseStyle style : NoiseStyle.values()) {
String name = style.name();
if (name.contains("CELL") || name.contains("VASCULAR") || name.contains("GLOB") || name.contains("HEX")) {
cellular.add(style);
}
}
return cellular;
}
private record Term(IrisGeneratorStyle style, long salt, double weight, double threshold, boolean invert) {
}
}
@@ -0,0 +1,185 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
public record CaveSelection(Field index, List<CaveSource> profiles, boolean uncovered, List<CaveSource> bands) {
private static final int UNCOVERED = -1;
private static final Field NO_INDEX = new Field.Const(0.0);
private static final Comparator<ClimateLayout.ClimateEntry> CODE_ORDER = Comparator
.comparingDouble(ClimateLayout.ClimateEntry::min)
.thenComparingDouble(ClimateLayout.ClimateEntry::max)
.thenComparing(ClimateLayout.ClimateEntry::biomeId);
public CaveSelection {
Objects.requireNonNull(index);
profiles = List.copyOf(profiles);
bands = List.copyOf(bands);
}
public static CaveSelection resolve(CaveInputs inputs, IrisDimension dimension, IrisData data, BiomeCodes codes) {
if (!dimension.isCarvingEnabled() || dimension.getDisabledComponents().contains(ReservedFlag.CARVED)) {
return new CaveSelection(NO_INDEX, List.of(), false, List.of());
}
Map<IrisCaveProfile, Integer> caseByProfile = new LinkedHashMap<>();
List<CaveSource> sources = new ArrayList<>();
IrisCaveProfile dimensionProfile = dimension.getCaveProfile();
int fallback = enabled(dimensionProfile)
? caseOf(source(inputs, inputs.resource(), dimensionProfile), caseByProfile, sources)
: UNCOVERED;
List<Double> thresholds = new ArrayList<>();
List<Integer> leaves = new ArrayList<>();
leaves.add(fallback);
List<ClimateLayout.ClimateEntry> entries = caveEntries(codes, regionCaveBiomes(dimension, data));
for (int position = 0; position < entries.size(); position++) {
ClimateLayout.ClimateEntry entry = entries.get(position);
Optional<CaveSource> source = entrySource(inputs, dimension, data, codes.loadKeyByBiomeId().get(entry.biomeId()));
int selected = source.isPresent() ? caseOf(source.get(), caseByProfile, sources) : fallback;
append(thresholds, leaves, entry.min(), selected);
double nextMin = position + 1 < entries.size() ? entries.get(position + 1).min() : Double.POSITIVE_INFINITY;
if (entry.max() < nextMin) {
append(thresholds, leaves, entry.max(), fallback);
}
}
boolean uncovered = leaves.contains(UNCOVERED);
int uncoveredCase = sources.size();
List<Field> constants = new ArrayList<>(leaves.size());
for (int leaf : leaves) {
constants.add(new Field.Const(leaf == UNCOVERED ? uncoveredCase : leaf));
}
Field index = thresholds.isEmpty() ? constants.getFirst() : new Field.Partition(codes.caveCode(), thresholds, constants);
return new CaveSelection(index, sources, uncovered, bands(inputs, dimension, data));
}
public boolean isEmpty() {
return profiles.isEmpty() && bands.isEmpty();
}
public List<CaveSource> sources() {
List<CaveSource> all = new ArrayList<>(profiles.size() + bands.size());
all.addAll(profiles);
all.addAll(bands);
return all;
}
public Field select(Function<CaveSource, Field> compiler, Field uncoveredCase) {
List<Field> cases = new ArrayList<>(profiles.size() + 1);
for (CaveSource source : profiles) {
cases.add(compiler.apply(source));
}
if (uncovered || cases.isEmpty()) {
cases.add(uncoveredCase);
}
return cases.size() == 1 ? cases.getFirst() : new Field.Mux(index, cases);
}
private static void append(List<Double> thresholds, List<Integer> leaves, double start, int selected) {
if (leaves.getLast() == selected) {
return;
}
thresholds.add(start);
leaves.add(selected);
}
private static List<ClimateLayout.ClimateEntry> caveEntries(BiomeCodes codes, Set<String> caveBiomes) {
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>();
for (ClimateLayout.ClimateEntry entry : codes.layout().entries()) {
if (entry.axis() == ClimateLayout.ClimateAxis.DEPTH && caveBiomes.contains(codes.loadKeyByBiomeId().get(entry.biomeId()))) {
entries.add(entry);
}
}
entries.sort(CODE_ORDER);
return entries;
}
private static Set<String> regionCaveBiomes(IrisDimension dimension, IrisData data) {
Set<String> caveBiomes = new HashSet<>();
for (String regionKey : dimension.getRegions()) {
IrisRegion region = regionKey == null ? null : data.getRegionLoader().load(regionKey, false);
if (region != null && !region.isCompatExcluded()) {
caveBiomes.addAll(region.getCaveBiomes());
}
}
return caveBiomes;
}
private static Optional<CaveSource> entrySource(CaveInputs inputs, IrisDimension dimension, IrisData data, String loadKey) {
IrisBiome biome = loadKey == null ? null : data.getBiomeLoader().load(loadKey, false);
if (biome == null || biome.isCompatExcluded()) {
return Optional.empty();
}
if (enabled(biome.getCaveProfile())) {
return Optional.of(source(inputs, "biomes/" + biome.getLoadKey(), biome.getCaveProfile()));
}
for (String regionKey : dimension.getRegions()) {
IrisRegion region = regionKey == null ? null : data.getRegionLoader().load(regionKey, false);
if (region == null || region.isCompatExcluded() || !region.getCaveBiomes().contains(loadKey) || !enabled(region.getCaveProfile())) {
continue;
}
return Optional.of(source(inputs, "regions/" + region.getLoadKey(), region.getCaveProfile()));
}
return Optional.empty();
}
private static List<CaveSource> bands(CaveInputs inputs, IrisDimension dimension, IrisData data) {
List<CaveSource> bands = new ArrayList<>();
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
if (entries == null) {
return bands;
}
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null || !entry.isEnabled()) {
continue;
}
IrisBiome biome = entry.getRealBiome(data);
if (biome == null || !enabled(biome.getCaveProfile())) {
continue;
}
IrisRange range = entry.getWorldYRange();
int low = range == null ? inputs.minY() : Math.max(inputs.minY(), (int) Math.ceil(range.getMin()));
int high = range == null ? inputs.maxY() : Math.min(inputs.maxY(), (int) Math.floor(range.getMax()));
if (high >= low) {
bands.add(new CaveSource("biomes/" + biome.getLoadKey(), biome.getCaveProfile(), low, high));
}
}
return bands;
}
private static int caseOf(CaveSource source, Map<IrisCaveProfile, Integer> caseByProfile, List<CaveSource> sources) {
Integer existing = caseByProfile.get(source.profile());
if (existing != null) {
return existing;
}
caseByProfile.put(source.profile(), sources.size());
sources.add(source);
return sources.size() - 1;
}
private static CaveSource source(CaveInputs inputs, String resource, IrisCaveProfile profile) {
return new CaveSource(resource, profile, inputs.minY(), inputs.maxY());
}
private static boolean enabled(IrisCaveProfile profile) {
return profile != null && profile.isEnabled();
}
}
@@ -0,0 +1,15 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import java.util.Objects;
public record CaveSource(String resource, IrisCaveProfile profile, int minY, int maxY) {
public CaveSource {
Objects.requireNonNull(resource);
Objects.requireNonNull(profile);
if (maxY < minY) {
throw new IllegalArgumentException("Cave source '" + resource + "' needs maxY at or above minY, got " + minY + ".." + maxY);
}
}
}
@@ -0,0 +1,61 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.Field;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class CaveStage implements CompileStage {
private static final String CARVERS = "carvers";
@Override
public String name() {
return "caves";
}
@Override
public void apply(PackCompileContext context) {
CaveInputs inputs = CaveInputs.of(context);
CaveSelection selection = CaveSelection.resolve(inputs, context.dimension(), context.data(), context.biomeCodes());
CaveFieldCompiler compiler = new CaveFieldCompiler(context.styles(), inputs);
Optional<Field> caves = compiler.compile(selection);
if (caves.isPresent()) {
Field.Shared field = new Field.Shared(DeepCaveCompiler.FIELD, caves.get());
Optional<DeepCaveCompiler.DeepCaves> deep = new DeepCaveCompiler(inputs).compile(field, selection);
context.setCaveField(field, CaveFieldCompiler.WALL_BAND);
context.setCaves(deep.map(DeepCaveCompiler.DeepCaves::router).orElse(field));
deep.ifPresent(found -> context.setDeepCaves(found.opening()));
}
context.setAquifer(new AquiferCompiler(inputs).compile(selection));
List<BiomeDef> biomes = context.biomes();
if (biomes.stream().anyMatch(CaveStage::carves)) {
context.setBiomes(withoutCarvers(biomes));
}
}
private static List<BiomeDef> withoutCarvers(List<BiomeDef> biomes) {
List<BiomeDef> cleaned = new ArrayList<>(biomes.size());
for (BiomeDef biome : biomes) {
if (!carves(biome)) {
cleaned.add(biome);
continue;
}
JsonObject json = biome.json().deepCopy();
json.add(CARVERS, new JsonArray());
cleaned.add(new BiomeDef(biome.id(), json));
}
return cleaned;
}
private static boolean carves(BiomeDef biome) {
JsonElement carvers = biome.json().get(CARVERS);
return carvers != null && !(carvers.isJsonArray() && carvers.getAsJsonArray().isEmpty());
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.lower.FieldDomains;
import java.util.Objects;
import java.util.Optional;
public final class DeepCaveCompiler {
static final int LAVA_LINE = -54;
static final String FIELD = "cave/field";
static final String OPENING = "cave/deep_opening";
private static final double OPEN_MARGIN = Math.nextUp(Float.MIN_VALUE);
private static final Field OPEN = new Field.Const(1.0);
private static final Field INSIDE = new Field.Const(0.0);
private static final Field CLOSED = new Field.Const(CaveProfileCompiler.SOLID);
private static final Field UNMASKED = new Field.Const(-FieldDomains.CONSTANT_LIMIT);
private final CaveInputs inputs;
private final Field blockY;
public DeepCaveCompiler(CaveInputs inputs) {
this.inputs = Objects.requireNonNull(inputs);
this.blockY = inputs.blockY();
}
public Optional<DeepCaves> compile(Field.Shared field, CaveSelection selection) {
Optional<Zone> found = zone(selection);
if (found.isEmpty()) {
return Optional.empty();
}
Zone zone = found.get();
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.DEEP_CAVE_LAYERS, inputs.resource(),
"Caves between Y " + zone.low() + " and Y " + zone.high() + " open in the material pass, so they get wall palettes but no floor or ceiling layers"));
Field opened = new Field.Binary(Field.BinaryOp.MAX, field, inside(zone, INSIDE, CLOSED));
Field.Shared opening = new Field.Shared(OPENING,
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, new Field.Const(OPEN_MARGIN), opened), OPEN));
Field mask = new Field.Binary(Field.BinaryOp.ADD, opening, inside(zone, INSIDE, UNMASKED));
return Optional.of(new DeepCaves(new Field.Binary(Field.BinaryOp.MAX, field, mask), opening));
}
private Optional<Zone> zone(CaveSelection selection) {
int high = Math.min(line() - 1, inputs.maxY());
int low = inputs.minY();
int deepest = Integer.MAX_VALUE;
for (CaveSource source : selection.sources()) {
deepest = Math.min(deepest, CaveProfileCompiler.floor(inputs, source));
if (source.profile().isAllowLava()) {
low = Math.max(low, inputs.lavaY() + 1);
}
}
if (high < low || high < deepest) {
return Optional.empty();
}
return Optional.of(new Zone(low, high));
}
private int line() {
return Math.min(LAVA_LINE, DimensionDef.seaLevel(inputs.fluidHeight()));
}
private Field inside(Zone zone, Field inRange, Field outOfRange) {
return new Field.RangeIf(blockY, zone.low(), zone.high() + 1.0, inRange, outOfRange);
}
public record DeepCaves(Field router, Field.Shared opening) {
public DeepCaves {
Objects.requireNonNull(router);
Objects.requireNonNull(opening);
}
}
private record Zone(int low, int high) {
}
}
Loaded 100 of 385 files, more files were not shown because too many files have changed in this diff. Show more