mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Datapack export
- /iris datapack export compiles one pack dimension into a worldgen datapack for Minecraft 26.3 and later - Bukkit and mod loaders take the same version, mode, namespace, output, format, objects and replace arguments - The default overworld mode replaces the Overworld of a new world with the default world type - Preset and dimension modes write a selectable world preset or a separate dimension - The export runs in the background and publishes the zip or folder atomically - Pack compiler stages turn noise, terrain, caves, subterrain, biomes, surface, decoration, objects and structures into a version-neutral IR - Capability lowering fits every field to 26.3 density function ranges - Disjoint min and max nodes fold to the input that always wins - The 26.3 writer emits density functions, noise settings, biomes, features, structures, presets and dimensions - Every export carries the Iris logo as its pack.png - Unused objects, templates and features are pruned from the output - The pruner keeps pack.png without reading references from its bytes - Imported pack datapacks are bundled into the export - Every approximation reports an export limit diagnostic - Generation cost is counted against a node budget and reported as a warning - Cave biomes over the wall palette limit use the closest kept palette - Jigsaw exports share the template encoder, block states, destinations and publisher with the worldgen export - DataVersion carries the pack format minor and world data version - Both platforms expose vanilla data from the server classpath - GoldenHashEngine and the export probes share one chunk spiral scan - Headless 26.3 probes measure export cost and compare column and chunk likeness against Iris - Real-codec tests decode the exported resources with the 26.3 registries - Paper registry tests run in isolated registry scopes - Command help and messages cover the export in every language file
This commit is contained in:
1 parent
b00e28e42e
commit
1ed1044474
385 files changed
+48102
-535
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user