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

@@ -54,7 +54,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.LockSupport;
final class RealPackProbeSupport {
public final class RealPackProbeSupport {
private static final long WORKER_DRAIN_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(120);
private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>());
@@ -208,7 +208,7 @@ final class RealPackProbeSupport {
throw new IllegalArgumentException(name + " must be true or false.");
}
static GeneratedChunk generateChunk(Engine engine, int chunkX, int chunkZ) {
public static GeneratedChunk generateChunk(Engine engine, int chunkX, int chunkZ) {
int height = engine.getTarget().getHeight();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, height, 16);
@@ -220,14 +220,14 @@ final class RealPackProbeSupport {
return new GeneratedChunk(chunkX, chunkZ, height, blocks, biomes);
}
record GeneratedChunk(
public record GeneratedChunk(
int chunkX,
int chunkZ,
int height,
Hunk<NativeBlockState> blocks,
Hunk<NativeBiome> biomes
) {
GeneratedChunk {
public GeneratedChunk {
if (height < 1 || blocks == null || biomes == null) {
throw new IllegalArgumentException("Generated chunk requires buffers and a positive height.");
}
@@ -34,6 +34,7 @@ import art.arcane.volmlib.nativelib.item.NativeItem;
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 art.arcane.iris.generation.geometry.IrisBlockVector;
@@ -43,6 +44,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
@@ -88,6 +90,7 @@ public final class StubPlatform implements IrisPlatform {
private final StubScheduler scheduler = new StubScheduler();
private final StubStructureHooks structureHooks = new StubStructureHooks();
private final StubBiomeWriter biomeWriter = new StubBiomeWriter();
private final StubVanillaDataSource vanillaData = new StubVanillaDataSource();
private final File dataFolder;
public StubPlatform() {
@@ -620,6 +623,18 @@ public final class StubPlatform implements IrisPlatform {
}
}
private static final class StubVanillaDataSource implements VanillaDataSource {
@Override
public Optional<byte[]> read(String dataPath) {
return Optional.empty();
}
@Override
public List<String> list(String prefix) {
return List.of();
}
}
private record StubEntityType(String key) implements NativeEntityType {
@Override
public String namespace() {
@@ -783,6 +798,11 @@ public final class StubPlatform implements IrisPlatform {
return biomeWriter;
}
@Override
public VanillaDataSource vanillaData() {
return vanillaData;
}
@Override
public File dataFolder() {
return dataFolder;
@@ -0,0 +1,136 @@
package art.arcane.iris.probe.fidelity;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
public record BiomeShareReport(FidelityReport mean, SortedMap<Long, FidelityReport> seeds, Map<String, Double> irisShares,
Map<String, Double> exportShares) {
private static final String SEED_HEADER = "seed";
private static final String PASSING_HEADER = "passing";
private static final String MISSING = "-";
public BiomeShareReport {
Objects.requireNonNull(mean, "mean");
seeds = Collections.unmodifiableSortedMap(new TreeMap<>(seeds));
if (seeds.isEmpty()) {
throw new IllegalArgumentException("A biome share report needs at least one seed");
}
irisShares = Collections.unmodifiableSortedMap(new TreeMap<>(irisShares));
exportShares = Collections.unmodifiableSortedMap(new TreeMap<>(exportShares));
}
public boolean passed() {
return mean.passed();
}
public int seedsPassing(String metric) {
int passing = 0;
for (FidelityReport report : seeds.values()) {
FidelityMetric found = find(report, metric);
if (found != null && found.pass()) {
passing++;
}
}
return passing;
}
public int seedsCompared(String metric) {
int compared = 0;
for (FidelityReport report : seeds.values()) {
if (find(report, metric) != null) {
compared++;
}
}
return compared;
}
public String toJson() {
JsonArray seedEntries = new JsonArray(seeds.size());
for (Map.Entry<Long, FidelityReport> entry : seeds.entrySet()) {
JsonObject seed = entry.getValue().toJsonObject();
seed.addProperty("seed", entry.getKey());
seedEntries.add(seed);
}
JsonObject passing = new JsonObject();
for (String metric : metricNames()) {
JsonObject counts = new JsonObject();
counts.addProperty("passing", seedsPassing(metric));
counts.addProperty("compared", seedsCompared(metric));
passing.add(metric, counts);
}
Set<String> biomes = new TreeSet<>(irisShares.keySet());
biomes.addAll(exportShares.keySet());
JsonObject shares = new JsonObject();
for (String biome : biomes) {
JsonObject share = new JsonObject();
share.addProperty("iris", irisShares.getOrDefault(biome, 0.0D));
share.addProperty("export", exportShares.getOrDefault(biome, 0.0D));
shares.add(biome, share);
}
JsonObject root = new JsonObject();
root.addProperty("passed", passed());
root.add("mean", mean.toJsonObject());
root.add("shares", shares);
root.add("seedsPassing", passing);
root.add("seeds", seedEntries);
return new GsonBuilder().setPrettyPrinting().create().toJson(root);
}
public String toTable() {
List<String> names = metricNames();
int width = SEED_HEADER.length();
for (String name : names) {
width = Math.max(width, name.length());
}
StringBuilder table = new StringBuilder();
table.append(String.format(Locale.ROOT, "%-" + width + "s", SEED_HEADER));
for (long seed : seeds.keySet()) {
table.append(String.format(Locale.ROOT, " %10s", seed));
}
table.append(String.format(Locale.ROOT, " %8s\n", PASSING_HEADER));
for (String name : names) {
table.append(String.format(Locale.ROOT, "%-" + width + "s", name));
for (FidelityReport report : seeds.values()) {
FidelityMetric metric = find(report, name);
table.append(metric == null ? String.format(Locale.ROOT, " %10s", MISSING) : String.format(Locale.ROOT, " %10.4f", metric.delta()));
}
table.append(String.format(Locale.ROOT, " %8s\n", seedsPassing(name) + "/" + seedsCompared(name)));
}
return table.append(mean.toTable()).toString();
}
private List<String> metricNames() {
Set<String> names = new LinkedHashSet<>();
for (FidelityMetric metric : mean.metrics()) {
names.add(metric.name());
}
for (FidelityReport report : seeds.values()) {
for (FidelityMetric metric : report.metrics()) {
names.add(metric.name());
}
}
return new ArrayList<>(names);
}
private static FidelityMetric find(FidelityReport report, String name) {
for (FidelityMetric metric : report.metrics()) {
if (metric.name().equals(name)) {
return metric;
}
}
return null;
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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 BiomeTypes {
private static final String LAND = "land";
private static final String SEA = "sea";
private static final String SHORE = "shore";
private static final String JOINER = "+";
private BiomeTypes() {
}
public static SortedMap<String, String> of(List<IrisRegion> regions, Function<String, IrisBiome> biomes) {
SortedMap<String, SortedSet<String>> types = new TreeMap<>();
for (IrisRegion region : regions) {
visit(region.getLandBiomes(), LAND, biomes, types);
visit(region.getSeaBiomes(), SEA, biomes, types);
visit(region.getShoreBiomes(), SHORE, biomes, types);
}
SortedMap<String, String> joined = new TreeMap<>();
for (Map.Entry<String, SortedSet<String>> entry : types.entrySet()) {
joined.put(entry.getKey(), String.join(JOINER, entry.getValue()));
}
return Collections.unmodifiableSortedMap(joined);
}
private static void visit(List<String> roots, String type, Function<String, IrisBiome> biomes, SortedMap<String, SortedSet<String>> types) {
Deque<String> pending = new ArrayDeque<>(roots);
Set<String> seen = new HashSet<>();
while (!pending.isEmpty()) {
String key = pending.pop();
if (!seen.add(key)) {
continue;
}
types.computeIfAbsent(key, ignored -> new TreeSet<>()).add(type);
IrisBiome biome = biomes.apply(key);
if (biome != null) {
pending.addAll(biome.getChildren());
}
}
}
}
@@ -0,0 +1,23 @@
package art.arcane.iris.probe.fidelity;
final class CaveWalls {
private static final int CHUNK_WIDTH = 16;
private static final int[][] SIDES = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private CaveWalls() {
}
static boolean besideCave(FidelityChunk chunk, int localX, int y, int localZ) {
for (int[] side : SIDES) {
int x = localX + side[0];
int z = localZ + side[1];
if (x < 0 || x >= CHUNK_WIDTH || z < 0 || z >= CHUNK_WIDTH) {
continue;
}
if (y < chunk.surfaceY(x, z) && (chunk.open(x, y, z) || chunk.fluid(x, y, z))) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.probe.fidelity;
public interface ColumnSource extends AutoCloseable {
String label();
int minY();
int maxY();
FidelityColumn column(int x, int z) throws Exception;
}
@@ -0,0 +1,81 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.generation.validation.ChunkSpiralScan;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class ColumnSurvey {
private ColumnSurvey() {
}
public static FidelityStats scan(ColumnSource source, SurveyWindow window, int threads) {
Objects.requireNonNull(source, "source");
Objects.requireNonNull(window, "window");
List<int[]> rows = new ArrayList<>(window.perAxis());
for (int row = 0; row < window.perAxis(); row++) {
rows.add(new int[]{row, 0});
}
Accumulator accumulator = new Accumulator(source, window);
int failures = runScan(source, rows, threads, accumulator);
if (failures > 0) {
throw new IllegalStateException(failures + " of " + rows.size() + " survey rows could not be sampled from "
+ source.label(), accumulator.firstFailure());
}
return accumulator.stats();
}
private static int runScan(ColumnSource source, List<int[]> rows, int threads, Accumulator accumulator) {
try {
return ChunkSpiralScan.scan(rows, threads, accumulator::sample, accumulator);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Column survey of " + source.label() + " was interrupted", e);
}
}
private static final class Accumulator implements ChunkSpiralScan.Listener {
private final ColumnSource source;
private final SurveyWindow window;
private final AtomicReference<Throwable> firstFailure = new AtomicReference<>();
private FidelityStats stats;
private Accumulator(ColumnSource source, SurveyWindow window) {
this.source = source;
this.window = window;
this.stats = new FidelityStats(source.minY(), source.maxY());
}
@Override
public void chunkDone(int row, int ignored, boolean ok) {
}
@Override
public void chunkFailed(int row, int ignored, Throwable error) {
firstFailure.compareAndSet(null, error);
}
private void sample(int row, int ignored) throws Exception {
FidelityStats rowStats = new FidelityStats(source.minY(), source.maxY());
int x = window.coordinate(window.centerX(), row);
for (int column = 0; column < window.perAxis(); column++) {
rowStats.accept(source.column(x, window.coordinate(window.centerZ(), column)));
}
merge(rowStats);
}
private synchronized void merge(FidelityStats rowStats) {
stats = stats.merge(rowStats);
}
private synchronized FidelityStats stats() {
return stats;
}
private Throwable firstFailure() {
return firstFailure.get();
}
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.probe.fidelity;
import java.util.List;
import java.util.Set;
final class FidelityBlocks {
static final String AIR = "minecraft:air";
static final String LAVA = "minecraft:lava";
private static final Set<String> OPEN = Set.of(AIR, "minecraft:cave_air", "minecraft:void_air");
private static final Set<String> FLUID = Set.of("minecraft:water", LAVA, "minecraft:bubble_column",
"minecraft:kelp", "minecraft:kelp_plant", "minecraft:seagrass", "minecraft:tall_seagrass");
private static final List<String> DECORATION_SUFFIXES = List.of("_log", "_wood", "_leaves", "_stem", "_hyphae",
"_mushroom_block", "_wart_block");
private FidelityBlocks() {
}
static String id(String stateKey) {
int properties = stateKey.indexOf('[');
return properties < 0 ? stateKey : stateKey.substring(0, properties);
}
static boolean open(String id) {
return OPEN.contains(id);
}
static boolean lava(String id) {
return LAVA.equals(id);
}
static boolean fluid(String id) {
return FLUID.contains(id);
}
static boolean decoration(String id) {
for (String suffix : DECORATION_SUFFIXES) {
if (id.endsWith(suffix)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,34 @@
package art.arcane.iris.probe.fidelity;
public interface FidelityChunk {
int surfaceY(int localX, int localZ);
String surfaceBiome(int localX, int localZ);
boolean open(int localX, int y, int localZ);
boolean fluid(int localX, int y, int localZ);
String block(int localX, int y, int localZ);
default boolean lava(int localX, int y, int localZ) {
String id = block(localX, y, localZ);
return id != null && FidelityBlocks.lava(id);
}
default boolean terrainMass(int localX, int y, int localZ) {
return true;
}
default boolean river(int localX, int localZ) {
return false;
}
default boolean wallsKnown() {
return false;
}
default boolean wall(int localX, int y, int localZ) {
return false;
}
}
@@ -0,0 +1,4 @@
package art.arcane.iris.probe.fidelity;
public record FidelityColumn(String biome, int terrainY, boolean wet) {
}
@@ -0,0 +1,225 @@
package art.arcane.iris.probe.fidelity;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
public final class FidelityComparison {
private static final double MIN_BIOME_AREA = 0.02D;
private static final List<Integer> HEIGHT_PERCENTILES = List.of(10, 50, 90);
private static final long MIN_BAND_COLUMNS = 64L;
private static final String UNKNOWN_TYPE = "unknown";
private FidelityComparison() {
}
public static FidelityReport compare(FidelitySamples columns, FidelitySamples chunks, FidelityThresholds thresholds) {
Objects.requireNonNull(columns, "columns");
Objects.requireNonNull(chunks, "chunks");
Objects.requireNonNull(thresholds, "thresholds");
List<FidelityMetric> metrics = new ArrayList<>();
addBiomeMetrics(columns.iris().biomeShares(), columns.export().biomeShares(), thresholds.biomeShare(), metrics);
addHeightMetrics(columns.iris(), columns.export(), thresholds.heightQuantile(), metrics);
List<FidelityObservation> observations = new ArrayList<>();
addCaveMetrics(chunks.iris(), chunks.export(), thresholds.caveBand(), metrics, observations);
addWallMetric(chunks.iris(), chunks.export(), thresholds.wallShare(), metrics);
addDecorationMetric(chunks.iris(), chunks.export(), thresholds.objectDensity(), metrics);
addLavaObservation(chunks.iris(), chunks.export(), observations);
addOneBlockWallObservation(chunks.iris(), chunks.export(), observations);
return new FidelityReport(metrics, List.of(columns.sampling(), chunks.sampling()), observations);
}
public static BiomeShareReport compareBiomeShares(List<FidelitySamples> seeds, Map<String, String> biomeTypes, FidelityThresholds thresholds) {
Objects.requireNonNull(seeds, "seeds");
Objects.requireNonNull(biomeTypes, "biomeTypes");
Objects.requireNonNull(thresholds, "thresholds");
if (seeds.isEmpty()) {
throw new IllegalArgumentException("Biome shares need at least one surveyed seed");
}
SortedMap<Long, FidelityReport> reports = new TreeMap<>();
List<Map<String, Double>> irisShares = new ArrayList<>(seeds.size());
List<Map<String, Double>> exportShares = new ArrayList<>(seeds.size());
double irisLand = 0.0D;
double exportLand = 0.0D;
List<Long> seedList = new ArrayList<>(seeds.size());
long columns = 0L;
for (FidelitySamples seed : seeds) {
Map<String, Double> iris = seed.iris().biomeShares();
Map<String, Double> export = seed.export().biomeShares();
irisShares.add(iris);
exportShares.add(export);
double seedIrisLand = landFraction(seed.iris());
double seedExportLand = landFraction(seed.export());
irisLand += seedIrisLand;
exportLand += seedExportLand;
seedList.addAll(seed.sampling().seeds());
columns += seed.sampling().columns();
reports.put(seed.sampling().seeds().getFirst(), biomeReport(iris, export, seedIrisLand, seedExportLand, biomeTypes, List.of(seed.sampling()),
thresholds));
}
FidelitySampling first = seeds.getFirst().sampling();
FidelitySampling pooled = new FidelitySampling(first.scope(), first.windowBlocks(), first.strideBlocks(), seedList, columns);
Map<String, Double> irisMean = mean(irisShares);
Map<String, Double> exportMean = mean(exportShares);
FidelityReport mean = biomeReport(irisMean, exportMean, irisLand / seeds.size(), exportLand / seeds.size(), biomeTypes, List.of(pooled),
thresholds);
return new BiomeShareReport(mean, reports, irisMean, exportMean);
}
private static FidelityReport biomeReport(Map<String, Double> irisShares, Map<String, Double> exportShares, double irisLand, double exportLand,
Map<String, String> biomeTypes, List<FidelitySampling> sampling, FidelityThresholds thresholds) {
List<FidelityMetric> metrics = new ArrayList<>();
addBiomeMetrics(irisShares, exportShares, thresholds.biomeShare(), metrics);
List<FidelityObservation> observations = new ArrayList<>();
Map<String, Double> irisTypes = typeShares(irisShares, biomeTypes);
Map<String, Double> exportTypes = typeShares(exportShares, biomeTypes);
Set<String> types = new TreeSet<>(irisTypes.keySet());
types.addAll(exportTypes.keySet());
for (String type : types) {
observations.add(new FidelityObservation("biome.type." + type, irisTypes.getOrDefault(type, 0.0D), exportTypes.getOrDefault(type, 0.0D)));
}
observations.add(new FidelityObservation("height.landFraction", irisLand, exportLand));
return new FidelityReport(metrics, sampling, observations);
}
private static double landFraction(FidelityStats stats) {
return stats.heightColumns() == 0L ? 0.0D : stats.landFraction();
}
private static Map<String, Double> mean(List<Map<String, Double>> shares) {
SortedMap<String, Double> mean = new TreeMap<>();
for (Map<String, Double> seed : shares) {
for (Map.Entry<String, Double> entry : seed.entrySet()) {
mean.merge(entry.getKey(), entry.getValue() / shares.size(), Double::sum);
}
}
return mean;
}
private static Map<String, Double> typeShares(Map<String, Double> shares, Map<String, String> biomeTypes) {
SortedMap<String, Double> types = new TreeMap<>();
for (Map.Entry<String, Double> entry : shares.entrySet()) {
types.merge(biomeTypes.getOrDefault(entry.getKey(), UNKNOWN_TYPE), entry.getValue(), Double::sum);
}
return types;
}
private static void addBiomeMetrics(Map<String, Double> irisShares, Map<String, Double> exportShares, double threshold, List<FidelityMetric> metrics) {
if (irisShares.isEmpty() || exportShares.isEmpty()) {
return;
}
Set<String> biomes = new TreeSet<>(irisShares.keySet());
biomes.addAll(exportShares.keySet());
double difference = 0.0D;
for (String biome : biomes) {
difference += Math.abs(exportShares.getOrDefault(biome, 0.0D) - irisShares.getOrDefault(biome, 0.0D));
}
double distance = difference / 2.0D;
metrics.add(new FidelityMetric("biome.totalVariation", 0.0D, distance, distance, distance <= threshold));
for (Map.Entry<String, Double> entry : irisShares.entrySet()) {
double share = entry.getValue();
if (share < MIN_BIOME_AREA) {
continue;
}
double exported = exportShares.getOrDefault(entry.getKey(), 0.0D);
double error = relativeError(share, exported);
metrics.add(new FidelityMetric("biome.share." + entry.getKey(), share, exported, error, error <= threshold));
}
}
private static void addHeightMetrics(FidelityStats iris, FidelityStats export, double threshold, List<FidelityMetric> metrics) {
if (iris.heightColumns() == 0L) {
return;
}
if (export.heightColumns() == 0L) {
metrics.add(new FidelityMetric("height.terrainColumns", iris.heightColumns(), 0.0D, 1.0D, false));
return;
}
double range = iris.maxY() - iris.minY();
for (int percentile : HEIGHT_PERCENTILES) {
double irisY = iris.heightQuantile(percentile / 100.0D);
double exportY = export.heightQuantile(percentile / 100.0D);
double delta = Math.abs(exportY - irisY) / range;
metrics.add(new FidelityMetric("height.p" + percentile, irisY, exportY, delta, delta <= threshold));
}
double irisLand = iris.landFraction();
double exportLand = export.landFraction();
double landDelta = Math.abs(exportLand - irisLand);
metrics.add(new FidelityMetric("height.landFraction", irisLand, exportLand, landDelta, landDelta <= threshold));
}
private static void addCaveMetrics(FidelityStats iris, FidelityStats export, double threshold, List<FidelityMetric> metrics,
List<FidelityObservation> observations) {
long[] irisVoxels = iris.subsurfaceVoxels();
long[] exportVoxels = export.subsurfaceVoxels();
long[] irisVoids = iris.voidVoxels();
long[] exportVoids = export.voidVoxels();
long minimum = MIN_BAND_COLUMNS * FidelityStats.BAND_HEIGHT;
for (int band = 0; band < irisVoxels.length; band++) {
if (irisVoxels[band] == 0L || exportVoxels[band] == 0L) {
continue;
}
boolean judged = irisVoxels[band] >= minimum && exportVoxels[band] >= minimum;
addBandShare("cave.band." + iris.bandMinY(band), iris.caveShare(band), export.caveShare(band), judged, threshold, metrics, observations);
if (irisVoids[band] > 0L || exportVoids[band] > 0L) {
addBandShare("void.band." + iris.bandMinY(band), iris.voidShare(band), export.voidShare(band), judged, threshold, metrics, observations);
}
}
}
private static void addBandShare(String name, double irisShare, double exportShare, boolean judged, double threshold, List<FidelityMetric> metrics,
List<FidelityObservation> observations) {
if (!judged) {
observations.add(new FidelityObservation(name, irisShare, exportShare));
return;
}
double delta = Math.abs(exportShare - irisShare);
metrics.add(new FidelityMetric(name, irisShare, exportShare, delta, delta <= threshold));
}
private static void addWallMetric(FidelityStats iris, FidelityStats export, double threshold, List<FidelityMetric> metrics) {
if (iris.wallSolidVoxels() == 0L || export.wallSolidVoxels() == 0L) {
return;
}
double irisShare = iris.wallShare();
double exportShare = export.wallShare();
double error = relativeError(irisShare, exportShare);
metrics.add(new FidelityMetric("cave.wallShare", irisShare, exportShare, error, error <= threshold));
}
private static void addLavaObservation(FidelityStats iris, FidelityStats export, List<FidelityObservation> observations) {
if (iris.deepVoxels() == 0L || export.deepVoxels() == 0L) {
return;
}
observations.add(new FidelityObservation("cave.lavaShare.belowY" + FidelityStats.DEEP_LAVA_LINE, iris.deepLavaShare(), export.deepLavaShare()));
}
private static void addOneBlockWallObservation(FidelityStats iris, FidelityStats export, List<FidelityObservation> observations) {
if (iris.wallSolidVoxels() == 0L || export.wallSolidVoxels() == 0L) {
return;
}
observations.add(new FidelityObservation("cave.wallShare.oneBlock", iris.oneBlockWallShare(), export.oneBlockWallShare()));
}
private static void addDecorationMetric(FidelityStats iris, FidelityStats export, double threshold, List<FidelityMetric> metrics) {
if (iris.blockColumns() == 0L || export.blockColumns() == 0L) {
return;
}
double irisDensity = iris.decorationPerSquareKilometre();
double exportDensity = export.decorationPerSquareKilometre();
double error = relativeError(irisDensity, exportDensity);
metrics.add(new FidelityMetric("decoration.columnsPerKm2", irisDensity, exportDensity, error, error <= threshold));
}
private static double relativeError(double reference, double measured) {
if (reference == 0.0D) {
return measured == 0.0D ? 0.0D : 1.0D;
}
return Math.abs(measured - reference) / reference;
}
}
@@ -0,0 +1,13 @@
package art.arcane.iris.probe.fidelity;
import java.util.Objects;
public record FidelityMetric(String name, double iris, double export, double delta, boolean pass) {
public FidelityMetric {
Objects.requireNonNull(name, "name");
if (!Double.isFinite(iris) || !Double.isFinite(export) || !Double.isFinite(delta)) {
throw new IllegalArgumentException("Fidelity metric " + name + " has a non-finite value: iris=" + iris
+ " export=" + export + " delta=" + delta);
}
}
}
@@ -0,0 +1,12 @@
package art.arcane.iris.probe.fidelity;
import java.util.Objects;
public record FidelityObservation(String name, double iris, double export) {
public FidelityObservation {
Objects.requireNonNull(name, "name");
if (!Double.isFinite(iris) || !Double.isFinite(export)) {
throw new IllegalArgumentException("Fidelity observation " + name + " has a non-finite value: iris=" + iris + " export=" + export);
}
}
}
@@ -0,0 +1,111 @@
package art.arcane.iris.probe.fidelity;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Locale;
public record FidelityReport(List<FidelityMetric> metrics, List<FidelitySampling> sampling, List<FidelityObservation> observations) {
private static final String NAME_HEADER = "metric";
private static final String OBSERVATION_VERDICT = "INFO";
public FidelityReport {
metrics = List.copyOf(metrics);
sampling = List.copyOf(sampling);
observations = List.copyOf(observations);
}
public boolean passed() {
if (metrics.isEmpty()) {
return false;
}
for (FidelityMetric metric : metrics) {
if (!metric.pass()) {
return false;
}
}
return true;
}
public String toJson() {
return new GsonBuilder().setPrettyPrinting().create().toJson(toJsonObject());
}
public JsonObject toJsonObject() {
JsonArray entries = new JsonArray(metrics.size());
for (FidelityMetric metric : metrics) {
JsonObject entry = new JsonObject();
entry.addProperty("name", metric.name());
entry.addProperty("iris", metric.iris());
entry.addProperty("export", metric.export());
entry.addProperty("delta", metric.delta());
entry.addProperty("pass", metric.pass());
entries.add(entry);
}
JsonArray samplingEntries = new JsonArray(sampling.size());
for (FidelitySampling scope : sampling) {
JsonObject entry = new JsonObject();
entry.addProperty("scope", scope.scope());
entry.addProperty("windowBlocks", scope.windowBlocks());
entry.addProperty("strideBlocks", scope.strideBlocks());
JsonArray seeds = new JsonArray(scope.seeds().size());
for (long seed : scope.seeds()) {
seeds.add(seed);
}
entry.add("seeds", seeds);
entry.addProperty("columns", scope.columns());
samplingEntries.add(entry);
}
JsonArray notes = new JsonArray(observations.size());
for (FidelityObservation observation : observations) {
JsonObject entry = new JsonObject();
entry.addProperty("name", observation.name());
entry.addProperty("iris", observation.iris());
entry.addProperty("export", observation.export());
notes.add(entry);
}
JsonObject root = new JsonObject();
root.addProperty("passed", passed());
root.add("sampling", samplingEntries);
root.add("metrics", entries);
root.add("observations", notes);
return root;
}
public String toTable() {
int width = NAME_HEADER.length();
for (FidelityMetric metric : metrics) {
width = Math.max(width, metric.name().length());
}
for (FidelityObservation observation : observations) {
width = Math.max(width, observation.name().length());
}
String header = "%-" + width + "s %12s %12s %10s %s\n";
String row = "%-" + width + "s %12.4f %12.4f %10.4f %s\n";
StringBuilder table = new StringBuilder();
for (FidelitySampling scope : sampling) {
table.append(String.format(Locale.ROOT, "sampling %s window=%d stride=%d seeds=%s columns=%d\n", scope.scope(),
scope.windowBlocks(), scope.strideBlocks(), scope.seeds(), scope.columns()));
}
table.append(String.format(Locale.ROOT, header, NAME_HEADER, "iris", "export", "delta", "result"));
int passing = 0;
for (FidelityMetric metric : metrics) {
if (metric.pass()) {
passing++;
}
table.append(String.format(Locale.ROOT, row, metric.name(), metric.iris(), metric.export(), metric.delta(),
metric.pass() ? "PASS" : "FAIL"));
}
String note = "%-" + width + "s %12.4f %12.4f %10s %s\n";
for (FidelityObservation observation : observations) {
table.append(String.format(Locale.ROOT, note, observation.name(), observation.iris(), observation.export(), "", OBSERVATION_VERDICT));
}
if (metrics.isEmpty()) {
return table.append("overall FAIL: no metrics were compared\n").toString();
}
return table.append("overall ").append(passed() ? "PASS" : "FAIL").append(": ").append(passing).append(" of ")
.append(metrics.size()).append(" metrics passed\n").toString();
}
}
@@ -0,0 +1,15 @@
package art.arcane.iris.probe.fidelity;
import java.util.Objects;
public record FidelitySamples(FidelityStats iris, FidelityStats export, FidelitySampling sampling) {
public FidelitySamples {
Objects.requireNonNull(iris, "iris");
Objects.requireNonNull(export, "export");
Objects.requireNonNull(sampling, "sampling");
if (iris.minY() != export.minY() || iris.maxY() != export.maxY()) {
throw new IllegalArgumentException("Iris height range [" + iris.minY() + ", " + iris.maxY()
+ ") differs from export height range [" + export.minY() + ", " + export.maxY() + ")");
}
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.probe.fidelity;
import java.util.List;
import java.util.Objects;
public record FidelitySampling(String scope, int windowBlocks, int strideBlocks, List<Long> seeds, long columns) {
public FidelitySampling {
Objects.requireNonNull(scope, "scope");
seeds = List.copyOf(seeds);
if (seeds.isEmpty()) {
throw new IllegalArgumentException("Fidelity sampling '" + scope + "' lists no seed");
}
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.generation.validation.ChunkSpiralScan;
import art.arcane.volmlib.util.math.ChunkSpiral;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class FidelityScanner {
private FidelityScanner() {
}
public static FidelityStats scan(FidelitySource source, int centerChunkX, int centerChunkZ, int radiusChunks, int stride, int threads) {
Objects.requireNonNull(source, "source");
FidelityStats.requireStride(stride);
List<int[]> targets = ChunkSpiral.centerOut(centerChunkX, centerChunkZ, Math.max(0, radiusChunks));
Accumulator accumulator = new Accumulator(source, stride);
int failures = runScan(source, targets, threads, accumulator);
if (failures > 0) {
throw new IllegalStateException(failures + " of " + targets.size() + " chunks could not be sampled from "
+ source.label(), accumulator.firstFailure());
}
return accumulator.stats();
}
private static int runScan(FidelitySource source, List<int[]> targets, int threads, Accumulator accumulator) {
try {
return ChunkSpiralScan.scan(targets, threads, accumulator::sample, accumulator);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Fidelity scan of " + source.label() + " was interrupted", e);
}
}
private static final class Accumulator implements ChunkSpiralScan.Listener {
private final FidelitySource source;
private final int stride;
private final AtomicReference<Throwable> firstFailure = new AtomicReference<>();
private FidelityStats stats;
private Accumulator(FidelitySource source, int stride) {
this.source = source;
this.stride = stride;
this.stats = new FidelityStats(source.minY(), source.maxY());
}
@Override
public void chunkDone(int chunkX, int chunkZ, boolean ok) {
}
@Override
public void chunkFailed(int chunkX, int chunkZ, Throwable error) {
firstFailure.compareAndSet(null, error);
}
private void sample(int chunkX, int chunkZ) throws Exception {
FidelityStats chunkStats = new FidelityStats(source.minY(), source.maxY());
chunkStats.accept(source.sample(chunkX, chunkZ), stride);
merge(chunkStats);
}
private synchronized void merge(FidelityStats chunkStats) {
stats = stats.merge(chunkStats);
}
private synchronized FidelityStats stats() {
return stats;
}
private Throwable firstFailure() {
return firstFailure.get();
}
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.probe.fidelity;
public interface FidelitySource extends AutoCloseable {
String label();
int minY();
int maxY();
FidelityChunk sample(int chunkX, int chunkZ) throws Exception;
}
@@ -0,0 +1,390 @@
package art.arcane.iris.probe.fidelity;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
public final class FidelityStats {
public static final int DEEP_LAVA_LINE = -54;
private static final int CHUNK_WIDTH = 16;
static final int BAND_HEIGHT = 16;
private static final int MIN_ROOF_THICKNESS = 4;
private static final double SQUARE_METRES_PER_SQUARE_KILOMETRE = 1_000_000.0D;
private final int minY;
private final int maxY;
private final long[] heightHistogram;
private final long[] subsurfaceVoxels;
private final long[] caveVoxels;
private final long[] voidVoxels;
private final SortedMap<String, Long> biomeColumns;
private long columns;
private long heightColumns;
private long fluidColumns;
private long blockColumns;
private long decorationColumns;
private long riverColumns;
private long deepVoxels;
private long deepLavaVoxels;
private long wallSolidVoxels;
private long wallVoxels;
private long oneBlockWallVoxels;
public FidelityStats(int minY, int maxY) {
if (maxY <= minY) {
throw new IllegalArgumentException("Fidelity height range [" + minY + ", " + maxY + ") is empty");
}
this.minY = minY;
this.maxY = maxY;
int height = maxY - minY;
int bands = (height + BAND_HEIGHT - 1) / BAND_HEIGHT;
this.heightHistogram = new long[height];
this.subsurfaceVoxels = new long[bands];
this.caveVoxels = new long[bands];
this.voidVoxels = new long[bands];
this.biomeColumns = new TreeMap<>();
}
public void accept(FidelityChunk chunk, int stride) {
Objects.requireNonNull(chunk, "chunk");
requireStride(stride);
for (int x = 0; x < CHUNK_WIDTH; x += stride) {
for (int z = 0; z < CHUNK_WIDTH; z += stride) {
acceptColumn(chunk, x, z);
}
}
}
public void accept(FidelityColumn column) {
Objects.requireNonNull(column, "column");
columns++;
if (column.biome() != null) {
biomeColumns.merge(column.biome(), 1L, Long::sum);
}
int terrainY = column.terrainY();
if (terrainY >= maxY) {
throw new IllegalArgumentException("Terrain Y " + terrainY + " is above the height range [" + minY + ", " + maxY + ")");
}
if (terrainY < minY) {
return;
}
heightColumns++;
heightHistogram[terrainY - minY]++;
if (column.wet()) {
fluidColumns++;
}
}
public FidelityStats merge(FidelityStats other) {
Objects.requireNonNull(other, "other");
if (other.minY != minY || other.maxY != maxY) {
throw new IllegalArgumentException("Cannot merge fidelity stats over [" + minY + ", " + maxY + ") with ["
+ other.minY + ", " + other.maxY + ")");
}
FidelityStats merged = new FidelityStats(minY, maxY);
merged.add(this);
merged.add(other);
return merged;
}
public int minY() {
return minY;
}
public int maxY() {
return maxY;
}
public long columns() {
return columns;
}
public long heightColumns() {
return heightColumns;
}
public long fluidColumns() {
return fluidColumns;
}
public long blockColumns() {
return blockColumns;
}
public long decorationColumns() {
return decorationColumns;
}
public long riverColumns() {
return riverColumns;
}
public long deepVoxels() {
return deepVoxels;
}
public long deepLavaVoxels() {
return deepLavaVoxels;
}
public long wallSolidVoxels() {
return wallSolidVoxels;
}
public long wallVoxels() {
return wallVoxels;
}
public long oneBlockWallVoxels() {
return oneBlockWallVoxels;
}
public Map<String, Long> biomeColumns() {
return Collections.unmodifiableSortedMap(biomeColumns);
}
public long[] heightHistogram() {
return heightHistogram.clone();
}
public long[] subsurfaceVoxels() {
return subsurfaceVoxels.clone();
}
public long[] caveVoxels() {
return caveVoxels.clone();
}
public long[] voidVoxels() {
return voidVoxels.clone();
}
public int bandCount() {
return subsurfaceVoxels.length;
}
public int bandMinY(int band) {
return minY + band * BAND_HEIGHT;
}
public Map<String, Double> biomeShares() {
long resolved = 0L;
for (long count : biomeColumns.values()) {
resolved += count;
}
SortedMap<String, Double> shares = new TreeMap<>();
for (Map.Entry<String, Long> entry : biomeColumns.entrySet()) {
shares.put(entry.getKey(), entry.getValue() / (double) resolved);
}
return Collections.unmodifiableSortedMap(shares);
}
public double heightQuantile(double fraction) {
if (heightColumns == 0L) {
throw new IllegalStateException("No terrain columns were sampled");
}
if (!(fraction >= 0.0D && fraction <= 1.0D)) {
throw new IllegalArgumentException("Quantile fraction must be within [0, 1], got " + fraction);
}
long rank = Math.max(1L, (long) Math.ceil(fraction * heightColumns));
long seen = 0L;
for (int index = 0; index < heightHistogram.length; index++) {
seen += heightHistogram[index];
if (seen >= rank) {
return minY + index;
}
}
return maxY - 1;
}
public double landFraction() {
if (heightColumns == 0L) {
throw new IllegalStateException("No terrain columns were sampled");
}
return (heightColumns - fluidColumns) / (double) heightColumns;
}
public double caveShare(int band) {
return bandShare(caveVoxels, band);
}
public double voidShare(int band) {
return bandShare(voidVoxels, band);
}
public double deepLavaShare() {
if (deepVoxels == 0L) {
throw new IllegalStateException("No subsurface voxels were sampled below Y " + DEEP_LAVA_LINE);
}
return deepLavaVoxels / (double) deepVoxels;
}
public double wallShare() {
if (wallSolidVoxels == 0L) {
throw new IllegalStateException("No solid subsurface voxels reported cave walls");
}
return wallVoxels / (double) wallSolidVoxels;
}
public double oneBlockWallShare() {
if (wallSolidVoxels == 0L) {
throw new IllegalStateException("No solid subsurface voxels reported cave walls");
}
return oneBlockWallVoxels / (double) wallSolidVoxels;
}
public double decorationPerSquareKilometre() {
if (blockColumns == 0L) {
throw new IllegalStateException("No sampled column reported block ids");
}
return decorationColumns / (double) blockColumns * SQUARE_METRES_PER_SQUARE_KILOMETRE;
}
static void requireStride(int stride) {
if (stride < 1 || stride > CHUNK_WIDTH) {
throw new IllegalArgumentException("Column stride must be within [1, " + CHUNK_WIDTH + "], got " + stride);
}
}
private void acceptColumn(FidelityChunk chunk, int x, int z) {
columns++;
String biome = chunk.surfaceBiome(x, z);
if (biome != null) {
biomeColumns.merge(biome, 1L, Long::sum);
}
if (chunk.river(x, z)) {
riverColumns++;
}
int surfaceY = chunk.surfaceY(x, z);
if (surfaceY >= maxY) {
throw new IllegalArgumentException("Surface Y " + surfaceY + " at column " + x + "," + z
+ " is above the height range [" + minY + ", " + maxY + ")");
}
if (surfaceY < minY) {
return;
}
int terrainY = surfaceY;
String top = chunk.block(x, surfaceY, z);
if (top != null) {
blockColumns++;
if (FidelityBlocks.decoration(top)) {
decorationColumns++;
terrainY = terrainBelow(chunk, x, z, surfaceY);
}
}
if (terrainY < minY) {
return;
}
heightColumns++;
heightHistogram[terrainY - minY]++;
if (terrainY + 1 < maxY && chunk.fluid(x, terrainY + 1, z)) {
fluidColumns++;
}
int caveCeiling = caveCeiling(chunk, x, z, terrainY);
boolean walls = chunk.wallsKnown() && interior(x, z);
for (int y = minY; y < caveCeiling; y++) {
int band = (y - minY) / BAND_HEIGHT;
subsurfaceVoxels[band]++;
boolean open = chunk.open(x, y, z);
if (open) {
if (chunk.terrainMass(x, y, z)) {
caveVoxels[band]++;
} else {
voidVoxels[band]++;
}
}
if (walls && !open && !chunk.fluid(x, y, z)) {
wallSolidVoxels++;
if (chunk.wall(x, y, z)) {
wallVoxels++;
}
if (CaveWalls.besideCave(chunk, x, y, z)) {
oneBlockWallVoxels++;
}
}
if (y < DEEP_LAVA_LINE) {
deepVoxels++;
if (chunk.lava(x, y, z)) {
deepLavaVoxels++;
}
}
}
}
private double bandShare(long[] voxels, int band) {
if (subsurfaceVoxels[band] == 0L) {
throw new IllegalStateException("No subsurface voxels were sampled in the band starting at Y " + bandMinY(band));
}
return voxels[band] / (double) subsurfaceVoxels[band];
}
private static boolean interior(int x, int z) {
return x > 0 && x < CHUNK_WIDTH - 1 && z > 0 && z < CHUNK_WIDTH - 1;
}
private int terrainBelow(FidelityChunk chunk, int x, int z, int surfaceY) {
int y = surfaceY - 1;
while (y >= minY && aboveTerrain(chunk, x, y, z)) {
y--;
}
return y;
}
private int caveCeiling(FidelityChunk chunk, int x, int z, int terrainY) {
int top = terrainY;
while (top >= minY) {
int bottom = top;
while (bottom >= minY && top - bottom < MIN_ROOF_THICKNESS && !aboveTerrain(chunk, x, bottom, z)) {
bottom--;
}
if (top - bottom >= MIN_ROOF_THICKNESS || bottom < minY || !decoration(chunk, x, bottom, z)) {
return top;
}
top = bottom;
while (top >= minY && aboveTerrain(chunk, x, top, z)) {
top--;
}
}
return top;
}
private static boolean decoration(FidelityChunk chunk, int x, int y, int z) {
String id = chunk.block(x, y, z);
return id != null && FidelityBlocks.decoration(id);
}
private static boolean aboveTerrain(FidelityChunk chunk, int x, int y, int z) {
if (chunk.open(x, y, z) || chunk.fluid(x, y, z)) {
return true;
}
String id = chunk.block(x, y, z);
return id != null && FidelityBlocks.decoration(id);
}
private void add(FidelityStats other) {
columns += other.columns;
heightColumns += other.heightColumns;
fluidColumns += other.fluidColumns;
blockColumns += other.blockColumns;
decorationColumns += other.decorationColumns;
riverColumns += other.riverColumns;
deepVoxels += other.deepVoxels;
deepLavaVoxels += other.deepLavaVoxels;
wallSolidVoxels += other.wallSolidVoxels;
wallVoxels += other.wallVoxels;
oneBlockWallVoxels += other.oneBlockWallVoxels;
for (int index = 0; index < heightHistogram.length; index++) {
heightHistogram[index] += other.heightHistogram[index];
}
for (int band = 0; band < subsurfaceVoxels.length; band++) {
subsurfaceVoxels[band] += other.subsurfaceVoxels[band];
caveVoxels[band] += other.caveVoxels[band];
voidVoxels[band] += other.voidVoxels[band];
}
for (Map.Entry<String, Long> entry : other.biomeColumns.entrySet()) {
biomeColumns.merge(entry.getKey(), entry.getValue(), Long::sum);
}
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.probe.fidelity;
public record FidelityThresholds(double biomeShare, double heightQuantile, double caveBand, double objectDensity, double wallShare) {
public FidelityThresholds {
requireThreshold("biomeShare", biomeShare);
requireThreshold("heightQuantile", heightQuantile);
requireThreshold("caveBand", caveBand);
requireThreshold("objectDensity", objectDensity);
requireThreshold("wallShare", wallShare);
}
public static FidelityThresholds defaults() {
return new FidelityThresholds(0.15D, 0.10D, 0.15D, 0.15D, 0.25D);
}
private static void requireThreshold(String name, double value) {
if (!Double.isFinite(value) || value < 0.0D) {
throw new IllegalArgumentException("Fidelity threshold " + name + " must be a finite non-negative number, got " + value);
}
}
}
@@ -0,0 +1,51 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.world.IrisWorld;
import java.util.Objects;
public final class IrisEngineColumnSource implements ColumnSource {
private final Engine engine;
private final IrisComplex complex;
private final int minY;
private final int maxY;
private final int fluidHeight;
public IrisEngineColumnSource(Engine engine) {
this.engine = Objects.requireNonNull(engine, "engine");
this.complex = engine.getComplex();
IrisWorld world = engine.getTarget().getWorld();
this.minY = world.minHeight();
this.maxY = world.maxHeight();
this.fluidHeight = (int) Math.round(complex.getFluidHeight());
}
@Override
public String label() {
return "iris:" + engine.getDimension().getLoadKey();
}
@Override
public int minY() {
return minY;
}
@Override
public int maxY() {
return maxY;
}
@Override
public FidelityColumn column(int x, int z) {
int height = Math.min(complex.naturalTrueHeight(x, z), maxY - minY - 1);
IrisBiome biome = complex.getNaturalTrueBiomeStream().get(x, z);
return new FidelityColumn(biome == null ? null : biome.getLoadKey(), height + minY, height < fluidHeight);
}
@Override
public void close() {
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.probe.RealPackProbeSupport;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
public final class IrisEngineFidelitySource implements FidelitySource {
private static final int CHUNK_WIDTH = 16;
private final Engine engine;
private final int minY;
private final int maxY;
public IrisEngineFidelitySource(Engine engine) {
this.engine = Objects.requireNonNull(engine, "engine");
IrisWorld world = engine.getTarget().getWorld();
this.minY = world.minHeight();
this.maxY = world.maxHeight();
}
@Override
public String label() {
return "iris:" + engine.getDimension().getLoadKey();
}
@Override
public int minY() {
return minY;
}
@Override
public int maxY() {
return maxY;
}
@Override
public FidelityChunk sample(int chunkX, int chunkZ) {
return new EngineChunk(RealPackProbeSupport.generateChunk(engine, chunkX, chunkZ));
}
@Override
public void close() {
}
private final class EngineChunk implements FidelityChunk {
private final RealPackProbeSupport.GeneratedChunk chunk;
private final Map<NativeBlockState, String> ids = new IdentityHashMap<>();
private final int[] surfaces = new int[CHUNK_WIDTH * CHUNK_WIDTH];
private final TerrainMass[] masses = new TerrainMass[CHUNK_WIDTH * CHUNK_WIDTH];
private EngineChunk(RealPackProbeSupport.GeneratedChunk chunk) {
this.chunk = chunk;
Arrays.fill(surfaces, Integer.MIN_VALUE);
}
@Override
public int surfaceY(int localX, int localZ) {
int index = localZ * CHUNK_WIDTH + localX;
if (surfaces[index] == Integer.MIN_VALUE) {
surfaces[index] = scanSurface(localX, localZ);
}
return surfaces[index];
}
@Override
public String surfaceBiome(int localX, int localZ) {
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get((chunk.chunkX() << 4) + localX, (chunk.chunkZ() << 4) + localZ);
return biome == null ? null : biome.getLoadKey();
}
@Override
public boolean open(int localX, int y, int localZ) {
return FidelityBlocks.open(block(localX, y, localZ));
}
@Override
public boolean fluid(int localX, int y, int localZ) {
return FidelityBlocks.fluid(block(localX, y, localZ));
}
@Override
public String block(int localX, int y, int localZ) {
NativeBlockState state = chunk.blocks().get(localX, y - minY, localZ);
if (state == null) {
return FidelityBlocks.AIR;
}
return ids.computeIfAbsent(state, (NativeBlockState key) -> FidelityBlocks.id(key.key()));
}
@Override
public boolean terrainMass(int localX, int y, int localZ) {
int index = localZ * CHUNK_WIDTH + localX;
TerrainMass mass = masses[index];
if (mass == null) {
mass = TerrainMass.of(engine.getComplex(), (chunk.chunkX() << 4) + localX, (chunk.chunkZ() << 4) + localZ);
masses[index] = mass;
}
return mass.solid(y - minY);
}
@Override
public boolean wallsKnown() {
return true;
}
@Override
public boolean wall(int localX, int y, int localZ) {
return CaveWalls.besideCave(this, localX, y, localZ);
}
private int scanSurface(int localX, int localZ) {
for (int y = maxY - 1; y >= minY; y--) {
String id = block(localX, y, localZ);
if (!FidelityBlocks.open(id) && !FidelityBlocks.fluid(id)) {
return y;
}
}
return minY - 1;
}
}
private record TerrainMass(int top, Terrain3DColumn column) {
private static TerrainMass of(IrisComplex complex, int x, int z) {
return new TerrainMass(complex.getRoundedHeighteightStream().get(x, z), complex.terrainColumn(x, z));
}
private boolean solid(int localY) {
return localY <= top && (column == null || column.isSolid(localY));
}
}
}
@@ -0,0 +1,207 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.volmlib.util.nbt.io.NBTDeserializer;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.mca.CompressionType;
import art.arcane.volmlib.util.nbt.mca.MCAFileSupport;
import art.arcane.volmlib.util.nbt.mca.MCAUtilSupport;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
public final class RegionFileFidelitySource implements FidelitySource {
private static final int CHUNK_WIDTH = 16;
private static final int SECTION_VOLUME = 4096;
private static final long SECTOR_BYTES = 4096L;
private static final int EXTERNAL_PAYLOAD = 0x80;
private static final Set<String> FULL_STATUS = Set.of("minecraft:full", "full");
private final Path regionDirectory;
private final int minY;
private final int maxY;
public RegionFileFidelitySource(Path regionDirectory, int minY, int maxY) {
this.regionDirectory = Objects.requireNonNull(regionDirectory, "regionDirectory");
if (maxY <= minY) {
throw new IllegalArgumentException("Region height range [" + minY + ", " + maxY + ") is empty");
}
this.minY = minY;
this.maxY = maxY;
}
@Override
public String label() {
return regionDirectory.toString();
}
@Override
public int minY() {
return minY;
}
@Override
public int maxY() {
return maxY;
}
@Override
public FidelityChunk sample(int chunkX, int chunkZ) throws IOException {
CompoundTag root = readChunk(chunkX, chunkZ);
String status = root.getString("Status");
if (!FULL_STATUS.contains(status)) {
throw new IllegalStateException("Chunk " + chunkX + "," + chunkZ + " in " + regionDirectory
+ " has status '" + status + "', not full");
}
String[] blocks = new String[CHUNK_WIDTH * CHUNK_WIDTH * (maxY - minY)];
Arrays.fill(blocks, FidelityBlocks.AIR);
ListTag<?> sections = root.getListTag("sections");
if (sections != null && sections.size() > 0) {
for (CompoundTag section : sections.asCompoundTagList()) {
decodeSection(section, blocks, chunkX, chunkZ);
}
}
return new RegionChunk(blocks);
}
@Override
public void close() {
}
private CompoundTag readChunk(int chunkX, int chunkZ) throws IOException {
Path file = regionDirectory.resolve(MCAUtilSupport.createNameFromChunkLocation(chunkX, chunkZ));
if (!Files.isRegularFile(file)) {
throw new IllegalStateException("Region file " + file + " for chunk " + chunkX + "," + chunkZ + " does not exist");
}
byte[] payload;
int compression;
try (RandomAccessFile region = new RandomAccessFile(file.toFile(), "r")) {
region.seek(MCAFileSupport.getChunkIndex(chunkX, chunkZ) * 4L);
int location = region.readInt();
int sector = location >>> 8;
if (sector == 0 || (location & 0xFF) == 0) {
throw new IllegalStateException("Chunk " + chunkX + "," + chunkZ + " is not stored in " + file);
}
region.seek(sector * SECTOR_BYTES);
int length = region.readInt();
compression = region.readUnsignedByte();
if (length < 1) {
throw new IOException("Chunk " + chunkX + "," + chunkZ + " in " + file + " has invalid length " + length);
}
payload = new byte[length - 1];
region.readFully(payload);
}
if ((compression & EXTERNAL_PAYLOAD) != 0) {
payload = Files.readAllBytes(regionDirectory.resolve("c." + chunkX + "." + chunkZ + ".mcc"));
compression &= ~EXTERNAL_PAYLOAD;
}
return parse(payload, compression, chunkX, chunkZ);
}
private CompoundTag parse(byte[] payload, int compression, int chunkX, int chunkZ) throws IOException {
CompressionType type = CompressionType.getFromID((byte) compression);
if (type == null) {
throw new IOException("Chunk " + chunkX + "," + chunkZ + " in " + regionDirectory
+ " uses unsupported compression type " + compression);
}
try (InputStream input = new BufferedInputStream(type.decompress(new ByteArrayInputStream(payload)))) {
NamedTag tag = new NBTDeserializer(false).fromStream(input);
if (tag == null || !(tag.getTag() instanceof CompoundTag root)) {
throw new IOException("Chunk " + chunkX + "," + chunkZ + " in " + regionDirectory + " has no compound root tag");
}
return root;
}
}
private void decodeSection(CompoundTag section, String[] blocks, int chunkX, int chunkZ) {
int baseY = section.getByte("Y") * CHUNK_WIDTH;
CompoundTag states = section.getCompoundTag("block_states");
if (baseY + CHUNK_WIDTH <= minY || baseY >= maxY || states == null) {
return;
}
ListTag<?> paletteTag = states.getListTag("palette");
if (paletteTag == null || paletteTag.size() == 0) {
return;
}
String[] palette = new String[paletteTag.size()];
int entry = 0;
for (CompoundTag state : paletteTag.asCompoundTagList()) {
palette[entry++] = state.getString("Name");
}
long[] data = states.getLongArray("data");
int bits = Math.max(4, 32 - Integer.numberOfLeadingZeros(palette.length - 1));
int perLong = 64 / bits;
long mask = (1L << bits) - 1L;
if (palette.length > 1 && data.length < (SECTION_VOLUME + perLong - 1) / perLong) {
throw new IllegalStateException("Section " + section.getByte("Y") + " of chunk " + chunkX + "," + chunkZ
+ " in " + regionDirectory + " has " + data.length + " block-state longs for a palette of " + palette.length);
}
for (int index = 0; index < SECTION_VOLUME; index++) {
int y = baseY + (index >> 8);
if (y < minY || y >= maxY) {
continue;
}
int paletteIndex = palette.length == 1 ? 0 : (int) ((data[index / perLong] >>> ((index % perLong) * bits)) & mask);
if (paletteIndex >= palette.length) {
throw new IllegalStateException("Section " + section.getByte("Y") + " of chunk " + chunkX + "," + chunkZ
+ " in " + regionDirectory + " references palette entry " + paletteIndex + " of " + palette.length);
}
blocks[offset(index & 15, y, (index >> 4) & 15)] = palette[paletteIndex];
}
}
private int offset(int localX, int y, int localZ) {
if (y < minY || y >= maxY) {
throw new IllegalArgumentException("Y " + y + " is outside [" + minY + ", " + maxY + ")");
}
return ((y - minY) * CHUNK_WIDTH + localZ) * CHUNK_WIDTH + localX;
}
private final class RegionChunk implements FidelityChunk {
private final String[] blocks;
private RegionChunk(String[] blocks) {
this.blocks = blocks;
}
@Override
public int surfaceY(int localX, int localZ) {
for (int y = maxY - 1; y >= minY; y--) {
String id = block(localX, y, localZ);
if (!FidelityBlocks.open(id) && !FidelityBlocks.fluid(id)) {
return y;
}
}
return minY - 1;
}
@Override
public String surfaceBiome(int localX, int localZ) {
return null;
}
@Override
public boolean open(int localX, int y, int localZ) {
return FidelityBlocks.open(block(localX, y, localZ));
}
@Override
public boolean fluid(int localX, int y, int localZ) {
return FidelityBlocks.fluid(block(localX, y, localZ));
}
@Override
public String block(int localX, int y, int localZ) {
return blocks[offset(localX, y, localZ)];
}
}
}
@@ -0,0 +1,24 @@
package art.arcane.iris.probe.fidelity;
public record SurveyWindow(int centerX, int centerZ, int size, int stride) {
public SurveyWindow {
if (stride < 1) {
throw new IllegalArgumentException("Survey stride must be positive, got " + stride);
}
if (size < stride || size % stride != 0) {
throw new IllegalArgumentException("Survey window " + size + " must be a positive multiple of the stride " + stride);
}
}
public int perAxis() {
return size / stride;
}
public long samples() {
return (long) perAxis() * perAxis();
}
public int coordinate(int center, int index) {
return center - size / 2 + stride / 2 + index * stride;
}
}
@@ -0,0 +1,63 @@
package art.arcane.iris.probe;
import art.arcane.iris.probe.fidelity.ColumnSurvey;
import art.arcane.iris.probe.fidelity.FidelityChunk;
import art.arcane.iris.probe.fidelity.FidelityColumn;
import art.arcane.iris.probe.fidelity.FidelityStats;
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.testsupport.IrisRuntimeState;
import org.junit.AfterClass;
import org.junit.Test;
import java.io.File;
import java.net.URL;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
public final class IrisEngineColumnSourceTest {
@AfterClass
public static void releaseRuntime() {
IrisRuntimeState.reset();
}
@Test(timeout = 180_000L)
public void columnsMatchTheGeneratedChunkSurface() throws Exception {
URL resource = IrisEngineColumnSourceTest.class.getResource("/seed-cohesion");
assertNotNull(resource);
File pack = new File(resource.toURI());
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(pack, "cohesion", "[fidelity]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(1337L, false, "fidelity");
IrisEngineColumnSource columns = new IrisEngineColumnSource(session.engine());
IrisEngineFidelitySource chunks = new IrisEngineFidelitySource(session.engine())) {
for (int chunkX = -1; chunkX <= 0; chunkX++) {
for (int chunkZ = -1; chunkZ <= 0; chunkZ++) {
assertChunkMatches(columns, chunks, chunkX, chunkZ);
}
}
FidelityStats stats = ColumnSurvey.scan(columns, new SurveyWindow(0, 0, 64, 8), 2);
assertEquals(64L, stats.columns());
assertEquals(64L, stats.heightColumns());
assertEquals(0L, stats.fluidColumns());
assertEquals(Map.of("terrain", 64L), stats.biomeColumns());
}
}
private static void assertChunkMatches(IrisEngineColumnSource columns, IrisEngineFidelitySource chunks, int chunkX, int chunkZ) throws Exception {
FidelityChunk chunk = chunks.sample(chunkX, chunkZ);
for (int x = 0; x < 16; x += 3) {
for (int z = 0; z < 16; z += 3) {
FidelityColumn column = columns.column((chunkX << 4) + x, (chunkZ << 4) + z);
int surface = chunk.surfaceY(x, z);
assertEquals("column " + x + "," + z + " of chunk " + chunkX + "," + chunkZ, surface, column.terrainY());
assertEquals(chunk.surfaceBiome(x, z), column.biome());
assertFalse(column.wet());
}
}
}
}
@@ -0,0 +1,81 @@
package art.arcane.iris.probe;
import art.arcane.iris.probe.fidelity.FidelityChunk;
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.FidelityStats;
import art.arcane.iris.probe.fidelity.FidelityThresholds;
import art.arcane.iris.probe.fidelity.IrisEngineFidelitySource;
import art.arcane.iris.testsupport.IrisRuntimeState;
import org.junit.AfterClass;
import org.junit.Test;
import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public final class IrisEngineFidelitySourceTest {
@AfterClass
public static void releaseRuntime() {
IrisRuntimeState.reset();
}
@Test(timeout = 180_000L)
public void realPackChunksScanIntoComparableStats() throws Exception {
URL resource = IrisEngineFidelitySourceTest.class.getResource("/seed-cohesion");
assertNotNull(resource);
File pack = new File(resource.toURI());
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(pack, "cohesion", "[fidelity]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(1337L, false, "fidelity");
IrisEngineFidelitySource source = new IrisEngineFidelitySource(session.engine())) {
FidelityStats stats = FidelityScanner.scan(source, 0, 0, 1, 4, 2);
assertEquals(0, source.minY());
assertEquals(64, source.maxY());
assertEquals(144L, stats.columns());
assertEquals(144L, stats.heightColumns());
assertEquals(Map.of("terrain", 144L), stats.biomeColumns());
assertEquals(144L, stats.blockColumns());
assertTrue(stats.wallSolidVoxels() > 0L);
assertTrue(stats.wallVoxels() <= stats.wallSolidVoxels());
double median = stats.heightQuantile(0.5D);
assertTrue("median " + median, median >= 20.0D && median < 40.0D);
FidelitySamples samples = new FidelitySamples(stats, stats, new FidelitySampling("chunks", 48, 4, List.of(1337L), stats.columns()));
FidelityReport report = FidelityComparison.compare(samples, samples, FidelityThresholds.defaults());
assertTrue(report.toJson(), report.passed());
}
}
@Test(timeout = 180_000L)
public void volumetricOverhangsAreTerrainVoidsAndRockIsTerrainMass() throws Exception {
URL resource = IrisEngineFidelitySourceTest.class.getResource("/terrain3d-pack");
assertNotNull(resource);
File pack = new File(resource.toURI());
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(pack, "terrain3d", "[fidelity]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(1337L, false, "fidelity-voids");
IrisEngineFidelitySource source = new IrisEngineFidelitySource(session.engine())) {
FidelityChunk chunk = source.sample(0, 0);
long voids = 0L;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = source.minY(); y < chunk.surfaceY(x, z); y++) {
boolean air = chunk.block(x, y, z).endsWith("air");
assertEquals(x + "," + y + "," + z, !air, chunk.terrainMass(x, y, z));
if (air) {
voids++;
}
}
}
}
assertTrue(voids > 0L);
}
}
}
@@ -0,0 +1,68 @@
package art.arcane.iris.probe.fidelity;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
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 class BiomeShareReportTest {
@Test
public void tableListsEverySeedDeltaWithThePassingCount() {
BiomeShareReport report = report();
List<String> lines = report.toTable().lines().toList();
assertTrue(lines.toString(), lines.contains(String.format(Locale.ROOT, "%-18s %10s %10s %8s", "seed", "1337", "1338", "passing")));
assertTrue(lines.toString(), lines.contains(String.format(Locale.ROOT, "%-18s %10.4f %10.4f %8s", "biome.share.forest", 0.5D, 0.05D, "1/2")));
assertTrue(lines.toString(), lines.contains(String.format(Locale.ROOT, "%-18s %10s %10.4f %8s", "biome.share.desert", "-", 0.0D, "1/1")));
assertEquals("overall FAIL: 0 of 1 metrics passed", lines.getLast());
assertFalse(report.passed());
}
@Test
public void jsonCarriesTheMeanAndEverySeed() {
JsonObject json = JsonParser.parseString(report().toJson()).getAsJsonObject();
assertFalse(json.get("passed").getAsBoolean());
JsonObject mean = json.getAsJsonObject("mean");
assertEquals("biome.share.forest", mean.getAsJsonArray("metrics").get(0).getAsJsonObject().get("name").getAsString());
JsonArray seeds = json.getAsJsonArray("seeds");
assertEquals(2, seeds.size());
assertEquals(1337L, seeds.get(0).getAsJsonObject().get("seed").getAsLong());
assertEquals(1, seeds.get(0).getAsJsonObject().getAsJsonArray("metrics").size());
JsonObject passing = json.getAsJsonObject("seedsPassing").getAsJsonObject("biome.share.forest");
assertEquals(1, passing.get("passing").getAsInt());
assertEquals(2, passing.get("compared").getAsInt());
JsonObject desert = json.getAsJsonObject("shares").getAsJsonObject("desert");
assertEquals(0.05D, desert.get("iris").getAsDouble(), 0.0D);
assertEquals(0.01D, desert.get("export").getAsDouble(), 0.0D);
}
@Test
public void reportNeedsAtLeastOneSeed() {
FidelityReport mean = new FidelityReport(List.of(), List.of(), List.of());
assertThrows(IllegalArgumentException.class, () -> new BiomeShareReport(mean, new TreeMap<>(), Map.of(), Map.of()));
}
private static BiomeShareReport report() {
FidelityReport mean = new FidelityReport(List.of(new FidelityMetric("biome.share.forest", 0.5D, 0.4D, 0.2D, false)), List.of(), List.of());
SortedMap<Long, FidelityReport> seeds = new TreeMap<>();
seeds.put(1337L, new FidelityReport(List.of(new FidelityMetric("biome.share.forest", 0.5D, 0.25D, 0.5D, false)), List.of(), List.of()));
seeds.put(1338L, new FidelityReport(List.of(
new FidelityMetric("biome.share.forest", 0.5D, 0.525D, 0.05D, true),
new FidelityMetric("biome.share.desert", 0.1D, 0.1D, 0.0D, true)), List.of(), List.of()));
return new BiomeShareReport(mean, seeds, Map.of("forest", 0.5D, "desert", 0.05D), Map.of("forest", 0.4D, "desert", 0.01D));
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class BiomeTypesTest {
@Test
public void childrenInheritTheListTypeOfTheirParent() {
IrisRegion region = region(List.of("plains"), List.of("ocean"), List.of("beach"));
Map<String, IrisBiome> biomes = Map.of(
"plains", biome("meadow"),
"meadow", biome("plains"),
"ocean", biome("reef"),
"reef", biome(),
"beach", biome());
Map<String, String> types = BiomeTypes.of(List.of(region), biomes::get);
assertEquals(Map.of("plains", "land", "meadow", "land", "ocean", "sea", "reef", "sea", "beach", "shore"), types);
}
@Test
public void biomesListedUnderSeveralTypesJoinThem() {
IrisRegion first = region(List.of("plains"), List.of(), List.of());
IrisRegion second = region(List.of(), List.of("plains"), List.of("missing"));
Map<String, String> types = BiomeTypes.of(List.of(first, second), key -> key.equals("plains") ? biome() : null);
assertEquals(Map.of("plains", "land+sea", "missing", "shore"), types);
}
private static IrisRegion region(List<String> land, List<String> sea, List<String> shore) {
return new IrisRegion().setLandBiomes(new KList<>(land)).setSeaBiomes(new KList<>(sea)).setShoreBiomes(new KList<>(shore));
}
private static IrisBiome biome(String... children) {
return new IrisBiome().setChildren(new KList<>(List.of(children)));
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.probe.fidelity;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CaveWallsTest {
private static final int MIN_Y = -64;
@Test
public void solidBesideCaveAirIsAWall() {
SyntheticFidelityChunk chunk = new SyntheticFidelityChunk(MIN_Y).surface(64).caves(-20, 0, 4);
assertTrue(CaveWalls.besideCave(chunk, 4, -10, 7));
assertFalse(CaveWalls.besideCave(chunk, 5, -10, 7));
assertFalse(CaveWalls.besideCave(chunk, 4, 5, 7));
assertFalse(CaveWalls.besideCave(chunk, 4, -21, 7));
}
@Test
public void fluidFilledCavesHaveWalls() {
SyntheticFidelityChunk chunk = new SyntheticFidelityChunk(MIN_Y).surface(64).lavaPool(-64, -56, 4);
assertTrue(CaveWalls.besideCave(chunk, 4, -60, 7));
assertFalse(CaveWalls.besideCave(chunk, 4, -50, 7));
}
@Test
public void chunkCornersOnlyReadNeighboursInsideTheChunk() {
assertTrue(CaveWalls.besideCave(new SyntheticFidelityChunk(MIN_Y).surface(64).caves(-20, 0, 15), 15, -10, 0));
assertFalse(CaveWalls.besideCave(new SyntheticFidelityChunk(MIN_Y).surface(64), 15, -10, 15));
assertFalse(CaveWalls.besideCave(new SyntheticFidelityChunk(MIN_Y).surface(64), 0, -10, 0));
}
}
@@ -0,0 +1,94 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.configuration.IrisSettings;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ColumnSurveyTest {
private static final int MIN_Y = -64;
private static final int MAX_Y = 320;
private IrisSettings previousSettings;
@Before
public void setUp() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
}
@After
public void tearDown() {
IrisSettings.settings = previousSettings;
}
@Test
public void surveyVisitsEveryStrideCellCentreOnce() {
SplitSource source = new SplitSource(null);
FidelityStats stats = ColumnSurvey.scan(source, new SurveyWindow(1000, -2000, 512, 128), 3);
assertEquals(16L, stats.columns());
assertEquals(16, source.visited().size());
assertTrue(source.visited().toString(), source.visited().contains(1000 - 256 + 64 + "," + (-2000 - 256 + 64)));
assertTrue(source.visited().toString(), source.visited().contains(1000 + 128 + 64 + "," + (-2000 + 128 + 64)));
assertEquals(Map.of("land", 8L, "sea", 8L), stats.biomeColumns());
assertEquals(16L, stats.heightColumns());
assertEquals(0.5D, stats.landFraction(), 0.0D);
assertEquals(30.0D, stats.heightQuantile(0.25D), 0.0D);
assertEquals(80.0D, stats.heightQuantile(0.75D), 0.0D);
}
@Test
public void failedColumnFailsTheSurveyWithItsCause() {
IllegalStateException broken = new IllegalStateException("sampler closed");
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> ColumnSurvey.scan(new SplitSource(broken), new SurveyWindow(0, 0, 64, 16), 2));
assertSame(broken, failure.getCause());
}
private record SplitSource(RuntimeException failure, Set<String> visited) implements ColumnSource {
private SplitSource(RuntimeException failure) {
this(failure, ConcurrentHashMap.newKeySet());
}
@Override
public String label() {
return "synthetic";
}
@Override
public int minY() {
return MIN_Y;
}
@Override
public int maxY() {
return MAX_Y;
}
@Override
public FidelityColumn column(int x, int z) {
if (failure != null) {
throw failure;
}
visited.add(x + "," + z);
return x < 1000 ? new FidelityColumn("sea", 30, true) : new FidelityColumn("land", 80, false);
}
@Override
public void close() {
}
}
}
@@ -0,0 +1,420 @@
package art.arcane.iris.probe.fidelity;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import java.util.Map;
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 class FidelityComparisonTest {
private static final int MIN_Y = -64;
private static final int MAX_Y = 320;
private static final double RANGE = MAX_Y - MIN_Y;
@Test
public void identicalStatsPass() {
FidelityReport report = compare(varied(), varied());
assertTrue(report.passed());
for (FidelityMetric metric : report.metrics()) {
assertEquals(metric.name(), 0.0D, metric.delta(), 1.0E-12D);
assertTrue(metric.name(), metric.pass());
}
assertEquals(0.5D, metric(report, "biome.share.plains").iris(), 1.0E-12D);
assertEquals(40.0D, metric(report, "height.p10").iris(), 0.0D);
assertEquals(0.5D, metric(report, "height.landFraction").iris(), 1.0E-12D);
assertEquals(0.125D, metric(report, "cave.band.-48").iris(), 1.0E-12D);
assertEquals(93_750.0D, metric(report, "decoration.columnsPerKm2").iris(), 1.0E-6D);
}
@Test
public void biomeShareBeyondThresholdFails() {
FidelityStats iris = stats(chunk().biomes("plains", "forest", 8));
FidelityStats export = stats(chunk().biomes("plains", "forest", 12));
FidelityReport report = compare(iris, export);
FidelityMetric distance = metric(report, "biome.totalVariation");
assertEquals(0.25D, distance.delta(), 1.0E-12D);
assertFalse(distance.pass());
FidelityMetric forest = metric(report, "biome.share.forest");
assertEquals(0.5D, forest.iris(), 1.0E-12D);
assertEquals(0.25D, forest.export(), 1.0E-12D);
assertEquals(0.5D, forest.delta(), 1.0E-12D);
assertFalse(forest.pass());
assertTrue(metric(report, "height.p50").pass());
assertFalse(report.passed());
}
@Test
public void biomesBelowTwoPercentAreOnlyCoveredByTotalVariation() {
FidelityStats iris = new FidelityStats(MIN_Y, MAX_Y);
for (int i = 0; i < 9; i++) {
iris.accept(chunk().biomes("plains", "plains", 16), 1);
}
iris.accept(chunk().biomes("swamp", "plains", 1), 1);
FidelityStats export = new FidelityStats(MIN_Y, MAX_Y);
for (int i = 0; i < 10; i++) {
export.accept(chunk().biomes("plains", "plains", 16), 1);
}
FidelityReport report = compare(iris, export);
assertNull(metric(report, "biome.share.swamp"));
assertEquals(0.00625D, metric(report, "biome.totalVariation").delta(), 1.0E-12D);
assertTrue(report.passed());
}
@Test
public void heightQuantileDeltaNormalisedByRange() {
FidelityStats iris = stats(chunk().surface(64));
FidelityReport close = compare(iris, stats(chunk().surface(94)));
FidelityMetric closeMedian = metric(close, "height.p50");
assertEquals(64.0D, closeMedian.iris(), 0.0D);
assertEquals(94.0D, closeMedian.export(), 0.0D);
assertEquals(30.0D / RANGE, closeMedian.delta(), 1.0E-12D);
assertTrue(closeMedian.pass());
FidelityReport far = compare(iris, stats(chunk().surface(112)));
FidelityMetric farMedian = metric(far, "height.p50");
assertEquals(48.0D / RANGE, farMedian.delta(), 1.0E-12D);
assertFalse(farMedian.pass());
assertFalse(metric(far, "height.p10").pass());
assertFalse(metric(far, "height.p90").pass());
assertFalse(far.passed());
}
@Test
public void landFractionUsesHeightThreshold() {
FidelityStats iris = stats(chunk().surface(64), chunk().surface(64));
FidelityStats export = stats(chunk().surface(64), chunk().surface(60).sea(62));
FidelityMetric land = metric(compare(iris, export), "height.landFraction");
assertEquals(1.0D, land.iris(), 0.0D);
assertEquals(0.5D, land.export(), 0.0D);
assertEquals(0.5D, land.delta(), 0.0D);
assertFalse(land.pass());
}
@Test
public void caveBandDeltaFails() {
FidelityStats iris = stats(chunk().surface(64).caves(0, 16, 2));
FidelityStats export = stats(chunk().surface(64).caves(0, 16, 8));
FidelityReport report = compare(iris, export);
FidelityMetric band = metric(report, "cave.band.0");
assertEquals(0.125D, band.iris(), 1.0E-12D);
assertEquals(0.5D, band.export(), 1.0E-12D);
assertEquals(0.375D, band.delta(), 1.0E-12D);
assertFalse(band.pass());
assertTrue(metric(report, "cave.band.-64").pass());
assertNull(metric(report, "cave.band.64"));
assertFalse(report.passed());
}
@Test
public void terrainVoidBandsAreJudgedApartFromCaveBands() {
FidelityStats iris = stats(chunk().surface(100).overhang(4, 80, 96).caves(0, 16, 4));
FidelityStats export = stats(chunk().surface(100).caves(0, 16, 4));
FidelityReport report = compare(iris, export);
FidelityMetric caves = metric(report, "cave.band.80");
assertEquals(0.0D, caves.iris(), 0.0D);
assertEquals(0.0D, caves.export(), 0.0D);
assertTrue(caves.pass());
assertTrue(metric(report, "cave.band.0").pass());
FidelityMetric voids = metric(report, "void.band.80");
assertEquals(0.25D, voids.iris(), 1.0E-12D);
assertEquals(0.0D, voids.export(), 0.0D);
assertEquals(0.25D, voids.delta(), 1.0E-12D);
assertFalse(voids.pass());
assertNull(metric(report, "void.band.0"));
assertFalse(report.passed());
}
@Test
public void voidBandsUseTheCaveBandThreshold() {
FidelityStats iris = stats(chunk().surface(100).overhang(4, 80, 96));
FidelityStats export = stats(chunk().surface(100).overhang(2, 80, 96));
FidelityMetric voids = metric(compare(iris, export), "void.band.80");
assertEquals(0.125D, voids.delta(), 1.0E-12D);
assertEquals(voids.delta() <= FidelityThresholds.defaults().caveBand(), voids.pass());
}
@Test
public void thinVoidBandsAreObservedNotJudged() {
FidelityStats iris = stats(chunk().surface(66).overhang(16, 64, 66));
FidelityStats export = stats(chunk().surface(66));
FidelityReport report = compare(iris, export);
assertNull(metric(report, "void.band.64"));
FidelityObservation thin = observation(report, "void.band.64");
assertTrue(thin.iris() > 0.0D);
assertEquals(0.0D, thin.export(), 0.0D);
}
@Test
public void decorationDensityUsesRelativeError() {
FidelityStats iris = stats(chunk().trees(4, 5));
FidelityMetric dense = metric(compare(iris, stats(chunk().trees(8, 5))),
"decoration.columnsPerKm2");
assertEquals(1.0D, dense.delta(), 1.0E-12D);
assertFalse(dense.pass());
FidelityReport noiseOnly = compare(iris, stats(chunk().trees(8, 5).noBlocks()));
assertNull(metric(noiseOnly, "decoration.columnsPerKm2"));
}
@Test
public void wallShareUsesRelativeError() {
FidelityStats iris = stats(chunk().surface(64).caves(-20, 0, 4).walls(1));
FidelityMetric same = metric(compare(iris, stats(chunk().surface(64).caves(-20, 0, 4).walls(1))), "cave.wallShare");
assertEquals(280.0D / 24_248.0D, same.iris(), 1.0E-12D);
assertEquals(0.0D, same.delta(), 0.0D);
assertTrue(same.pass());
FidelityMetric thick = metric(compare(iris, stats(chunk().surface(64).caves(-20, 0, 4).walls(3))), "cave.wallShare");
assertEquals(840.0D / 24_248.0D, thick.export(), 1.0E-12D);
assertEquals(2.0D, thick.delta(), 1.0E-12D);
assertFalse(thick.pass());
assertNull(metric(compare(iris, stats(chunk().surface(64).caves(-20, 0, 4))), "cave.wallShare"));
}
@Test
public void differentHeightRangesAreRejected() {
FidelityStats shallow = new FidelityStats(0, 256);
shallow.accept(new SyntheticFidelityChunk(0), 1);
assertThrows(IllegalArgumentException.class, () -> compare(varied(), shallow));
}
@Test
public void reportJsonListsEveryMetric() {
FidelityStats iris = stats(chunk().surface(64));
FidelityReport report = compare(iris, stats(chunk().surface(112)));
JsonObject json = JsonParser.parseString(report.toJson()).getAsJsonObject();
assertFalse(json.get("passed").getAsBoolean());
assertEquals(report.metrics().size(), json.getAsJsonArray("metrics").size());
JsonObject first = json.getAsJsonArray("metrics").get(0).getAsJsonObject();
assertEquals(report.metrics().getFirst().name(), first.get("name").getAsString());
assertEquals(report.metrics().getFirst().pass(), first.get("pass").getAsBoolean());
}
@Test
public void thinCaveBandsAreObservedNotJudged() {
FidelityStats iris = stats(chunk().surface(66).caves(64, 66, 16));
FidelityStats export = stats(chunk().surface(66));
FidelityReport report = compare(iris, export);
assertNull(metric(report, "cave.band.64"));
assertTrue(metric(report, "cave.band.48").pass());
FidelityObservation thin = observation(report, "cave.band.64");
assertTrue(thin.iris() > 0.0D);
assertEquals(0.0D, thin.export(), 0.0D);
assertTrue(report.passed());
}
@Test
public void deepLavaShareIsObservedNotJudged() {
FidelityStats iris = stats(chunk().surface(64).caves(-64, -56, 4));
FidelityStats export = stats(chunk().surface(64).lavaPool(-64, -56, 4));
FidelityReport report = compare(iris, export);
FidelityObservation lava = report.observations().getFirst();
assertEquals("cave.lavaShare.belowY-54", lava.name());
assertEquals(0.0D, lava.iris(), 0.0D);
assertEquals(0.2D, lava.export(), 1.0E-12D);
assertNull(metric(report, "cave.lavaShare.belowY-54"));
}
@Test
public void oneBlockWallShareIsObservedNotJudged() {
FidelityStats iris = stats(chunk().surface(64).caves(-20, 0, 4).walls(1));
FidelityStats export = stats(chunk().surface(64).caves(-20, 0, 4).walls(3));
FidelityReport report = compare(iris, export);
FidelityObservation oneBlock = observation(report, "cave.wallShare.oneBlock");
assertEquals(280.0D / 24_248.0D, oneBlock.iris(), 1.0E-12D);
assertEquals(280.0D / 24_248.0D, oneBlock.export(), 1.0E-12D);
assertNull(metric(report, "cave.wallShare.oneBlock"));
}
@Test
public void emptyReportDoesNotPass() {
assertFalse(new FidelityReport(List.of(), List.of(), List.of()).passed());
}
@Test
public void surfaceMetricsComeFromColumnsAndSubsurfaceMetricsFromChunks() {
FidelityStats columns = stats(chunk().biomes("plains", "forest", 8).surface(70));
FidelityStats exportColumns = stats(chunk().biomes("plains", "forest", 8).surface(70));
FidelityStats chunks = stats(chunk().biomes("desert", "desert", 16).surface(20).caves(0, 16, 2).trees(3, 5));
FidelityStats exportChunks = stats(chunk().biomes("ocean", "ocean", 16).surface(90).caves(0, 16, 2).trees(3, 5));
FidelityReport report = FidelityComparison.compare(
new FidelitySamples(columns, exportColumns, sampling("columns")),
new FidelitySamples(chunks, exportChunks, sampling("chunks")),
FidelityThresholds.defaults());
assertTrue(report.toTable(), report.passed());
assertEquals(0.5D, metric(report, "biome.share.plains").iris(), 1.0E-12D);
assertNull(metric(report, "biome.share.desert"));
assertEquals(70.0D, metric(report, "height.p50").export(), 0.0D);
assertEquals(0.125D, metric(report, "cave.band.0").export(), 1.0E-12D);
assertEquals(List.of("columns", "chunks"), report.sampling().stream().map(FidelitySampling::scope).toList());
}
@Test
public void biomeSharesAreJudgedOnTheMeanOverEverySeed() {
FidelitySamples first = seed(1337L, columns("plains", 8, "forest", 8), columns("plains", 12, "forest", 4));
FidelitySamples second = seed(1338L, columns("plains", 8, "forest", 8), columns("plains", 4, "forest", 12));
BiomeShareReport report = FidelityComparison.compareBiomeShares(List.of(first, second), Map.of(), FidelityThresholds.defaults());
FidelityMetric forest = metric(report.mean(), "biome.share.forest");
assertEquals(0.5D, forest.iris(), 1.0E-12D);
assertEquals(0.5D, forest.export(), 1.0E-12D);
assertTrue(forest.pass());
assertEquals(0.0D, metric(report.mean(), "biome.totalVariation").delta(), 1.0E-12D);
assertTrue(report.toTable(), report.passed());
assertEquals(0.25D, metric(report.seeds().get(1337L), "biome.share.forest").export(), 1.0E-12D);
assertFalse(metric(report.seeds().get(1338L), "biome.share.forest").pass());
assertEquals(0, report.seedsPassing("biome.share.forest"));
assertEquals(2, report.seedsCompared("biome.share.forest"));
assertNull(metric(report.mean(), "height.p50"));
FidelitySampling sampling = report.mean().sampling().getFirst();
assertEquals(List.of(1337L, 1338L), sampling.seeds());
assertEquals(32L, sampling.columns());
}
@Test
public void meanSharesWeighEverySeedEqually() {
FidelitySamples small = seed(1337L, columns("plains", 4, "forest", 0), columns("plains", 4, "forest", 0));
FidelitySamples large = seed(1338L, columns("plains", 0, "forest", 12), columns("plains", 6, "forest", 6));
BiomeShareReport report = FidelityComparison.compareBiomeShares(List.of(small, large), Map.of(), FidelityThresholds.defaults());
FidelityMetric plains = metric(report.mean(), "biome.share.plains");
assertEquals(0.5D, plains.iris(), 1.0E-12D);
assertEquals(0.75D, plains.export(), 1.0E-12D);
assertFalse(plains.pass());
assertFalse(report.passed());
}
@Test
public void biomeTypeSharesAreObservedWithoutAffectingTheVerdict() {
FidelitySamples first = seed(1337L, columns("plains", 6, "ocean", 4, "beach", 2), columns("plains", 6, "ocean", 5, "beach", 1));
FidelitySamples second = seed(1338L, columns("plains", 6, "ocean", 4, "beach", 2), columns("plains", 6, "ocean", 4, "beach", 2));
Map<String, String> types = Map.of("plains", "land", "ocean", "sea", "beach", "shore");
BiomeShareReport report = FidelityComparison.compareBiomeShares(List.of(first, second), types, FidelityThresholds.defaults());
FidelityObservation sea = observation(report.mean(), "biome.type.sea");
assertEquals(4.0D / 12.0D, sea.iris(), 1.0E-12D);
assertEquals(4.5D / 12.0D, sea.export(), 1.0E-12D);
assertEquals(0.5D, observation(report.mean(), "biome.type.land").export(), 1.0E-12D);
assertEquals(1.0D / 12.0D, observation(report.seeds().get(1337L), "biome.type.shore").export(), 1.0E-12D);
assertEquals(0.5D, observation(report.mean(), "height.landFraction").iris(), 1.0E-12D);
}
@Test
public void meanSharesListEveryBiomeIncludingTheRareOnes() {
FidelitySamples first = seed(1337L, columns("plains", 99, "swamp", 1), columns("plains", 100));
FidelitySamples second = seed(1338L, columns("plains", 100), columns("plains", 98, "swamp", 2));
BiomeShareReport report = FidelityComparison.compareBiomeShares(List.of(first, second), Map.of(), FidelityThresholds.defaults());
assertNull(metric(report.mean(), "biome.share.swamp"));
assertEquals(0.005D, report.irisShares().get("swamp"), 1.0E-12D);
assertEquals(0.01D, report.exportShares().get("swamp"), 1.0E-12D);
assertEquals(0.995D, report.irisShares().get("plains"), 1.0E-12D);
}
@Test
public void biomeSharesNeedAtLeastOneSeed() {
assertThrows(IllegalArgumentException.class,
() -> FidelityComparison.compareBiomeShares(List.of(), Map.of(), FidelityThresholds.defaults()));
}
private static FidelityStats varied() {
return stats(chunk().biomes("plains", "forest", 8).caves(-48, -32, 4).trees(3, 5).surface(70),
chunk().biomes("plains", "forest", 8).surface(40).sea(62));
}
private static FidelityReport compare(FidelityStats iris, FidelityStats export) {
return FidelityComparison.compare(new FidelitySamples(iris, export, sampling("columns")),
new FidelitySamples(iris, export, sampling("chunks")), FidelityThresholds.defaults());
}
private static FidelitySampling sampling(String scope) {
return new FidelitySampling(scope, 16, 1, List.of(1337L), 256L);
}
private static SyntheticFidelityChunk chunk() {
return new SyntheticFidelityChunk(MIN_Y);
}
private static FidelityStats stats(SyntheticFidelityChunk... chunks) {
FidelityStats stats = new FidelityStats(MIN_Y, MAX_Y);
for (SyntheticFidelityChunk chunk : chunks) {
stats.accept(chunk, 1);
}
return stats;
}
private static FidelitySamples seed(long seed, FidelityStats iris, FidelityStats export) {
return new FidelitySamples(iris, export, new FidelitySampling("columns", 64, 16, List.of(seed), iris.columns()));
}
private static FidelityStats columns(Object... biomesAndCounts) {
FidelityStats stats = new FidelityStats(MIN_Y, MAX_Y);
for (int index = 0; index < biomesAndCounts.length; index += 2) {
String biome = (String) biomesAndCounts[index];
int count = (Integer) biomesAndCounts[index + 1];
for (int column = 0; column < count; column++) {
stats.accept(new FidelityColumn(biome, biome.equals("plains") ? 80 : 40, !biome.equals("plains")));
}
}
return stats;
}
private static FidelityObservation observation(FidelityReport report, String name) {
for (FidelityObservation observation : report.observations()) {
if (observation.name().equals(name)) {
return observation;
}
}
return null;
}
private static FidelityMetric metric(FidelityReport report, String name) {
for (FidelityMetric metric : report.metrics()) {
if (metric.name().equals(name)) {
return metric;
}
}
return null;
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.probe.fidelity;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class FidelityReportTest {
@Test
public void tableListsEveryMetricWithItsVerdict() {
FidelityReport report = new FidelityReport(List.of(
new FidelityMetric("biome.totalVariation", 0.0D, 0.03125D, 0.03125D, true),
new FidelityMetric("height.p50", 64.0D, 80.0D, 0.041666D, false)), List.of(), List.of());
List<String> lines = report.toTable().lines().toList();
assertEquals(4, lines.size());
assertTrue(lines.get(0), lines.get(0).startsWith("metric"));
assertTrue(lines.get(1), lines.get(1).startsWith("biome.totalVariation"));
assertTrue(lines.get(1), lines.get(1).contains("0.0313"));
assertTrue(lines.get(1), lines.get(1).endsWith("PASS"));
assertTrue(lines.get(2), lines.get(2).startsWith("height.p50"));
assertTrue(lines.get(2), lines.get(2).contains("64.0000"));
assertTrue(lines.get(2), lines.get(2).contains("80.0000"));
assertTrue(lines.get(2), lines.get(2).endsWith("FAIL"));
assertEquals("overall FAIL: 1 of 2 metrics passed", lines.get(3));
assertEquals(lines.get(1).indexOf("PASS"), lines.get(2).indexOf("FAIL"));
}
@Test
public void passingReportEndsWithOverallPass() {
FidelityReport report = new FidelityReport(List.of(new FidelityMetric("height.p10", 40.0D, 40.0D, 0.0D, true)), List.of(), List.of());
List<String> lines = report.toTable().lines().toList();
assertEquals("overall PASS: 1 of 1 metrics passed", lines.getLast());
}
@Test
public void observationsAreListedWithoutAffectingTheVerdict() {
FidelityReport report = new FidelityReport(List.of(new FidelityMetric("height.p10", 40.0D, 40.0D, 0.0D, true)), List.of(),
List.of(new FidelityObservation("cave.lavaShare.belowY-54", 0.01D, 0.5D)));
List<String> lines = report.toTable().lines().toList();
JsonObject json = JsonParser.parseString(report.toJson()).getAsJsonObject();
assertTrue(report.passed());
assertTrue(lines.toString(), lines.stream().anyMatch(line -> line.startsWith("cave.lavaShare.belowY-54") && line.endsWith("INFO")));
assertEquals("overall PASS: 1 of 1 metrics passed", lines.getLast());
JsonObject observation = json.getAsJsonArray("observations").get(0).getAsJsonObject();
assertEquals("cave.lavaShare.belowY-54", observation.get("name").getAsString());
assertEquals(0.5D, observation.get("export").getAsDouble(), 0.0D);
assertFalse(observation.has("pass"));
}
@Test
public void samplingIsListedInTheTableAndJson() {
FidelityReport report = new FidelityReport(List.of(new FidelityMetric("height.p10", 40.0D, 40.0D, 0.0D, true)),
List.of(new FidelitySampling("columns", 65536, 256, List.of(1337L, 1338L), 65536L)), List.of());
List<String> lines = report.toTable().lines().toList();
JsonObject sampling = JsonParser.parseString(report.toJson()).getAsJsonObject().getAsJsonArray("sampling").get(0).getAsJsonObject();
assertEquals("sampling columns window=65536 stride=256 seeds=[1337, 1338] columns=65536", lines.getFirst());
assertTrue(lines.get(1), lines.get(1).startsWith("metric"));
assertEquals("columns", sampling.get("scope").getAsString());
assertEquals(65536, sampling.get("windowBlocks").getAsInt());
assertEquals(256, sampling.get("strideBlocks").getAsInt());
assertEquals(2, sampling.getAsJsonArray("seeds").size());
assertEquals(65536L, sampling.get("columns").getAsLong());
}
@Test
public void emptyReportFailsInTheTable() {
List<String> lines = new FidelityReport(List.of(), List.of(), List.of()).toTable().lines().toList();
assertEquals("overall FAIL: no metrics were compared", lines.getLast());
}
}
@@ -0,0 +1,79 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.iris.configuration.IrisSettings;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class FidelityScannerTest {
private static final int MIN_Y = -64;
private static final int MAX_Y = 320;
private IrisSettings previousSettings;
@Before
public void setUp() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
}
@After
public void tearDown() {
IrisSettings.settings = previousSettings;
}
@Test
public void scanMergesEveryChunkOfTheSpiral() {
FidelityStats stats = FidelityScanner.scan(new BandedSource(null), 10, -4, 1, 4, 3);
assertEquals(144L, stats.columns());
assertEquals(Map.of("plains", 128L, "forest", 16L), stats.biomeColumns());
assertEquals(144L, stats.heightColumns());
}
@Test
public void failedChunkFailsTheScanWithItsCause() {
IllegalStateException broken = new IllegalStateException("region missing");
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> FidelityScanner.scan(new BandedSource(broken), 0, 0, 1, 4, 2));
assertSame(broken, failure.getCause());
}
private record BandedSource(RuntimeException failure) implements FidelitySource {
@Override
public String label() {
return "synthetic";
}
@Override
public int minY() {
return MIN_Y;
}
@Override
public int maxY() {
return MAX_Y;
}
@Override
public FidelityChunk sample(int chunkX, int chunkZ) {
if (failure != null && chunkX == 1 && chunkZ == 1) {
throw failure;
}
String biome = chunkX == 10 && chunkZ == -4 ? "forest" : "plains";
return new SyntheticFidelityChunk(MIN_Y).biomes(biome, biome, 16);
}
@Override
public void close() {
}
}
}
@@ -0,0 +1,235 @@
package art.arcane.iris.probe.fidelity;
import org.junit.Test;
import java.util.Map;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class FidelityStatsTest {
private static final int MIN_Y = -64;
private static final int MAX_Y = 320;
@Test
public void strideSamplingCounts() {
int[] strides = {1, 3, 4, 16};
long[] expected = {256L, 36L, 16L, 1L};
for (int i = 0; i < strides.length; i++) {
FidelityStats stats = new FidelityStats(MIN_Y, MAX_Y);
stats.accept(chunk(), strides[i]);
assertEquals(expected[i], stats.columns());
assertEquals(expected[i], stats.heightColumns());
assertEquals(expected[i], stats.blockColumns());
assertEquals(expected[i], stats.biomeColumns().values().stream().mapToLong(Long::longValue).sum());
}
}
@Test
public void strideOutsideChunkIsRejected() {
FidelityStats stats = new FidelityStats(MIN_Y, MAX_Y);
assertThrows(IllegalArgumentException.class, () -> stats.accept(chunk(), 0));
assertThrows(IllegalArgumentException.class, () -> stats.accept(chunk(), 17));
}
@Test
public void mergeIsAssociative() {
FidelityStats a = stats(chunk().surface(40).biomes("plains", "forest", 4).caves(-20, 0, 3).walls(2));
FidelityStats b = stats(chunk().surface(40).sea(62).biomes("ocean", "ocean", 16).overhang(3, 20, 30));
FidelityStats c = stats(chunk().surface(90).trees(5, 6).caves(-64, -30, 16).lavaPool(-64, -60, 3).noBlocks());
FidelityStats left = a.merge(b).merge(c);
FidelityStats right = a.merge(b.merge(c));
FidelityStats combined = new FidelityStats(MIN_Y, MAX_Y);
combined.accept(chunk().surface(40).biomes("plains", "forest", 4).caves(-20, 0, 3).walls(2), 1);
combined.accept(chunk().surface(40).sea(62).biomes("ocean", "ocean", 16).overhang(3, 20, 30), 1);
combined.accept(chunk().surface(90).trees(5, 6).caves(-64, -30, 16).lavaPool(-64, -60, 3).noBlocks(), 1);
assertSameStats(left, right);
assertSameStats(left, combined);
assertSameStats(a, stats(chunk().surface(40).biomes("plains", "forest", 4).caves(-20, 0, 3).walls(2)));
assertEquals(768L, left.columns());
}
@Test
public void columnsCountBiomeHeightAndWater() {
FidelityStats stats = new FidelityStats(MIN_Y, MAX_Y);
stats.accept(new FidelityColumn("plains", 70, false));
stats.accept(new FidelityColumn("ocean", 20, true));
stats.accept(new FidelityColumn(null, MIN_Y - 1, false));
assertEquals(3L, stats.columns());
assertEquals(2L, stats.heightColumns());
assertEquals(1L, stats.fluidColumns());
assertEquals(0.5D, stats.landFraction(), 0.0D);
assertEquals(20.0D, stats.heightQuantile(0.5D), 0.0D);
assertEquals(70.0D, stats.heightQuantile(1.0D), 0.0D);
assertEquals(Map.of("plains", 1L, "ocean", 1L), stats.biomeColumns());
assertEquals(0L, stats.blockColumns());
assertThrows(IllegalArgumentException.class, () -> stats.accept(new FidelityColumn("peak", MAX_Y, false)));
}
@Test
public void mergeRejectsDifferentHeightRanges() {
FidelityStats overworld = new FidelityStats(MIN_Y, MAX_Y);
FidelityStats shallow = new FidelityStats(0, 256);
assertThrows(IllegalArgumentException.class, () -> overworld.merge(shallow));
}
@Test
public void decorationAboveTerrainIsCountedButNotMeasuredAsHeight() {
FidelityStats stats = stats(chunk().surface(70).trees(4, 6));
assertEquals(70.0D, stats.heightQuantile(1.0D), 0.0D);
assertEquals(64L, stats.decorationColumns());
assertEquals(256L, stats.blockColumns());
assertEquals(250_000.0D, stats.decorationPerSquareKilometre(), 1.0E-6D);
}
@Test
public void noiseOnlyChunksSkipDecoration() {
FidelityStats stats = stats(chunk().trees(4, 6).noBlocks());
assertEquals(0L, stats.blockColumns());
assertEquals(0L, stats.decorationColumns());
}
@Test
public void subsurfaceOpenVoxelsAreSharedPerBand() {
FidelityStats stats = stats(chunk().surface(64).caves(-64, -48, 4));
assertEquals(24, stats.bandCount());
assertEquals(0.25D, stats.caveShare(0), 1.0E-12D);
assertEquals(0.0D, stats.caveShare(1), 0.0D);
assertEquals(0.0D, stats.voidShare(0), 0.0D);
assertEquals(256L * 16L, stats.subsurfaceVoxels()[7]);
assertEquals(0L, stats.subsurfaceVoxels()[8]);
assertEquals(1.0D, stats.landFraction(), 0.0D);
}
@Test
public void snowCappedCanopyDoesNotRoofCaves() {
FidelityStats stats = stats(chunk().surface(64).canopy(4, 90, "minecraft:snow"));
assertEquals(0L, sum(stats.caveVoxels()) + sum(stats.voidVoxels()));
assertEquals(0L, stats.subsurfaceVoxels()[(80 - MIN_Y) / 16]);
assertEquals(256L * 16L, stats.subsurfaceVoxels()[(48 - MIN_Y) / 16]);
}
@Test
public void cavesUnderACanopyStillCount() {
FidelityStats stats = stats(chunk().surface(64).caves(48, 64, 4).canopy(4, 90, "minecraft:snow"));
assertEquals(0.25D, stats.caveShare((48 - MIN_Y) / 16), 1.0E-12D);
}
@Test
public void openSpaceUnderAFloatingRoofIsATerrainVoid() {
FidelityStats stats = stats(chunk().surface(64).roof(4, 90, 95));
int band = (80 - MIN_Y) / 16;
assertEquals(64L * 15L, stats.subsurfaceVoxels()[band]);
assertEquals(64L * 10L, stats.voidVoxels()[band]);
assertEquals(0L, stats.caveVoxels()[band]);
}
@Test
public void overhangsBelowTheTerrainTopAreVoidsAndCarvedSpaceIsCave() {
FidelityStats stats = stats(chunk().surface(100).overhang(4, 80, 96).caves(0, 16, 8));
int overhangBand = (80 - MIN_Y) / 16;
int caveBand = (0 - MIN_Y) / 16;
assertEquals(0.25D, stats.voidShare(overhangBand), 1.0E-12D);
assertEquals(0.0D, stats.caveShare(overhangBand), 0.0D);
assertEquals(0.5D, stats.caveShare(caveBand), 1.0E-12D);
assertEquals(0.0D, stats.voidShare(caveBand), 0.0D);
assertEquals(256L * 16L, stats.subsurfaceVoxels()[overhangBand]);
}
@Test
public void deepLavaShareCoversSubsurfaceBelowTheLavaLine() {
FidelityStats stats = stats(chunk().surface(64).lavaPool(-64, -56, 4).caves(-56, -40, 4));
assertEquals(256L * (FidelityStats.DEEP_LAVA_LINE - MIN_Y), stats.deepVoxels());
assertEquals(4L * 16L * 8L, stats.deepLavaVoxels());
assertEquals(0.2D, stats.deepLavaShare(), 1.0E-12D);
}
@Test
public void wallShareCountsSolidSubsurfaceVoxelsOfInteriorColumns() {
FidelityStats stats = stats(chunk().surface(64).caves(-20, 0, 4).walls(1));
assertEquals(14L * 20L, stats.wallVoxels());
assertEquals(42L * 108L + 154L * 128L, stats.wallSolidVoxels());
assertEquals(280.0D / 24_248.0D, stats.wallShare(), 1.0E-12D);
}
@Test
public void oneBlockWallsCountSolidVoxelsBesideCaveAir() {
FidelityStats stats = stats(chunk().surface(64).caves(-20, 0, 4).walls(3));
assertEquals(3L * 14L * 20L, stats.wallVoxels());
assertEquals(14L * 20L, stats.oneBlockWallVoxels());
assertEquals(280.0D / 24_248.0D, stats.oneBlockWallShare(), 1.0E-12D);
}
@Test
public void chunksWithoutWallsAddNoWallVoxels() {
FidelityStats stats = stats(chunk().surface(64).caves(-20, 0, 4));
assertEquals(0L, stats.wallSolidVoxels());
assertEquals(0L, stats.wallVoxels());
assertEquals(0L, stats.oneBlockWallVoxels());
assertThrows(IllegalStateException.class, stats::wallShare);
assertThrows(IllegalStateException.class, stats::oneBlockWallShare);
}
@Test
public void submergedColumnsCountAsFluid() {
FidelityStats stats = stats(chunk().surface(40).sea(62));
assertEquals(256L, stats.fluidColumns());
assertEquals(0.0D, stats.landFraction(), 0.0D);
assertEquals(40.0D, stats.heightQuantile(0.5D), 0.0D);
}
private static long sum(long[] voxels) {
long total = 0L;
for (long count : voxels) {
total += count;
}
return total;
}
private static SyntheticFidelityChunk chunk() {
return new SyntheticFidelityChunk(MIN_Y);
}
private static FidelityStats stats(SyntheticFidelityChunk chunk) {
FidelityStats stats = new FidelityStats(MIN_Y, MAX_Y);
stats.accept(chunk, 1);
return stats;
}
private static void assertSameStats(FidelityStats expected, FidelityStats actual) {
assertEquals(expected.minY(), actual.minY());
assertEquals(expected.maxY(), actual.maxY());
assertEquals(expected.columns(), actual.columns());
assertEquals(expected.heightColumns(), actual.heightColumns());
assertEquals(expected.fluidColumns(), actual.fluidColumns());
assertEquals(expected.blockColumns(), actual.blockColumns());
assertEquals(expected.decorationColumns(), actual.decorationColumns());
assertEquals(expected.riverColumns(), actual.riverColumns());
assertEquals(expected.biomeColumns(), actual.biomeColumns());
assertArrayEquals(expected.heightHistogram(), actual.heightHistogram());
assertArrayEquals(expected.subsurfaceVoxels(), actual.subsurfaceVoxels());
assertArrayEquals(expected.caveVoxels(), actual.caveVoxels());
assertArrayEquals(expected.voidVoxels(), actual.voidVoxels());
assertEquals(expected.deepVoxels(), actual.deepVoxels());
assertEquals(expected.deepLavaVoxels(), actual.deepLavaVoxels());
assertEquals(expected.wallSolidVoxels(), actual.wallSolidVoxels());
assertEquals(expected.wallVoxels(), actual.wallVoxels());
assertEquals(expected.oneBlockWallVoxels(), actual.oneBlockWallVoxels());
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.probe.fidelity;
import art.arcane.volmlib.util.nbt.io.NBTSerializer;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Path;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
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 class RegionFileFidelitySourceTest {
private static final int CHUNK_X = 2;
private static final int CHUNK_Z = -1;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void decodesPackedSectionsFromRegionFiles() throws Exception {
Path regions = temporaryFolder.newFolder("region").toPath();
writeRegion(regions.resolve("r.0.-1.mca"), chunk("minecraft:full"));
try (RegionFileFidelitySource source = new RegionFileFidelitySource(regions, -64, 320)) {
FidelityChunk chunk = source.sample(CHUNK_X, CHUNK_Z);
assertEquals(-40, chunk.surfaceY(0, 0));
assertEquals(-40, chunk.surfaceY(9, 3));
assertTrue(chunk.fluid(0, -39, 0));
assertFalse(chunk.fluid(9, -39, 0));
assertTrue(chunk.open(9, -39, 0));
assertTrue(chunk.open(0, -60, 0));
assertFalse(chunk.open(1, -60, 0));
assertEquals("minecraft:stone", chunk.block(15, -64, 15));
assertEquals("minecraft:air", chunk.block(4, 100, 4));
assertNull(chunk.surfaceBiome(0, 0));
FidelityStats stats = new FidelityStats(-64, 320);
stats.accept(chunk, 1);
assertEquals(128L, stats.fluidColumns());
assertEquals(16L, stats.caveVoxels()[0]);
}
}
@Test
public void missingChunkFails() throws Exception {
Path regions = temporaryFolder.newFolder("region").toPath();
writeRegion(regions.resolve("r.0.-1.mca"), chunk("minecraft:full"));
try (RegionFileFidelitySource source = new RegionFileFidelitySource(regions, -64, 320)) {
assertThrows(IllegalStateException.class, () -> source.sample(CHUNK_X + 1, CHUNK_Z));
assertThrows(IllegalStateException.class, () -> source.sample(40, 40));
}
}
@Test
public void partiallyGeneratedChunkFails() throws Exception {
Path regions = temporaryFolder.newFolder("region").toPath();
writeRegion(regions.resolve("r.0.-1.mca"), chunk("minecraft:features"));
try (RegionFileFidelitySource source = new RegionFileFidelitySource(regions, -64, 320)) {
assertThrows(IllegalStateException.class, () -> source.sample(CHUNK_X, CHUNK_Z));
}
}
private static CompoundTag chunk(String status) {
CompoundTag root = new CompoundTag();
root.putInt("DataVersion", 5023);
root.putInt("xPos", CHUNK_X);
root.putInt("zPos", CHUNK_Z);
root.putInt("yPos", -4);
root.putString("Status", status);
ListTag<CompoundTag> sections = new ListTag<>(CompoundTag.class);
sections.add(section(-4, List.of("minecraft:stone", "minecraft:air"), (int x, int localY, int z) -> localY == 4 && x == 0 ? 1 : 0));
sections.add(section(-3, List.of("minecraft:stone", "minecraft:water", "minecraft:air"),
(int x, int localY, int z) -> localY <= 8 ? 0 : x < 8 ? 1 : 2));
sections.add(section(-2, List.of("minecraft:air"), (int x, int localY, int z) -> 0));
root.put("sections", sections);
return root;
}
private static CompoundTag section(int sectionY, List<String> palette, VoxelPalette voxels) {
CompoundTag section = new CompoundTag();
section.putByte("Y", (byte) sectionY);
CompoundTag blockStates = new CompoundTag();
ListTag<CompoundTag> paletteTag = new ListTag<>(CompoundTag.class);
for (String name : palette) {
CompoundTag entry = new CompoundTag();
entry.putString("Name", name);
paletteTag.add(entry);
}
blockStates.put("palette", paletteTag);
if (palette.size() > 1) {
long[] data = new long[256];
for (int index = 0; index < 4096; index++) {
long value = voxels.index(index & 15, index >> 8, (index >> 4) & 15);
data[index / 16] |= value << ((index % 16) * 4);
}
blockStates.putLongArray("data", data);
}
section.put("block_states", blockStates);
return section;
}
private static void writeRegion(Path file, CompoundTag chunk) throws IOException {
ByteArrayOutputStream compressed = new ByteArrayOutputStream();
try (DeflaterOutputStream out = new DeflaterOutputStream(compressed)) {
new NBTSerializer(false).toStream(new NamedTag("", chunk), out);
}
byte[] payload = compressed.toByteArray();
int sectors = (payload.length + 5 + 4095) / 4096;
int index = (CHUNK_X & 31) + (CHUNK_Z & 31) * 32;
try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), "rw")) {
raf.setLength(8192L + sectors * 4096L);
raf.seek(index * 4L);
raf.writeInt((2 << 8) | sectors);
raf.seek(8192L);
raf.writeInt(payload.length + 1);
raf.writeByte(2);
raf.write(payload);
}
}
@FunctionalInterface
private interface VoxelPalette {
int index(int x, int localY, int z);
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.probe.fidelity;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class SurveyWindowTest {
@Test
public void coordinatesSitAtStrideCellCentres() {
SurveyWindow window = new SurveyWindow(0, 0, 65536, 256);
assertEquals(256, window.perAxis());
assertEquals(65536L, window.samples());
assertEquals(-32768 + 128, window.coordinate(0, 0));
assertEquals(32768 - 128, window.coordinate(0, 255));
}
@Test
public void windowMustBeAPositiveMultipleOfTheStride() {
assertThrows(IllegalArgumentException.class, () -> new SurveyWindow(0, 0, 1000, 256));
assertThrows(IllegalArgumentException.class, () -> new SurveyWindow(0, 0, 128, 256));
assertThrows(IllegalArgumentException.class, () -> new SurveyWindow(0, 0, 256, 0));
}
}
@@ -0,0 +1,184 @@
package art.arcane.iris.probe.fidelity;
final class SyntheticFidelityChunk implements FidelityChunk {
private static final int CANOPY_DEPTH = 3;
private final int minY;
private int surfaceY = 64;
private int seaLevel = Integer.MIN_VALUE;
private int biomeSplit = 16;
private String westBiome = "plains";
private String eastBiome = "forest";
private int caveMinY;
private int caveMaxY;
private int caveColumns;
private int treeColumns;
private int treeHeight;
private int canopyColumns;
private int canopyY;
private String canopyCap;
private int roofColumns;
private int roofMinY;
private int roofMaxY;
private int lavaMinY;
private int lavaMaxY;
private int lavaColumns;
private int wallThickness;
private int overhangColumns;
private int overhangMinY;
private int overhangMaxY;
private boolean blocks = true;
SyntheticFidelityChunk(int minY) {
this.minY = minY;
}
SyntheticFidelityChunk surface(int y) {
surfaceY = y;
return this;
}
SyntheticFidelityChunk sea(int y) {
seaLevel = y;
return this;
}
SyntheticFidelityChunk biomes(String west, String east, int split) {
westBiome = west;
eastBiome = east;
biomeSplit = split;
return this;
}
SyntheticFidelityChunk caves(int fromY, int toY, int columns) {
caveMinY = fromY;
caveMaxY = toY;
caveColumns = columns;
return this;
}
SyntheticFidelityChunk trees(int columns, int height) {
treeColumns = columns;
treeHeight = height;
return this;
}
SyntheticFidelityChunk canopy(int columns, int leavesY, String cap) {
canopyColumns = columns;
canopyY = leavesY;
canopyCap = cap;
return this;
}
SyntheticFidelityChunk roof(int columns, int fromY, int toY) {
roofColumns = columns;
roofMinY = fromY;
roofMaxY = toY;
return this;
}
SyntheticFidelityChunk lavaPool(int fromY, int toY, int columns) {
lavaMinY = fromY;
lavaMaxY = toY;
lavaColumns = columns;
return this;
}
SyntheticFidelityChunk walls(int thickness) {
wallThickness = thickness;
return this;
}
SyntheticFidelityChunk overhang(int columns, int fromY, int toY) {
overhangColumns = columns;
overhangMinY = fromY;
overhangMaxY = toY;
return this;
}
SyntheticFidelityChunk noBlocks() {
blocks = false;
return this;
}
@Override
public int surfaceY(int localX, int localZ) {
if (localX < roofColumns) {
return roofMaxY;
}
if (localX < canopyColumns) {
return canopyY + CANOPY_DEPTH;
}
return localX < treeColumns ? surfaceY + treeHeight : surfaceY;
}
@Override
public String surfaceBiome(int localX, int localZ) {
return localX < biomeSplit ? westBiome : eastBiome;
}
@Override
public boolean open(int localX, int y, int localZ) {
String id = id(localX, y, localZ);
return id.equals("minecraft:air") || id.equals("minecraft:cave_air");
}
@Override
public boolean fluid(int localX, int y, int localZ) {
String id = id(localX, y, localZ);
return id.equals("minecraft:water") || id.equals("minecraft:lava");
}
@Override
public String block(int localX, int y, int localZ) {
return blocks ? id(localX, y, localZ) : null;
}
@Override
public boolean terrainMass(int localX, int y, int localZ) {
return y <= surfaceY && !overhang(localX, y);
}
@Override
public boolean wallsKnown() {
return wallThickness > 0;
}
@Override
public boolean wall(int localX, int y, int localZ) {
return localX >= caveColumns && localX < caveColumns + wallThickness && y >= caveMinY && y < caveMaxY;
}
private String id(int localX, int y, int localZ) {
if (y < minY) {
return "minecraft:void_air";
}
if (y <= surfaceY) {
if (localX < lavaColumns && y >= lavaMinY && y < lavaMaxY) {
return "minecraft:lava";
}
if (overhang(localX, y)) {
return "minecraft:air";
}
boolean cave = localX < caveColumns && y >= caveMinY && y < caveMaxY;
return cave ? "minecraft:cave_air" : y == surfaceY ? "minecraft:grass_block" : "minecraft:stone";
}
if (localX < roofColumns && y >= roofMinY && y <= roofMaxY) {
return "minecraft:stone";
}
if (localX < canopyColumns && y >= canopyY && y <= canopyY + CANOPY_DEPTH) {
return y == canopyY + CANOPY_DEPTH ? canopyCap : "minecraft:spruce_leaves";
}
if (localX < treeColumns && y <= surfaceY + treeHeight) {
return y == surfaceY + treeHeight ? "minecraft:oak_leaves" : "minecraft:oak_log";
}
if (y <= seaLevel) {
return "minecraft:water";
}
return "minecraft:air";
}
private boolean overhang(int localX, int y) {
return localX < overhangColumns && y >= overhangMinY && y < overhangMaxY;
}
}