Terrain-first hydrology

- Replace the river package with the hydrology planner, footprint compiler, cave containment and runtime
- Add the hydrology pack schema, validator, probes and tests
- Wire hydrology into IrisComplex, actuators, mantle, Vision and commands
This commit is contained in:
Brian Neumann-Fopiano
2026-09-01 18:28:00 -04:00
parent bdaa84975a
commit 48afa0d93f
352 changed files with 57955 additions and 19113 deletions
+136
View File
@@ -49,6 +49,15 @@ tasks.named('test').configure {
dependsOn(':core:compileJava')
}
tasks.register('hydrologyProbe', JavaExec) {
group = 'verification'
description = 'Scans deterministic hydrology seeds and reports accepted feature coordinates plus cold and warm planner timings.'
mainClass = 'art.arcane.iris.probe.HydrologyPlannerProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector')
dependsOn(':core:compileJava')
}
Provider<String> probePack = providers.gradleProperty('probePack')
Provider<String> probeDimension = providers.gradleProperty('probeDimension')
Provider<String> probeWarmupChunks = providers.gradleProperty('probeWarmupChunks').orElse('256')
@@ -57,6 +66,22 @@ Provider<String> probeCenterChunkX = providers.gradleProperty('probeCenterChunkX
Provider<String> probeCenterChunkZ = providers.gradleProperty('probeCenterChunkZ').orElse('2048')
Provider<String> probeMulticore = providers.gradleProperty('probeMulticore').orElse('false')
Provider<String> probeStudio = providers.gradleProperty('probeStudio').orElse('false')
Provider<String> explicitProbeMulticore = providers.gradleProperty('probeMulticore')
Provider<String> explicitProbeStudio = providers.gradleProperty('probeStudio')
Provider<String> probeSeeds = providers.gradleProperty('probeSeeds')
Provider<String> probeMinimumTileX = providers.gradleProperty('probeMinimumTileX')
Provider<String> probeMaximumTileX = providers.gradleProperty('probeMaximumTileX')
Provider<String> probeMinimumTileZ = providers.gradleProperty('probeMinimumTileZ')
Provider<String> probeMaximumTileZ = providers.gradleProperty('probeMaximumTileZ')
Provider<String> probeRequiredCoverage = providers.gradleProperty('probeRequiredCoverage')
Provider<String> probeSeed = providers.gradleProperty('probeSeed')
Provider<String> probeMinimumChunkX = providers.gradleProperty('probeMinimumChunkX')
Provider<String> probeMaximumChunkX = providers.gradleProperty('probeMaximumChunkX')
Provider<String> probeMinimumChunkZ = providers.gradleProperty('probeMinimumChunkZ')
Provider<String> probeMaximumChunkZ = providers.gradleProperty('probeMaximumChunkZ')
Provider<String> probeParallelism = providers.gradleProperty('probeParallelism')
Provider<String> probeShuffleSeed = providers.gradleProperty('probeShuffleSeed')
Provider<String> probeJfr = providers.gradleProperty('probeJfr')
tasks.register('genProbe', JavaExec) {
group = 'verification'
@@ -84,6 +109,117 @@ tasks.register('genProbe', JavaExec) {
}
}
tasks.register('hydrologyPackProbe', JavaExec) {
group = 'verification'
description = 'Scans explicitly bounded real-pack hydrology tiles across multiple engine seeds and enforces accepted feature/profile coverage.'
mainClass = 'art.arcane.iris.probe.HydrologyPackProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector')
dependsOn(':core:compileJava')
doFirst {
Map<String, Provider<String>> required = [
probePack: probePack,
probeDimension: probeDimension,
probeSeeds: probeSeeds,
probeMinimumTileX: probeMinimumTileX,
probeMaximumTileX: probeMaximumTileX,
probeMinimumTileZ: probeMinimumTileZ,
probeMaximumTileZ: probeMaximumTileZ,
probeRequiredCoverage: probeRequiredCoverage,
probeStudio: explicitProbeStudio
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("hydrologyPackProbe requires -P${name}=<value>")
}
}
args(
probePack.get(),
probeDimension.get(),
probeSeeds.get(),
probeMinimumTileX.get(),
probeMaximumTileX.get(),
probeMinimumTileZ.get(),
probeMaximumTileZ.get(),
probeRequiredCoverage.get(),
explicitProbeStudio.get())
}
}
tasks.register('generationOrderProbe', JavaExec) {
group = 'verification'
description = 'Compares complete real-pack chunk block and biome hashes across forward, reverse, shuffled and bounded-parallel generation orders.'
mainClass = 'art.arcane.iris.probe.GenerationOrderProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector')
dependsOn(':core:compileJava')
doFirst {
Map<String, Provider<String>> required = [
probePack: probePack,
probeDimension: probeDimension,
probeSeed: probeSeed,
probeMinimumChunkX: probeMinimumChunkX,
probeMaximumChunkX: probeMaximumChunkX,
probeMinimumChunkZ: probeMinimumChunkZ,
probeMaximumChunkZ: probeMaximumChunkZ,
probeParallelism: probeParallelism,
probeShuffleSeed: probeShuffleSeed,
probeMulticore: explicitProbeMulticore,
probeStudio: explicitProbeStudio
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("generationOrderProbe requires -P${name}=<value>")
}
}
args(
probePack.get(),
probeDimension.get(),
probeSeed.get(),
probeMinimumChunkX.get(),
probeMaximumChunkX.get(),
probeMinimumChunkZ.get(),
probeMaximumChunkZ.get(),
probeParallelism.get(),
probeShuffleSeed.get(),
explicitProbeMulticore.get(),
explicitProbeStudio.get())
}
}
['hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe'].each { String taskName ->
tasks.named(taskName, JavaExec).configure { JavaExec task ->
doFirst {
if (probeJfr.isPresent()) {
String requestedPath = probeJfr.get()
if (requestedPath.isBlank()) {
throw new GradleException('probeJfr must be a nonblank absolute path')
}
File recording = new File(requestedPath)
if (!recording.isAbsolute()) {
throw new GradleException('probeJfr must be an absolute path')
}
if (recording.isDirectory()) {
throw new GradleException('probeJfr must name a recording file, not a directory')
}
java.nio.file.Path normalized = recording.canonicalFile.toPath()
java.nio.file.Path repository = rootProject.projectDir.canonicalFile.toPath()
if (normalized.startsWith(repository)) {
throw new GradleException('probeJfr must point outside the repository')
}
File parent = normalized.toFile().parentFile
if (parent == null || !parent.isDirectory()) {
throw new GradleException('probeJfr parent directory must already exist')
}
task.jvmArgs(
'-XX:FlightRecorderOptions=stackdepth=256',
"-XX:StartFlightRecording=filename=${normalized},settings=profile,dumponexit=true"
)
}
}
}
}
String deserializationPack = providers.gradleProperty('deserializationPack')
.orElse(project.file('src/main/resources/deserialization-pack').absolutePath)
.get()
@@ -0,0 +1,599 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public final class GenerationOrderProbe {
private static final String LOG_PREFIX = "[generation-order-probe]";
private static final int MAXIMUM_CHUNKS = 1_024;
private static final int MAXIMUM_PARALLELISM = 32;
private static final int MINIMUM_SAFE_CHUNK_COORDINATE = -134_217_728;
private static final int MAXIMUM_SAFE_CHUNK_COORDINATE = 134_217_727;
private GenerationOrderProbe() {
}
enum GenerationOrder {
FORWARD,
REVERSE,
SHUFFLED,
BOUNDED_PARALLEL
}
record ProbeConfiguration(
File packSource,
String dimensionKey,
long seed,
int minimumChunkX,
int maximumChunkX,
int minimumChunkZ,
int maximumChunkZ,
int parallelism,
long shuffleSeed,
boolean multicore,
boolean studio
) {
ProbeConfiguration {
if (packSource == null) {
throw new IllegalArgumentException("Pack folder is required.");
}
if (dimensionKey == null || dimensionKey.isBlank()
|| dimensionKey.chars().anyMatch(Character::isWhitespace)) {
throw new IllegalArgumentException("Dimension key must be non-blank and contain no whitespace.");
}
if (minimumChunkX > maximumChunkX || minimumChunkZ > maximumChunkZ) {
throw new IllegalArgumentException("Minimum chunk bounds cannot exceed maximum chunk bounds.");
}
if (minimumChunkX < MINIMUM_SAFE_CHUNK_COORDINATE
|| maximumChunkX > MAXIMUM_SAFE_CHUNK_COORDINATE
|| minimumChunkZ < MINIMUM_SAFE_CHUNK_COORDINATE
|| maximumChunkZ > MAXIMUM_SAFE_CHUNK_COORDINATE) {
throw new IllegalArgumentException("Chunk bounds overflow their block origins.");
}
long chunks = boundedArea(minimumChunkX, maximumChunkX, minimumChunkZ, maximumChunkZ);
if (chunks > MAXIMUM_CHUNKS) {
throw new IllegalArgumentException("Generation-order probe exceeds "
+ MAXIMUM_CHUNKS + " chunks.");
}
if (parallelism < 2 || parallelism > MAXIMUM_PARALLELISM) {
throw new IllegalArgumentException("Parallelism must be between 2 and "
+ MAXIMUM_PARALLELISM + ".");
}
}
static ProbeConfiguration parse(String[] arguments) {
if (arguments.length != 11) {
throw new IllegalArgumentException(
"Expected: <pack> <dimension> <seed> <minimumChunkX> <maximumChunkX> "
+ "<minimumChunkZ> <maximumChunkZ> <parallelism> <shuffleSeed> <multicore> <studio>");
}
return new ProbeConfiguration(
new File(arguments[0]),
arguments[1],
Long.parseLong(arguments[2]),
Integer.parseInt(arguments[3]),
Integer.parseInt(arguments[4]),
Integer.parseInt(arguments[5]),
Integer.parseInt(arguments[6]),
Integer.parseInt(arguments[7]),
Long.parseLong(arguments[8]),
RealPackProbeSupport.parseBoolean(arguments[9], "multicore"),
RealPackProbeSupport.parseBoolean(arguments[10], "studio")
);
}
int chunkCount() {
return Math.toIntExact(boundedArea(
minimumChunkX, maximumChunkX, minimumChunkZ, maximumChunkZ));
}
}
record ChunkCoordinate(int x, int z) implements Comparable<ChunkCoordinate> {
@Override
public int compareTo(ChunkCoordinate other) {
int zComparison = Integer.compare(z, other.z);
return zComparison != 0 ? zComparison : Integer.compare(x, other.x);
}
}
record ChunkHash(
String blocks,
String biomes,
String combined,
Map<String, Integer> blockCounts,
Map<String, Integer> blockCountsByY
) {
ChunkHash(String blocks, String biomes, String combined) {
this(blocks, biomes, combined, Map.of(), Map.of());
}
ChunkHash {
blockCounts = Map.copyOf(blockCounts);
blockCountsByY = Map.copyOf(blockCountsByY);
}
}
record AggregateSignature(String blocks, String biomes, String combined) {
}
record ChunkOutput(ChunkCoordinate coordinate, ChunkHash hash) {
}
record ModeResult(
GenerationOrder order,
Map<ChunkCoordinate, ChunkHash> chunks,
AggregateSignature signature,
long engineReadyNanos,
long generationNanos
) {
}
record ProbeResult(
String status,
String dimensionKey,
long seed,
int chunks,
int minimumChunkX,
int maximumChunkX,
int minimumChunkZ,
int maximumChunkZ,
int parallelism,
long shuffleSeed,
boolean multicore,
boolean studio,
AggregateSignature signature
) {
String machineLine() {
return String.format(
Locale.ROOT,
"IRIS_GENERATION_ORDER_RESULT version=1 status=%s dimension=%s seed=%d chunks=%d "
+ "chunk_x=%d..%d chunk_z=%d..%d parallelism=%d shuffle_seed=%d "
+ "multicore=%s studio=%s "
+ "block_signature=%s biome_signature=%s combined_signature=%s",
status,
dimensionKey,
seed,
chunks,
minimumChunkX,
maximumChunkX,
minimumChunkZ,
maximumChunkZ,
parallelism,
shuffleSeed,
multicore,
studio,
signature.blocks(),
signature.biomes(),
signature.combined()
);
}
}
public static void main(String[] arguments) {
ProbeConfiguration configuration;
try {
configuration = ProbeConfiguration.parse(arguments);
} catch (Throwable failure) {
System.out.println(LOG_PREFIX + " FAIL: " + failure.getMessage());
failure.printStackTrace(System.out);
System.exit(2);
return;
}
try {
ProbeResult result = run(configuration);
System.out.println(result.machineLine());
System.exit(0);
} catch (Throwable failure) {
System.out.println(LOG_PREFIX + " FAIL: probe execution failed");
failure.printStackTrace(System.out);
AggregateSignature unavailable = new AggregateSignature("unavailable", "unavailable", "unavailable");
ProbeResult failed = new ProbeResult(
"FAIL",
configuration.dimensionKey(),
configuration.seed(),
configuration.chunkCount(),
configuration.minimumChunkX(),
configuration.maximumChunkX(),
configuration.minimumChunkZ(),
configuration.maximumChunkZ(),
configuration.parallelism(),
configuration.shuffleSeed(),
configuration.multicore(),
configuration.studio(),
unavailable
);
System.out.println(failed.machineLine());
System.exit(1);
}
}
static List<ChunkCoordinate> coordinates(ProbeConfiguration configuration) {
ArrayList<ChunkCoordinate> coordinates = new ArrayList<>(configuration.chunkCount());
for (long chunkZ = configuration.minimumChunkZ(); chunkZ <= configuration.maximumChunkZ(); chunkZ++) {
for (long chunkX = configuration.minimumChunkX(); chunkX <= configuration.maximumChunkX(); chunkX++) {
coordinates.add(new ChunkCoordinate((int) chunkX, (int) chunkZ));
}
}
return List.copyOf(coordinates);
}
static List<ChunkCoordinate> orderedCoordinates(
List<ChunkCoordinate> coordinates,
GenerationOrder order,
long shuffleSeed
) {
ArrayList<ChunkCoordinate> ordered = new ArrayList<>(coordinates);
switch (order) {
case FORWARD -> {
}
case REVERSE -> Collections.reverse(ordered);
case SHUFFLED, BOUNDED_PARALLEL -> Collections.shuffle(ordered, new Random(shuffleSeed));
}
return List.copyOf(ordered);
}
static AggregateSignature aggregateSignature(Map<ChunkCoordinate, ChunkHash> chunks) {
MessageDigest blocks = sha256();
MessageDigest biomes = sha256();
MessageDigest combined = sha256();
TreeMap<ChunkCoordinate, ChunkHash> ordered = new TreeMap<>(chunks);
for (Map.Entry<ChunkCoordinate, ChunkHash> entry : ordered.entrySet()) {
ChunkCoordinate coordinate = entry.getKey();
ChunkHash hash = entry.getValue();
updateDigest(blocks, coordinate.x() + "," + coordinate.z());
updateDigest(blocks, hash.blocks());
updateDigest(biomes, coordinate.x() + "," + coordinate.z());
updateDigest(biomes, hash.biomes());
updateDigest(combined, coordinate.x() + "," + coordinate.z());
updateDigest(combined, hash.combined());
}
return new AggregateSignature(
HexFormat.of().formatHex(blocks.digest()),
HexFormat.of().formatHex(biomes.digest()),
HexFormat.of().formatHex(combined.digest())
);
}
static List<ChunkCoordinate> mismatchedChunks(
Map<ChunkCoordinate, ChunkHash> expected,
Map<ChunkCoordinate, ChunkHash> observed
) {
TreeMap<ChunkCoordinate, ChunkHash> all = new TreeMap<>(expected);
for (Map.Entry<ChunkCoordinate, ChunkHash> entry : observed.entrySet()) {
all.putIfAbsent(entry.getKey(), entry.getValue());
}
ArrayList<ChunkCoordinate> mismatches = new ArrayList<>();
for (ChunkCoordinate coordinate : all.keySet()) {
if (!Objects.equals(expected.get(coordinate), observed.get(coordinate))) {
mismatches.add(coordinate);
}
}
return List.copyOf(mismatches);
}
static ChunkHash hashChunk(
ChunkCoordinate coordinate,
Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes,
int height
) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
MessageDigest combinedDigest = sha256();
HashMap<String, byte[]> encodedKeys = new HashMap<>();
HashMap<String, Integer> blockCounts = new HashMap<>();
HashMap<String, Integer> blockCountsByY = new HashMap<>();
updateDigest(blockDigest, coordinate.x() + "," + coordinate.z() + "," + height);
updateDigest(biomeDigest, coordinate.x() + "," + coordinate.z() + "," + height);
updateDigest(combinedDigest, coordinate.x() + "," + coordinate.z() + "," + height);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
PlatformBlockState state = blocks.get(x, y, z);
PlatformBiome biome = biomes.get(x, y, z);
String blockKey = state == null ? "minecraft:air" : state.key();
String biomeKey = biome == null ? "<null>" : biome.key();
blockCounts.merge(blockKey, 1, Integer::sum);
blockCountsByY.merge(y + ":" + blockKey, 1, Integer::sum);
byte[] encodedBlock = encoded(encodedKeys, blockKey);
byte[] encodedBiome = encoded(encodedKeys, biomeKey);
updateDigest(blockDigest, encodedBlock);
updateDigest(biomeDigest, encodedBiome);
combinedDigest.update((byte) 'B');
updateDigest(combinedDigest, encodedBlock);
combinedDigest.update((byte) 'M');
updateDigest(combinedDigest, encodedBiome);
}
}
}
return new ChunkHash(
HexFormat.of().formatHex(blockDigest.digest()),
HexFormat.of().formatHex(biomeDigest.digest()),
HexFormat.of().formatHex(combinedDigest.digest()),
blockCounts,
blockCountsByY
);
}
private static ProbeResult run(ProbeConfiguration configuration) throws Exception {
System.out.println(LOG_PREFIX + " pack: " + configuration.packSource().getAbsolutePath());
System.out.println(LOG_PREFIX + " dimension: " + configuration.dimensionKey());
System.out.println(LOG_PREFIX + " seed: " + configuration.seed());
System.out.println(LOG_PREFIX + " chunk bounds: " + configuration.minimumChunkX() + ".."
+ configuration.maximumChunkX() + "," + configuration.minimumChunkZ() + ".."
+ configuration.maximumChunkZ());
System.out.println(LOG_PREFIX + " chunks: " + configuration.chunkCount());
System.out.println(LOG_PREFIX + " parallelism: " + configuration.parallelism());
System.out.println(LOG_PREFIX + " shuffle seed: " + configuration.shuffleSeed());
System.out.println(LOG_PREFIX + " multicore: " + configuration.multicore());
System.out.println(LOG_PREFIX + " studio: " + configuration.studio());
List<ChunkCoordinate> coordinates = coordinates(configuration);
LinkedHashMap<GenerationOrder, ModeResult> results = new LinkedHashMap<>();
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
configuration.packSource(), configuration.dimensionKey(), LOG_PREFIX)) {
for (GenerationOrder order : GenerationOrder.values()) {
ModeResult result = generateMode(configuration, workspace, coordinates, order);
results.put(order, result);
printMode(result);
}
}
ModeResult baseline = results.get(GenerationOrder.FORWARD);
for (GenerationOrder order : GenerationOrder.values()) {
if (order == GenerationOrder.FORWARD) {
continue;
}
ModeResult observed = results.get(order);
List<ChunkCoordinate> mismatches = mismatchedChunks(baseline.chunks(), observed.chunks());
if (mismatches.isEmpty()) {
continue;
}
int reportCount = Math.min(16, mismatches.size());
for (int index = 0; index < reportCount; index++) {
ChunkCoordinate coordinate = mismatches.get(index);
ChunkHash expected = baseline.chunks().get(coordinate);
ChunkHash actual = observed.chunks().get(coordinate);
System.out.println("IRIS_GENERATION_ORDER_MISMATCH mode=" + order.name()
+ " chunk_x=" + coordinate.x() + " chunk_z=" + coordinate.z()
+ " forward_blocks=" + expected.blocks()
+ " observed_blocks=" + actual.blocks()
+ " forward_biomes=" + expected.biomes()
+ " observed_biomes=" + actual.biomes()
+ " block_count_delta=" + blockCountDelta(expected, actual)
+ " block_y_count_delta=" + blockYCountDelta(expected, actual));
}
throw new IllegalStateException(order + " generation differed from forward generation in "
+ mismatches.size() + " chunk(s).");
}
return new ProbeResult(
"PASS",
configuration.dimensionKey(),
configuration.seed(),
configuration.chunkCount(),
configuration.minimumChunkX(),
configuration.maximumChunkX(),
configuration.minimumChunkZ(),
configuration.maximumChunkZ(),
configuration.parallelism(),
configuration.shuffleSeed(),
configuration.multicore(),
configuration.studio(),
baseline.signature()
);
}
private static ModeResult generateMode(
ProbeConfiguration configuration,
RealPackProbeSupport.Workspace workspace,
List<ChunkCoordinate> coordinates,
GenerationOrder order
) throws Exception {
List<ChunkCoordinate> scheduled = orderedCoordinates(coordinates, order, configuration.shuffleSeed());
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(
configuration.seed(), configuration.studio(), order.name().toLowerCase(Locale.ROOT))) {
RealPackProbeSupport.drainReported();
long started = System.nanoTime();
Map<ChunkCoordinate, ChunkHash> chunks = order == GenerationOrder.BOUNDED_PARALLEL
? generateParallel(session.engine(), scheduled, configuration)
: generateSequential(session.engine(), scheduled, configuration);
long generationNanos = System.nanoTime() - started;
List<Throwable> reports = RealPackProbeSupport.settleAndDrain();
if (!reports.isEmpty()) {
RealPackProbeSupport.printReports(LOG_PREFIX, order + " generation reports", reports);
throw reportedFailure(order + " generation", reports);
}
TreeMap<ChunkCoordinate, ChunkHash> ordered = new TreeMap<>(chunks);
return new ModeResult(
order,
Collections.unmodifiableMap(ordered),
aggregateSignature(ordered),
session.readyNanos(),
generationNanos
);
}
}
private static Map<ChunkCoordinate, ChunkHash> generateSequential(
Engine engine,
List<ChunkCoordinate> coordinates,
ProbeConfiguration configuration
) throws Exception {
LinkedHashMap<ChunkCoordinate, ChunkHash> chunks = new LinkedHashMap<>(coordinates.size());
int completed = 0;
for (ChunkCoordinate coordinate : coordinates) {
ChunkOutput output = generateChunk(engine, coordinate, configuration.multicore());
chunks.put(output.coordinate(), output.hash());
completed++;
printProgress(completed, coordinates.size());
}
return chunks;
}
private static Map<ChunkCoordinate, ChunkHash> generateParallel(
Engine engine,
List<ChunkCoordinate> coordinates,
ProbeConfiguration configuration
) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(configuration.parallelism());
try {
ArrayList<Future<ChunkOutput>> futures = new ArrayList<>(coordinates.size());
for (ChunkCoordinate coordinate : coordinates) {
futures.add(executor.submit(() -> generateChunk(engine, coordinate, configuration.multicore())));
}
LinkedHashMap<ChunkCoordinate, ChunkHash> chunks = new LinkedHashMap<>(coordinates.size());
int completed = 0;
for (Future<ChunkOutput> future : futures) {
ChunkOutput output;
try {
output = future.get();
} catch (ExecutionException failure) {
Throwable cause = failure.getCause();
if (cause instanceof Error error) {
throw error;
}
if (cause instanceof Exception exception) {
throw exception;
}
throw new IllegalStateException(cause);
}
chunks.put(output.coordinate(), output.hash());
completed++;
printProgress(completed, coordinates.size());
}
return chunks;
} finally {
executor.shutdown();
if (!executor.awaitTermination(30L, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
}
}
private static ChunkOutput generateChunk(
Engine engine,
ChunkCoordinate coordinate,
boolean multicore
) throws Exception {
int height = engine.getTarget().getHeight();
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore);
return new ChunkOutput(coordinate, hashChunk(coordinate, blocks, biomes, height));
}
private static void printProgress(int completed, int total) {
if (completed == total || completed % 32 == 0) {
System.out.println(LOG_PREFIX + " generation progress: " + completed + "/" + total);
}
}
private static void printMode(ModeResult result) {
System.out.printf(
Locale.ROOT,
"IRIS_GENERATION_ORDER_MODE mode=%s chunks=%d engine_ready_ms=%.3f generation_ms=%.3f "
+ "block_signature=%s biome_signature=%s combined_signature=%s%n",
result.order().name(),
result.chunks().size(),
result.engineReadyNanos() / 1_000_000D,
result.generationNanos() / 1_000_000D,
result.signature().blocks(),
result.signature().biomes(),
result.signature().combined()
);
}
private static IllegalStateException reportedFailure(String operation, List<Throwable> reports) {
IllegalStateException failure = new IllegalStateException(
operation + " reported " + reports.size() + " error(s).");
for (Throwable report : reports) {
failure.addSuppressed(report);
}
return failure;
}
static Map<String, Integer> blockCountDelta(ChunkHash expected, ChunkHash observed) {
return countDelta(expected.blockCounts(), observed.blockCounts());
}
static Map<String, Integer> blockYCountDelta(ChunkHash expected, ChunkHash observed) {
return countDelta(expected.blockCountsByY(), observed.blockCountsByY());
}
private static Map<String, Integer> countDelta(
Map<String, Integer> expected,
Map<String, Integer> observed
) {
TreeMap<String, Integer> deltas = new TreeMap<>();
for (Map.Entry<String, Integer> entry : expected.entrySet()) {
deltas.put(entry.getKey(), -entry.getValue());
}
for (Map.Entry<String, Integer> entry : observed.entrySet()) {
deltas.merge(entry.getKey(), entry.getValue(), Integer::sum);
}
deltas.entrySet().removeIf(entry -> entry.getValue() == 0);
return Collections.unmodifiableMap(deltas);
}
private static long boundedArea(int minimumX, int maximumX, int minimumZ, int maximumZ) {
long width = (long) maximumX - minimumX + 1L;
long depth = (long) maximumZ - minimumZ + 1L;
try {
return Math.multiplyExact(width, depth);
} catch (ArithmeticException failure) {
throw new IllegalArgumentException("Chunk bounds overflow their bounded area.", failure);
}
}
private static void updateDigest(MessageDigest digest, String value) {
updateDigest(digest, value.getBytes(StandardCharsets.UTF_8));
}
private static void updateDigest(MessageDigest digest, byte[] value) {
digest.update(value);
digest.update((byte) 0);
}
private static byte[] encoded(Map<String, byte[]> cache, String value) {
byte[] encoded = cache.get(value);
if (encoded != null) {
return encoded;
}
byte[] created = value.getBytes(StandardCharsets.UTF_8);
cache.put(value, created);
return created;
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException(failure);
}
}
}
@@ -482,17 +482,28 @@ public final class GenerationProbe {
private static void updateSignature(MessageDigest digest, ChunkCoordinate coordinate,
Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, int height) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
updateDigest(digest, coordinate.x() + "," + coordinate.z());
updateDigest(blockDigest, coordinate.x() + "," + coordinate.z());
updateDigest(biomeDigest, coordinate.x() + "," + coordinate.z());
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
PlatformBlockState state = blocks.get(x, y, z);
PlatformBiome biome = biomes.get(x, y, z);
updateDigest(digest, state == null ? "minecraft:air" : state.key());
updateDigest(digest, biome == null ? "null" : biome.key());
String stateKey = state == null ? "minecraft:air" : state.key();
String biomeKey = biome == null ? "null" : biome.key();
updateDigest(digest, stateKey);
updateDigest(digest, biomeKey);
updateDigest(blockDigest, stateKey);
updateDigest(biomeDigest, biomeKey);
}
}
}
System.out.println("GENPROBE_CHUNK_HASH chunk=" + coordinate.x() + "," + coordinate.z()
+ " blocks=" + HexFormat.of().formatHex(blockDigest.digest()).substring(0, 16)
+ " biomes=" + HexFormat.of().formatHex(biomeDigest.digest()).substring(0, 16));
}
private static void updateDigest(MessageDigest digest, String value) {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,524 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyPlanner;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileCache;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public final class HydrologyPlannerProbe {
private static final long[] DEFAULT_SEEDS = {1L, 19L, 52L, 77L, 331L, 501L, 712L, 992L};
private static final long SURFACE_INLAND_SEED = 7012L;
private static final long UNDERGROUND_INLAND_SEED = 7013L;
private static final HydrologyTileKey ORIGIN = new HydrologyTileKey(0, 0);
private static final int WARM_SAMPLES = 2_048;
private HydrologyPlannerProbe() {
}
public static void main(String[] arguments) {
long[] seeds = parseSeeds(arguments);
long[] coldTimings = new long[seeds.length];
long[] warmTimings = new long[seeds.length];
long signature = 0xCBF29CE484222325L;
EnumMap<HydrologyFeatureType, HydrologyFeatureRef> firstFeatures =
new EnumMap<>(HydrologyFeatureType.class);
for (int seedIndex = 0; seedIndex < seeds.length; seedIndex++) {
long seed = seeds[seedIndex];
HydrologyPlanner planner = new HydrologyPlanner(seed, settings(), terrain(seed));
HydrologyTileCache cache = new HydrologyTileCache(planner, 32);
long coldStart = System.nanoTime();
HydrologyTile tile = cache.get(ORIGIN);
coldTimings[seedIndex] = System.nanoTime() - coldStart;
HydrologyTile repeated = new HydrologyPlanner(seed, settings(), terrain(seed)).plan(ORIGIN);
if (!tile.equals(repeated)) {
throw new IllegalStateException("Hydrology plan changed across deterministic replay for seed " + seed);
}
EnumMap<HydrologyFeatureType, Integer> counts = new EnumMap<>(HydrologyFeatureType.class);
for (HydrologyFeatureRef feature : tile.features()) {
counts.merge(feature.type(), 1, Integer::sum);
signature = mix(signature, feature.id());
signature = mix(signature, feature.type().ordinal());
signature = mix(signature, feature.x());
signature = mix(signature, feature.y());
signature = mix(signature, feature.z());
}
collectAccepted(firstFeatures, tile);
long warmStart = System.nanoTime();
for (int sampleIndex = 0; sampleIndex < WARM_SAMPLES; sampleIndex++) {
int x = Math.floorMod(sampleIndex * 1_229, 256);
int z = Math.floorMod(sampleIndex * 811, 256);
Optional<HydrologyColumnSample> sample = cache.columnAt(x, z);
signature = mix(signature, sample.map(HydrologyColumnSample::layers).map(List::size).orElse(0));
}
warmTimings[seedIndex] = System.nanoTime() - warmStart;
System.out.printf(
Locale.ROOT,
"IRIS_HYDROLOGY_SEED seed=%d cold_nanos=%d warm_nanos=%d courses=%d features=%d diagnostics=%d counts=%s%n",
seed,
coldTimings[seedIndex],
warmTimings[seedIndex],
tile.courses().size(),
tile.features().size(),
tile.diagnosticCandidates().size(),
counts
);
cache.close();
}
collectAccepted(firstFeatures, surfaceInlandTile());
collectAccepted(firstFeatures, undergroundInlandTile());
collectAccepted(firstFeatures, surfaceMouthTile());
requireCompleteCoverage(firstFeatures.keySet());
for (Map.Entry<HydrologyFeatureType, HydrologyFeatureRef> entry : firstFeatures.entrySet()) {
HydrologyFeatureRef feature = entry.getValue();
System.out.printf(
Locale.ROOT,
"IRIS_HYDROLOGY_COORDINATE type=%s x=%d y=%d z=%d id=%016x%n",
entry.getKey(),
feature.x(),
feature.y(),
feature.z(),
feature.id()
);
}
long[] sortedCold = coldTimings.clone();
long[] sortedWarm = warmTimings.clone();
Arrays.sort(sortedCold);
Arrays.sort(sortedWarm);
System.out.printf(
Locale.ROOT,
"IRIS_HYDROLOGY_RESULT version=1 seeds=%d warm_samples=%d cold_median_nanos=%d cold_p95_nanos=%d warm_median_nanos=%d warm_p95_nanos=%d feature_types=%d signature=%016x%n",
seeds.length,
WARM_SAMPLES,
percentile(sortedCold, 0.5D),
percentile(sortedCold, 0.95D),
percentile(sortedWarm, 0.5D),
percentile(sortedWarm, 0.95D),
firstFeatures.size(),
signature
);
}
static Map<HydrologyFeatureType, HydrologyFeatureRef> deterministicAcceptedCoverage() {
EnumMap<HydrologyFeatureType, HydrologyFeatureRef> coverage = new EnumMap<>(HydrologyFeatureType.class);
for (long seed : DEFAULT_SEEDS) {
collectAccepted(
coverage,
new HydrologyPlanner(seed, settings(), terrain(seed)).plan(ORIGIN)
);
}
collectAccepted(coverage, surfaceInlandTile());
collectAccepted(coverage, undergroundInlandTile());
collectAccepted(coverage, surfaceMouthTile());
requireCompleteCoverage(coverage.keySet());
return Map.copyOf(coverage);
}
private static void collectAccepted(
Map<HydrologyFeatureType, HydrologyFeatureRef> features,
HydrologyTile tile
) {
for (HydrologyFeatureRef feature : tile.features()) {
features.putIfAbsent(feature.type(), feature);
}
}
private static void requireCompleteCoverage(Set<HydrologyFeatureType> observed) {
EnumSet<HydrologyFeatureType> missing = EnumSet.allOf(HydrologyFeatureType.class);
missing.removeAll(observed);
if (!missing.isEmpty()) {
throw new IllegalStateException("Hydrology accepted-feature coverage is missing " + missing + ".");
}
}
private static HydrologyTile surfaceInlandTile() {
return new HydrologyPlanner(
SURFACE_INLAND_SEED,
inlandSettings(0D, 0D),
inlandTerrain(true, false),
solidCaveView()
).plan(ORIGIN);
}
private static HydrologyTile undergroundInlandTile() {
return new HydrologyPlanner(
UNDERGROUND_INLAND_SEED,
inlandSettings(0D, 1D),
inlandTerrain(false, true),
solidCaveView()
).plan(ORIGIN);
}
private static HydrologyTile surfaceMouthTile() {
return new HydrologyPlanner(
DEFAULT_SEEDS[0],
settings(false),
terrain(DEFAULT_SEEDS[0])
).plan(ORIGIN);
}
private static HydrologyPlannerSettings inlandSettings(
double surfaceDensity,
double undergroundDensity
) {
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
true,
surfaceDensity,
80,
0,
6,
24
);
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
true,
undergroundDensity,
Integer.MIN_VALUE,
0,
4,
32
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(
128,
16,
4,
512,
256,
new HydrologyPlannerSettings.Branching(32, 16),
0.5D,
12D,
0.5D,
0.1D
),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
4,
18,
2,
4,
4,
8,
10,
1.5D,
4,
8,
true,
96,
8
),
new HydrologyPlannerSettings.Hydraulics(12, 28, 1, 3, 8, 4),
new HydrologyPlannerSettings.Underground(
undergroundDensity > 0D,
undergroundSources,
68,
82,
4,
12,
2,
4,
5,
9,
true
),
new HydrologyPlannerSettings.Outlets(
false,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
true,
12,
32,
2,
4
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of()
);
}
private static HydrologyTerrainSampler inlandTerrain(
boolean surfaceSource,
boolean undergroundSource
) {
return (int x, int z) -> {
boolean source = x == 0 && z == 0;
boolean outlet = x == 64 && z == 64;
int height = 120 - Math.floorDiv(x + z, 16);
return new HydrologyTerrainSample(
height,
1D,
false,
true,
72,
74,
true,
outlet,
surfaceSource && source,
surfaceSource && source,
undergroundSource && source,
undergroundSource && source,
0D,
surfaceSource && source ? 1D : 0D,
undergroundSource && source ? 1D : 0D,
1D,
1D,
1D,
1D,
"parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("water")
);
};
}
private static CaveVoxelView solidCaveView() {
return new CaveVoxelView() {
@Override
public boolean isInWorld(CavePosition position) {
return position.y() > -4096 && position.y() < 4096;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
return CaveVoxel.SOLID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
return false;
}
@Override
public boolean isAboveTerrainSurface(CavePosition position) {
return false;
}
};
}
private static HydrologyPlannerSettings settings() {
return settings(true);
}
private static HydrologyPlannerSettings settings(boolean coastalGrottosEnabled) {
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
true,
4D,
80,
1,
6,
32
);
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
true,
4D,
Integer.MIN_VALUE,
1,
4,
48
);
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
"deep_lava",
true,
3D,
48,
18,
42,
3,
5,
2,
3,
8,
24,
4,
2,
3,
4096,
4,
true,
true
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(
256,
16,
4,
1_089,
256,
new HydrologyPlannerSettings.Branching(32, 16),
0.5D,
12D,
0.5D,
0.2D
),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
4,
20,
2,
5,
4,
8,
12,
1.5D,
4,
10,
true,
96,
10
),
new HydrologyPlannerSettings.Hydraulics(12, 40, 1, 3, 8, 4),
new HydrologyPlannerSettings.Underground(
true,
undergroundSources,
68,
82,
4,
14,
2,
5,
6,
12,
true
),
new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(coastalGrottosEnabled, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
true,
12,
48,
2,
8
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(deepFluid)
);
}
private static HydrologyTerrainSampler terrain(long seed) {
return (int x, int z) -> {
if (x >= 224) {
return oceanTerrain();
}
double wave = StrictMath.sin((z + seed % 37L) / 19D) * 3D;
double ridge = x >= 96 && x <= 128 ? 34D : 0D;
double cliff = x >= 144 ? -24D : 0D;
int height = 126 - Math.floorDiv(x, 13) + (int) StrictMath.round(wave + ridge + cliff);
double slope = ridge > 0D || x >= 136 && x <= 144
? 18D
: 1D + StrictMath.abs(wave) * 0.25D;
boolean surfaceSource = x >= 0 && x <= 48;
boolean undergroundSource = x >= 0 && x <= 64;
return new HydrologyTerrainSample(
height,
slope,
false,
true,
70,
74 + Math.floorMod(x + z, 7),
true,
true,
surfaceSource,
surfaceSource,
undergroundSource,
undergroundSource,
0D,
surfaceSource ? 1D : 0D,
undergroundSource ? 1D : 0D,
1D,
1D,
1D,
1D,
"parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("water")
);
};
}
private static HydrologyTerrainSample oceanTerrain() {
return new HydrologyTerrainSample(
54,
0D,
true,
false,
30,
32,
false,
false,
false,
false,
false,
false,
0D,
0D,
0D,
1D,
1D,
1D,
1D,
"ocean_parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("water")
);
}
private static long[] parseSeeds(String[] arguments) {
if (arguments.length == 0) {
return DEFAULT_SEEDS.clone();
}
long[] seeds = new long[arguments.length];
for (int index = 0; index < arguments.length; index++) {
seeds[index] = Long.parseLong(arguments[index]);
}
return seeds;
}
private static long percentile(long[] sorted, double percentile) {
int index = Math.max(0, (int) StrictMath.ceil(sorted.length * percentile) - 1);
return sorted[index];
}
private static long mix(long hash, long value) {
return (hash ^ value) * 0x100000001B3L;
}
}
@@ -0,0 +1,527 @@
package art.arcane.iris.probe;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.stream.Stream;
final class RealPackProbeSupport {
private static final List<Throwable> REPORTED = Collections.synchronizedList(new ArrayList<>());
private RealPackProbeSupport() {
}
static Workspace openWorkspace(File packSource, String dimensionKey, String logPrefix) throws Exception {
if (packSource == null || !packSource.isDirectory()) {
throw new IllegalArgumentException("Pack folder not found: "
+ (packSource == null ? "null" : packSource.getAbsolutePath()));
}
if (dimensionKey == null || dimensionKey.isBlank()
|| dimensionKey.chars().anyMatch(Character::isWhitespace)) {
throw new IllegalArgumentException("Dimension key must be non-blank and contain no whitespace.");
}
File workRoot = Files.createTempDirectory("iris-real-pack-probe-").toFile();
try {
File pack = clonePack(packSource, workRoot);
configureRuntime(new File(workRoot, "platform-validation"), false);
validatePack(pack, logPrefix);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, false);
} catch (Throwable failure) {
Throwable cleanupFailure = deleteWorkRoot(workRoot);
if (cleanupFailure != null) {
failure.addSuppressed(cleanupFailure);
}
throwAsException(failure);
throw new IllegalStateException("Unreachable");
}
}
static Workspace openPreparedWorkspace(
File pack,
String dimensionKey,
String logPrefix,
boolean hydrologyGeneratedVerification
) throws Exception {
if (pack == null || !pack.isDirectory()) {
throw new IllegalArgumentException("Prepared pack folder not found: "
+ (pack == null ? "null" : pack.getAbsolutePath()));
}
File workRoot = Files.createTempDirectory("iris-generated-chunk-probe-").toFile();
try {
configureRuntime(new File(workRoot, "platform-validation"), hydrologyGeneratedVerification);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, hydrologyGeneratedVerification);
} catch (Throwable failure) {
Throwable cleanupFailure = deleteWorkRoot(workRoot);
if (cleanupFailure != null) {
failure.addSuppressed(cleanupFailure);
}
throwAsException(failure);
throw new IllegalStateException("Unreachable");
}
}
static List<Throwable> drainReported() {
synchronized (REPORTED) {
List<Throwable> drained = new ArrayList<>(REPORTED);
REPORTED.clear();
return drained;
}
}
static List<Throwable> settleAndDrain() throws InterruptedException {
List<Throwable> drained = new ArrayList<>();
long quietSince = System.currentTimeMillis();
long start = quietSince;
while (System.currentTimeMillis() - start < 15_000L) {
List<Throwable> batch = drainReported();
if (batch.isEmpty()) {
if (System.currentTimeMillis() - quietSince >= 1_500L) {
break;
}
} else {
drained.addAll(batch);
quietSince = System.currentTimeMillis();
}
Thread.sleep(50L);
}
return drained;
}
static void printReports(String logPrefix, String label, List<Throwable> reports) {
if (reports.isEmpty()) {
return;
}
Map<String, Integer> distinct = new LinkedHashMap<>();
for (Throwable report : reports) {
distinct.merge(causeKey(report), 1, Integer::sum);
}
System.out.println(logPrefix + " " + label + " (" + distinct.size() + " distinct):");
for (Map.Entry<String, Integer> entry : distinct.entrySet()) {
System.out.println(" x" + entry.getValue() + " " + entry.getKey());
}
for (Throwable report : reports) {
report.printStackTrace(System.out);
}
}
static boolean parseBoolean(String value, String name) {
if ("true".equalsIgnoreCase(value)) {
return true;
}
if ("false".equalsIgnoreCase(value)) {
return false;
}
throw new IllegalArgumentException(name + " must be true or false.");
}
static GeneratedChunk generateChunk(Engine engine, int chunkX, int chunkZ) {
int height = engine.getTarget().getHeight();
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
} catch (Exception failure) {
throw new IllegalStateException("Failed to generate probe chunk " + chunkX + "," + chunkZ, failure);
}
return new GeneratedChunk(chunkX, chunkZ, height, blocks, biomes);
}
record GeneratedChunk(
int chunkX,
int chunkZ,
int height,
Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes
) {
GeneratedChunk {
if (height < 1 || blocks == null || biomes == null) {
throw new IllegalArgumentException("Generated chunk requires buffers and a positive height.");
}
}
PlatformBlockState blockAt(int blockX, int y, int blockZ) {
validatePosition(blockX, y, blockZ);
return blocks.get(Math.floorMod(blockX, 16), y, Math.floorMod(blockZ, 16));
}
PlatformBiome biomeAt(int blockX, int y, int blockZ) {
validatePosition(blockX, y, blockZ);
return biomes.get(Math.floorMod(blockX, 16), y, Math.floorMod(blockZ, 16));
}
private void validatePosition(int blockX, int y, int blockZ) {
if (Math.floorDiv(blockX, 16) != chunkX || Math.floorDiv(blockZ, 16) != chunkZ) {
throw new IllegalArgumentException("Block coordinate is outside the generated chunk.");
}
if (y < 0 || y >= height) {
throw new IllegalArgumentException("Block Y is outside the generated chunk height.");
}
}
}
static final class Workspace implements AutoCloseable {
private final File workRoot;
private final File pack;
private final String dimensionKey;
private final String logPrefix;
private final boolean hydrologyGeneratedVerification;
private int sessionSequence;
private boolean sessionOpen;
private boolean closed;
private Workspace(
File workRoot,
File pack,
String dimensionKey,
String logPrefix,
boolean hydrologyGeneratedVerification
) {
this.workRoot = workRoot;
this.pack = pack;
this.dimensionKey = dimensionKey;
this.logPrefix = logPrefix;
this.hydrologyGeneratedVerification = hydrologyGeneratedVerification;
}
EngineSession openEngine(long seed, boolean studio, String runLabel) throws Exception {
if (closed) {
throw new IllegalStateException("Probe workspace is closed.");
}
if (sessionOpen) {
throw new IllegalStateException("Only one real-pack engine session may be open at a time.");
}
sessionOpen = true;
int sequence = sessionSequence++;
File platformRoot = new File(workRoot, "platform-" + sequence);
File worldRoot = new File(workRoot, "world-" + sequence);
IrisData data = null;
Engine engine = null;
try {
configureRuntime(platformRoot, hydrologyGeneratedVerification);
data = IrisData.openRuntime(pack);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Dimension '" + dimensionKey
+ "' did not load from " + pack.getAbsolutePath());
}
IrisWorld world = IrisWorld.builder()
.platformIdentity("iris:probe")
.name("probe")
.seed(seed)
.worldFolder(worldRoot)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
long started = System.nanoTime();
engine = new IrisEngine(
new EngineTarget(world, dimension, data),
studio ? IrisEngine.InitializationMode.STUDIO : IrisEngine.InitializationMode.RUNTIME
);
long readyNanos = System.nanoTime() - started;
List<Throwable> initializationReports = settleAndDrain();
printReports(logPrefix, runLabel + " engine-init reports", initializationReports);
return new EngineSession(this, data, engine, readyNanos);
} catch (Throwable failure) {
Throwable cleanupFailure = closeResources(engine, data);
sessionOpen = false;
if (cleanupFailure != null) {
failure.addSuppressed(cleanupFailure);
}
throwAsException(failure);
throw new IllegalStateException("Unreachable");
}
}
File pack() {
return pack;
}
@Override
public void close() throws Exception {
if (closed) {
return;
}
if (sessionOpen) {
throw new IllegalStateException("Cannot close a probe workspace with an open engine session.");
}
closed = true;
IrisPlatforms.unbind();
StubPlatform.errorSink(null);
Throwable failure = deleteWorkRoot(workRoot);
if (failure != null) {
throwAsException(failure);
}
}
private void releaseSession() {
sessionOpen = false;
}
}
static final class EngineSession implements AutoCloseable {
private final Workspace workspace;
private final IrisData data;
private final Engine engine;
private final long readyNanos;
private boolean closed;
private EngineSession(Workspace workspace, IrisData data, Engine engine, long readyNanos) {
this.workspace = workspace;
this.data = data;
this.engine = engine;
this.readyNanos = readyNanos;
}
Engine engine() {
return engine;
}
long readyNanos() {
return readyNanos;
}
@Override
public void close() throws Exception {
if (closed) {
return;
}
closed = true;
Throwable failure = closeResources(engine, data);
List<Throwable> reported = drainReported();
if (!reported.isEmpty()) {
IllegalStateException reportedFailure = new IllegalStateException(
"The real-pack engine reported " + reported.size() + " error(s) during shutdown.");
for (Throwable report : reported) {
reportedFailure.addSuppressed(report);
}
failure = appendFailure(failure, reportedFailure);
}
workspace.releaseSession();
if (failure != null) {
throwAsException(failure);
}
}
}
private static final class InertPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class InertWorldManager implements EngineWorldManager {
@Override
public void close() {
}
@Override
public int getEntityCount() {
return 0;
}
@Override
public int getChunkCount() {
return 0;
}
@Override
public double getEntitySaturation() {
return 0;
}
@Override
public void onTick() {
}
@Override
public void onSave() {
}
}
private static final class InertEffects extends EngineAssignedComponent implements EngineEffects {
private InertEffects(Engine engine) {
super(engine, "FX");
}
@Override
public void updatePlayerMap() {
}
@Override
public void tickRandomPlayer() {
}
}
private static final class InertPlatformHooks implements EnginePlatformHooks {
private final boolean hydrologyGeneratedVerification;
private InertPlatformHooks(boolean hydrologyGeneratedVerification) {
this.hydrologyGeneratedVerification = hydrologyGeneratedVerification;
}
@Override
public boolean shouldGenerateMantleComponent(Engine engine, MantleComponent component) {
return !hydrologyGeneratedVerification || !(component instanceof MantleObjectComponent);
}
}
private static void configureRuntime(File platformRoot, boolean hydrologyGeneratedVerification) {
REPORTED.clear();
StubPlatform.bindGenerationStateHandlers();
StubPlatform.verbose(false);
StubPlatform.errorSink(REPORTED::add);
IrisServices.register(PreservationRegistry.class, new InertPreservation());
IrisServices.register(EngineWorldManagerProvider.class,
(EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new);
IrisServices.register(EnginePlatformHooks.class, new InertPlatformHooks(hydrologyGeneratedVerification));
IrisPlatforms.unbind();
IrisPlatforms.bind(new StubPlatform(platformRoot));
}
private static void validatePack(File pack, String logPrefix) {
PackValidationResult validation = PackValidator.validateForDatapackBootstrap(pack);
for (String warning : validation.getWarnings()) {
System.out.println(logPrefix + " pack warning: " + warning);
}
if (!validation.isLoadable()) {
for (String error : validation.getBlockingErrors()) {
System.out.println(logPrefix + " pack error: " + error);
}
throw new IllegalStateException("Pack validation blocked generation.");
}
System.out.println(logPrefix + " offline pack validation: PASS");
}
private static File clonePack(File source, File workRoot) throws Exception {
File destination = new File(workRoot, "pack");
Process clone = new ProcessBuilder("cp", "-Rc", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO()
.start();
if (clone.waitFor() != 0) {
deleteRecursively(destination.toPath());
Process copy = new ProcessBuilder("cp", "-R", source.getAbsolutePath(), destination.getAbsolutePath())
.inheritIO()
.start();
if (copy.waitFor() != 0) {
throw new IllegalStateException("Failed to copy pack to " + destination.getAbsolutePath());
}
}
return destination;
}
private static Throwable closeResources(Engine engine, IrisData data) {
Throwable failure = null;
if (engine != null) {
try {
engine.close();
} catch (Throwable engineFailure) {
failure = engineFailure;
}
}
if (data != null) {
try {
data.close();
} catch (Throwable dataFailure) {
failure = appendFailure(failure, dataFailure);
}
}
return failure;
}
private static Throwable deleteWorkRoot(File workRoot) {
try {
deleteRecursively(workRoot.toPath());
return null;
} catch (Throwable failure) {
return failure;
}
}
private static void deleteRecursively(Path root) throws Exception {
if (!Files.exists(root)) {
return;
}
List<Path> paths;
try (Stream<Path> walk = Files.walk(root)) {
paths = walk.sorted(Comparator.reverseOrder()).toList();
}
for (Path path : paths) {
Files.deleteIfExists(path);
}
}
private static Throwable appendFailure(Throwable failure, Throwable additional) {
if (failure == null) {
return additional;
}
if (failure != additional) {
failure.addSuppressed(additional);
}
return failure;
}
private static void throwAsException(Throwable failure) throws Exception {
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof Exception exception) {
throw exception;
}
throw new IllegalStateException(failure);
}
private static String causeKey(Throwable failure) {
List<Throwable> chain = new ArrayList<>();
Throwable cause = failure;
while (cause != null && !chain.contains(cause)) {
chain.add(cause);
cause = cause.getCause();
}
Throwable root = chain.getLast();
return root.getClass().getName() + ": " + root.getMessage();
}
}
@@ -1,157 +0,0 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.river.RiverBodyProfile;
import art.arcane.iris.engine.river.RiverEdgeId;
import art.arcane.iris.engine.river.RiverNode;
import art.arcane.iris.engine.river.RiverNodeId;
import art.arcane.iris.engine.river.RiverPolyline;
import art.arcane.iris.engine.river.RiverReach;
import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverSample;
import art.arcane.iris.engine.river.RiverTile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public final class RiverTileProbe {
private static final int WARMUP_ROUNDS = 5;
private static final int MEASURED_ROUNDS = 25;
private static final int SAMPLES_PER_ROUND = 8_192;
private RiverTileProbe() {
}
public static void main(String[] args) {
RiverTile tile = createTile();
long expectedSignature = sampleRound(tile);
for (int round = 0; round < WARMUP_ROUNDS; round++) {
requireSignature(expectedSignature, sampleRound(tile));
}
long[] timings = new long[MEASURED_ROUNDS];
for (int round = 0; round < MEASURED_ROUNDS; round++) {
long start = System.nanoTime();
long signature = sampleRound(tile);
timings[round] = System.nanoTime() - start;
requireSignature(expectedSignature, signature);
System.out.printf(Locale.ROOT,
"IRIS_RIVER_TILE_SAMPLE round=%d nanos=%d signature=%016x%n",
round,
timings[round],
signature);
}
long[] sorted = timings.clone();
Arrays.sort(sorted);
System.out.printf(Locale.ROOT,
"IRIS_RIVER_TILE_RESULT version=1 rounds=%d samples_per_round=%d median_nanos=%d p95_nanos=%d signature=%016x%n",
MEASURED_ROUNDS,
SAMPLES_PER_ROUND,
sorted[MEASURED_ROUNDS / 2],
sorted[(int) StrictMath.ceil(MEASURED_ROUNDS * 0.95D) - 1],
expectedSignature);
}
private static RiverTile createTile() {
List<RiverReach> reaches = new ArrayList<>(64);
for (int reachIndex = 0; reachIndex < 64; reachIndex++) {
RiverNode from = node(0L, reachIndex, 0D, reachIndex * 32D);
RiverNode to = node(1L, reachIndex, 2_048D, reachIndex * 32D);
RiverBodyProfile profile = profile(reachIndex);
double[] x = new double[33];
double[] z = new double[33];
for (int point = 0; point < x.length; point++) {
x[point] = point * 64D;
z[point] = reachIndex * 32D
+ StrictMath.sin(point * 0.625D + reachIndex * 0.25D) * 12D;
}
reaches.add(new RiverReach(
RiverEdgeId.of(from.id(), to.id()),
from,
to,
RiverRouteState.WET,
1 + reachIndex % 4,
1 + reachIndex % 3,
profile.maximumWidth(),
profile.maximumBankWidth(),
profile.maximumDepth(),
profile,
false,
false,
new RiverPolyline(x, z)
));
}
return new RiverTile(0, 0, 0, 0, 2_048, 2_048, reaches);
}
private static RiverBodyProfile profile(int reachIndex) {
double[] positions = new double[17];
double[] widths = new double[17];
double[] bankWidths = new double[17];
double[] depths = new double[17];
double[] roofScales = new double[17];
for (int index = 0; index < positions.length; index++) {
double position = index / 16D;
double body = StrictMath.sin(StrictMath.PI * position);
positions[index] = position;
widths[index] = 8D + reachIndex % 5 + body * 6D;
bankWidths[index] = 4D + reachIndex % 3 + body * 4D;
depths[index] = 3D + reachIndex % 2 + body * 2D;
roofScales[index] = 1D - body * 0.4D;
}
return new RiverBodyProfile(positions, widths, bankWidths, depths, roofScales);
}
private static RiverNode node(long cellX, long cellZ, double x, double z) {
return new RiverNode(
new RiverNodeId(cellX, cellZ),
x,
z,
64D,
64D,
64D,
64D,
false,
true
);
}
private static long sampleRound(RiverTile tile) {
long signature = 0xCBF29CE484222325L;
for (int sampleIndex = 0; sampleIndex < SAMPLES_PER_ROUND; sampleIndex++) {
double x = Math.floorMod(sampleIndex * 1_229, 2_048) + 0.375D;
double z = Math.floorMod(sampleIndex * 811, 2_048) + 0.625D;
double additionalRadius = 16D + sampleIndex % 17;
RiverSample sample = tile.sampleExpanded(x, z, additionalRadius);
signature = mix(signature, sample.present() ? 1L : 0L);
if (!sample.present()) {
continue;
}
signature = mix(signature, sample.reachId().stableId());
signature = mix(signature, Double.doubleToLongBits(sample.distance()));
signature = mix(signature, Double.doubleToLongBits(sample.alongReach()));
signature = mix(signature, Double.doubleToLongBits(sample.carveWeight()));
signature = mix(signature, Double.doubleToLongBits(sample.width()));
signature = mix(signature, Double.doubleToLongBits(sample.bankWidth()));
signature = mix(signature, Double.doubleToLongBits(sample.depth()));
signature = mix(signature, sample.section().ordinal());
}
return signature;
}
private static long mix(long hash, long value) {
return (hash ^ value) * 0x100000001B3L;
}
private static void requireSignature(long expected, long actual) {
if (actual != expected) {
throw new IllegalStateException(String.format(
Locale.ROOT,
"River tile output changed: expected %016x but got %016x",
expected,
actual
));
}
}
}
@@ -40,6 +40,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
@@ -91,13 +92,23 @@ public final class StubPlatform implements IrisPlatform {
private static final class StubBlockState implements PlatformBlockState {
private static final ConcurrentHashMap<String, StubBlockState> CACHE = new ConcurrentHashMap<>();
private final String key;
private final String blockKey;
private final LinkedHashMap<String, String> properties;
private StubBlockState(String key) {
private StubBlockState(String key, ParsedState parsed) {
this.key = key;
this.blockKey = parsed.blockKey();
this.properties = new LinkedHashMap<>(parsed.properties());
}
static StubBlockState of(String key) {
return CACHE.computeIfAbsent(key, StubBlockState::new);
StubBlockState cached = key == null ? null : CACHE.get(key);
return cached == null ? of(ParsedState.parse(key)) : cached;
}
static StubBlockState of(ParsedState parsed) {
String key = parsed.serialize();
return CACHE.computeIfAbsent(key, ignored -> new StubBlockState(key, parsed));
}
@Override
@@ -147,7 +158,7 @@ public final class StubPlatform implements IrisPlatform {
@Override
public boolean isWaterLogged() {
return false;
return "true".equals(properties.get("waterlogged"));
}
@Override
@@ -227,9 +238,14 @@ public final class StubPlatform implements IrisPlatform {
@Override
public PlatformBlockState withProperty(String name, String value) {
ParsedState parsed = ParsedState.parse(key);
parsed.properties().put(normalizeProperty(name), normalizeProperty(value));
return of(parsed.serialize());
String propertyName = normalizeProperty(name);
String propertyValue = normalizeProperty(value);
if (propertyValue.equals(properties.get(propertyName))) {
return this;
}
ParsedState parsed = parsed();
parsed.properties().put(propertyName, propertyValue);
return of(parsed);
}
@Override
@@ -238,8 +254,11 @@ public final class StubPlatform implements IrisPlatform {
}
private String blockKey() {
int properties = key.indexOf('[');
return properties >= 0 ? key.substring(0, properties) : key;
return blockKey;
}
private ParsedState parsed() {
return new ParsedState(blockKey, new LinkedHashMap<>(properties));
}
}
@@ -248,18 +267,33 @@ public final class StubPlatform implements IrisPlatform {
if (state == null || rotation == null || !rotation.canRotate()) {
return state;
}
ParsedState parsed = ParsedState.parse(state.key());
ParsedState parsed = parsedState(state);
Map<String, String> properties = parsed.properties();
if (properties.containsKey("facing")) {
properties.put("facing", rotateFace(rotation, properties.get("facing"), spinX, spinY, spinZ));
String facing = properties.get("facing");
String rotated = rotateFace(rotation, facing, spinX, spinY, spinZ);
if (facing.equals(rotated)) {
return state;
}
properties.put("facing", rotated);
} else if (properties.containsKey("rotation")) {
properties.put("rotation", rotateSegment(rotation, properties.get("rotation"), spinX, spinY, spinZ));
String segment = properties.get("rotation");
String rotated = rotateSegment(rotation, segment, spinX, spinY, spinZ);
if (segment.equals(rotated)) {
return state;
}
properties.put("rotation", rotated);
} else if (properties.containsKey("axis")) {
properties.put("axis", rotateAxis(rotation, properties.get("axis"), spinX, spinY, spinZ));
} else {
rotateFaceProperties(rotation, properties, spinX, spinY, spinZ);
String axis = properties.get("axis");
String rotated = rotateAxis(rotation, axis, spinX, spinY, spinZ);
if (axis.equals(rotated)) {
return state;
}
properties.put("axis", rotated);
} else if (!rotateFaceProperties(rotation, properties, spinX, spinY, spinZ)) {
return state;
}
return StubBlockState.of(parsed.serialize());
return StubBlockState.of(parsed);
}
private static PlatformBlockState mergeStates(PlatformBlockState base, PlatformBlockState update) {
@@ -269,13 +303,21 @@ public final class StubPlatform implements IrisPlatform {
if (update == null) {
return base;
}
ParsedState parsedBase = ParsedState.parse(base.key());
ParsedState parsedUpdate = ParsedState.parse(update.key());
ParsedState parsedBase = parsedState(base);
ParsedState parsedUpdate = parsedState(update);
if (!parsedBase.blockKey().equals(parsedUpdate.blockKey())) {
return update;
}
if (parsedUpdate.properties().isEmpty()) {
return base;
}
LinkedHashMap<String, String> original = new LinkedHashMap<>(parsedBase.properties());
parsedBase.properties().putAll(parsedUpdate.properties());
return StubBlockState.of(parsedBase.serialize());
return original.equals(parsedBase.properties()) ? base : StubBlockState.of(parsedBase);
}
private static ParsedState parsedState(PlatformBlockState state) {
return state instanceof StubBlockState stub ? stub.parsed() : ParsedState.parse(state.key());
}
private static String rotateFace(IrisObjectRotation rotation, String face,
@@ -327,8 +369,8 @@ public final class StubPlatform implements IrisPlatform {
return Integer.toString(Math.floorMod(rotatedSegment, 16));
}
private static void rotateFaceProperties(IrisObjectRotation rotation, Map<String, String> properties,
int spinX, int spinY, int spinZ) {
private static boolean rotateFaceProperties(IrisObjectRotation rotation, Map<String, String> properties,
int spinX, int spinY, int spinZ) {
List<String> faces = List.of("north", "east", "south", "west", "up", "down");
int present = 0;
for (String face : faces) {
@@ -337,7 +379,7 @@ public final class StubPlatform implements IrisPlatform {
}
}
if (present < 2) {
return;
return false;
}
Map<String, String> rotated = new LinkedHashMap<>();
for (String face : faces) {
@@ -346,13 +388,16 @@ public final class StubPlatform implements IrisPlatform {
rotated.put(rotateFace(rotation, face, spinX, spinY, spinZ), value);
}
}
boolean changed = false;
for (String face : faces) {
if (properties.containsKey(face)) {
String defaultValue = "true".equals(properties.get(face)) || "false".equals(properties.get(face))
? "false" : "none";
properties.put(face, rotated.getOrDefault(face, defaultValue));
String value = rotated.getOrDefault(face, defaultValue);
changed |= !Objects.equals(properties.put(face, value), value);
}
}
return changed;
}
private static IrisBlockVector faceVector(String face) {
@@ -392,14 +437,19 @@ public final class StubPlatform implements IrisPlatform {
return new ParsedState(normalized, new LinkedHashMap<>());
}
LinkedHashMap<String, String> properties = new LinkedHashMap<>();
String body = normalized.substring(open + 1, normalized.length() - 1);
if (!body.isBlank()) {
for (String property : body.split(",")) {
int separator = property.indexOf('=');
if (separator > 0 && separator < property.length() - 1) {
properties.put(property.substring(0, separator), property.substring(separator + 1));
}
int end = normalized.length() - 1;
int start = open + 1;
while (start < end) {
int comma = normalized.indexOf(',', start);
int propertyEnd = comma < 0 || comma > end ? end : comma;
int separator = normalized.indexOf('=', start);
if (separator > start && separator < propertyEnd - 1) {
properties.put(
normalized.substring(start, separator),
normalized.substring(separator + 1, propertyEnd)
);
}
start = propertyEnd + 1;
}
return new ParsedState(normalized.substring(0, open), properties);
}
@@ -0,0 +1,168 @@
package art.arcane.iris.probe;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.Test;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class GenerationOrderProbeTest {
@Test
public void parsesExplicitBoundedOrderInputs() {
GenerationOrderProbe.ProbeConfiguration configuration = GenerationOrderProbe.ProbeConfiguration.parse(
new String[]{
"/tmp/pack",
"underworld",
"1337",
"-2",
"2",
"-1",
"1",
"4",
"991",
"true",
"false"
});
assertEquals(new File("/tmp/pack"), configuration.packSource());
assertEquals("underworld", configuration.dimensionKey());
assertEquals(1337L, configuration.seed());
assertEquals(15, configuration.chunkCount());
assertEquals(4, configuration.parallelism());
assertTrue(configuration.multicore());
assertTrue(!configuration.studio());
}
@Test
public void rejectsImplicitUnboundedOrInvalidOrderInputs() {
assertThrows(IllegalArgumentException.class,
() -> GenerationOrderProbe.ProbeConfiguration.parse(new String[]{"/tmp/pack"}));
assertThrows(IllegalArgumentException.class,
() -> GenerationOrderProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1", "0", "40", "0", "40", "4", "2", "false", "false"
}));
assertThrows(IllegalArgumentException.class,
() -> GenerationOrderProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1", "0", "0", "0", "0", "1", "2", "false", "false"
}));
assertThrows(IllegalArgumentException.class,
() -> GenerationOrderProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1", "0", "0", "0", "0", "2", "2", "maybe", "false"
}));
assertThrows(IllegalArgumentException.class,
() -> new GenerationOrderProbe.ProbeConfiguration(
new File("/tmp/pack"), "overworld", 1L,
134_217_728, 134_217_728, 0, 0, 2, 2L, false, false));
}
@Test
public void createsForwardReverseAndDeterministicShuffleSchedules() {
GenerationOrderProbe.ProbeConfiguration configuration = new GenerationOrderProbe.ProbeConfiguration(
new File("/tmp/pack"), "overworld", 1L, -1, 1, 0, 1, 2, 91L, false, false);
List<GenerationOrderProbe.ChunkCoordinate> forward = GenerationOrderProbe.coordinates(configuration);
List<GenerationOrderProbe.ChunkCoordinate> reverse = GenerationOrderProbe.orderedCoordinates(
forward, GenerationOrderProbe.GenerationOrder.REVERSE, 91L);
List<GenerationOrderProbe.ChunkCoordinate> shuffled = GenerationOrderProbe.orderedCoordinates(
forward, GenerationOrderProbe.GenerationOrder.SHUFFLED, 91L);
List<GenerationOrderProbe.ChunkCoordinate> parallel = GenerationOrderProbe.orderedCoordinates(
forward, GenerationOrderProbe.GenerationOrder.BOUNDED_PARALLEL, 91L);
assertEquals(new GenerationOrderProbe.ChunkCoordinate(-1, 0), forward.getFirst());
assertEquals(new GenerationOrderProbe.ChunkCoordinate(1, 1), reverse.getFirst());
assertEquals(shuffled, parallel);
assertEquals(shuffled, GenerationOrderProbe.orderedCoordinates(
forward, GenerationOrderProbe.GenerationOrder.SHUFFLED, 91L));
}
@Test
public void aggregateSignatureIgnoresMapInsertionOrderButNotChunkContent() {
GenerationOrderProbe.ChunkCoordinate first = new GenerationOrderProbe.ChunkCoordinate(0, 0);
GenerationOrderProbe.ChunkCoordinate second = new GenerationOrderProbe.ChunkCoordinate(1, 0);
GenerationOrderProbe.ChunkHash firstHash = new GenerationOrderProbe.ChunkHash("a", "b", "c");
GenerationOrderProbe.ChunkHash secondHash = new GenerationOrderProbe.ChunkHash("d", "e", "f");
LinkedHashMap<GenerationOrderProbe.ChunkCoordinate, GenerationOrderProbe.ChunkHash> forward =
new LinkedHashMap<>();
forward.put(first, firstHash);
forward.put(second, secondHash);
LinkedHashMap<GenerationOrderProbe.ChunkCoordinate, GenerationOrderProbe.ChunkHash> reverse =
new LinkedHashMap<>();
reverse.put(second, secondHash);
reverse.put(first, firstHash);
assertEquals(
GenerationOrderProbe.aggregateSignature(forward),
GenerationOrderProbe.aggregateSignature(reverse)
);
reverse.put(first, new GenerationOrderProbe.ChunkHash("changed", "b", "changed"));
assertNotEquals(
GenerationOrderProbe.aggregateSignature(forward),
GenerationOrderProbe.aggregateSignature(reverse)
);
}
@Test
public void completeChunkHashSeparatesBlockAndBiomeChanges() {
StubPlatform platform = new StubPlatform(new File("/tmp/iris-order-probe-test"));
PlatformBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
PlatformBlockState water = StubPlatform.blockStateForTest("minecraft:water[level=0]");
PlatformBiome plains = platform.registries().biome("minecraft:plains");
PlatformBiome forest = platform.registries().biome("minecraft:forest");
Hunk<PlatformBlockState> baselineBlocks = Hunk.newArrayHunk(16, 2, 16);
Hunk<PlatformBiome> baselineBiomes = Hunk.newArrayHunk(16, 2, 16);
baselineBlocks.set(0, 0, 0, stone);
baselineBiomes.set(0, 0, 0, plains);
GenerationOrderProbe.ChunkCoordinate coordinate = new GenerationOrderProbe.ChunkCoordinate(0, 0);
GenerationOrderProbe.ChunkHash baseline = GenerationOrderProbe.hashChunk(
coordinate, baselineBlocks, baselineBiomes, 2);
Hunk<PlatformBiome> changedBiomes = Hunk.newArrayHunk(16, 2, 16);
changedBiomes.set(0, 0, 0, forest);
GenerationOrderProbe.ChunkHash biomeChange = GenerationOrderProbe.hashChunk(
coordinate, baselineBlocks, changedBiomes, 2);
assertEquals(baseline.blocks(), biomeChange.blocks());
assertNotEquals(baseline.biomes(), biomeChange.biomes());
assertNotEquals(baseline.combined(), biomeChange.combined());
Hunk<PlatformBlockState> changedBlocks = Hunk.newArrayHunk(16, 2, 16);
changedBlocks.set(0, 0, 0, water);
GenerationOrderProbe.ChunkHash blockChange = GenerationOrderProbe.hashChunk(
coordinate, changedBlocks, baselineBiomes, 2);
assertNotEquals(baseline.blocks(), blockChange.blocks());
assertEquals(baseline.biomes(), blockChange.biomes());
assertNotEquals(baseline.combined(), blockChange.combined());
assertEquals(
Map.of("minecraft:stone", -1, "minecraft:water[level=0]", 1),
GenerationOrderProbe.blockCountDelta(baseline, blockChange)
);
assertEquals(
Map.of("0:minecraft:stone", -1, "0:minecraft:water[level=0]", 1),
GenerationOrderProbe.blockYCountDelta(baseline, blockChange)
);
}
@Test
public void mismatchDetectionIncludesMissingAndChangedChunks() {
GenerationOrderProbe.ChunkCoordinate first = new GenerationOrderProbe.ChunkCoordinate(0, 0);
GenerationOrderProbe.ChunkCoordinate second = new GenerationOrderProbe.ChunkCoordinate(1, 0);
GenerationOrderProbe.ChunkHash firstHash = new GenerationOrderProbe.ChunkHash("a", "b", "c");
GenerationOrderProbe.ChunkHash secondHash = new GenerationOrderProbe.ChunkHash("d", "e", "f");
Map<GenerationOrderProbe.ChunkCoordinate, GenerationOrderProbe.ChunkHash> expected = Map.of(
first, firstHash,
second, secondHash
);
Map<GenerationOrderProbe.ChunkCoordinate, GenerationOrderProbe.ChunkHash> observed = Map.of(
first, new GenerationOrderProbe.ChunkHash("changed", "b", "changed")
);
assertEquals(List.of(first, second), GenerationOrderProbe.mismatchedChunks(expected, observed));
}
}
@@ -0,0 +1,998 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydraulicSegment;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class HydrologyPackProbeTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void parsesExplicitBoundedCoverageInputs() {
HydrologyPackProbe.ProbeConfiguration configuration = HydrologyPackProbe.ProbeConfiguration.parse(
new String[]{
"/tmp/pack",
"overworld",
"1,-19,331",
"-2",
"2",
"-1",
"1",
"SURFACE_POOL@water,WATERFALL@*,DEEP_POOL@deep_lava",
"false"
});
assertEquals(new File("/tmp/pack"), configuration.packSource());
assertEquals("overworld", configuration.dimensionKey());
assertEquals(List.of(1L, -19L, 331L), configuration.seeds());
assertEquals(15, configuration.tilesPerSeed());
assertEquals(3, configuration.requiredCoverage().size());
assertTrue(!configuration.studio());
}
@Test
public void rejectsImplicitUnboundedOrDuplicateInputs() {
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.ProbeConfiguration.parse(new String[]{"/tmp/pack"}));
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1", "0", "0", "0", "0",
"SURFACE_POOL@water", "false"
}));
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1,1", "0", "0", "0", "0",
"SURFACE_POOL@water", "false"
}));
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1,2", "0", "20", "0", "20",
"SURFACE_POOL@water", "false"
}));
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.ProbeConfiguration.parse(new String[]{
"/tmp/pack", "overworld", "1,2", "0", "0", "0", "0",
"SURFACE_POOL@water", "sometimes"
}));
}
@Test
public void selectorsMatchExactOrWildcardProfiles() {
HydrologyPackProbe.CoverageKey water = new HydrologyPackProbe.CoverageKey(
HydrologyFeatureType.WATERFALL, "water");
HydrologyPackProbe.CoverageKey lava = new HydrologyPackProbe.CoverageKey(
HydrologyFeatureType.WATERFALL, "lava");
HydrologyPackProbe.CoverageSelector exact = HydrologyPackProbe.CoverageSelector.parse(
"WATERFALL@water");
HydrologyPackProbe.CoverageSelector wildcard = HydrologyPackProbe.CoverageSelector.parse(
"WATERFALL@*");
assertTrue(exact.matches(water));
assertTrue(!exact.matches(lava));
assertTrue(wildcard.matches(water));
assertTrue(wildcard.matches(lava));
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.CoverageSelector.parse("WATERFALL"));
assertThrows(IllegalArgumentException.class,
() -> HydrologyPackProbe.CoverageSelector.parse("UNKNOWN@water"));
}
@Test
public void wildcardSelectorsRepresentEveryAcceptedFeatureType() {
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
HydrologyPackProbe.CoverageSelector selector = HydrologyPackProbe.CoverageSelector.parse(
type.name() + "@*");
HydrologyPackProbe.CoverageKey observed = new HydrologyPackProbe.CoverageKey(type, "profile");
assertTrue(selector.matches(observed));
}
}
@Test
public void everyFeatureTypeMapsToOneGeneratedAssertionFamily() {
assertEquals(HydrologyPackProbe.VerificationFamily.SURFACE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.SURFACE_POOL));
assertEquals(HydrologyPackProbe.VerificationFamily.SURFACE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.RIFFLE));
assertEquals(HydrologyPackProbe.VerificationFamily.SURFACE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.CASCADE));
assertEquals(HydrologyPackProbe.VerificationFamily.SURFACE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.WATERFALL));
assertEquals(HydrologyPackProbe.VerificationFamily.SURFACE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.MOUTH));
assertEquals(HydrologyPackProbe.VerificationFamily.CAVE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.RIDGE_BORE));
assertEquals(HydrologyPackProbe.VerificationFamily.CAVE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.UNDERGROUND_POOL));
assertEquals(HydrologyPackProbe.VerificationFamily.CAVE,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.UNDERGROUND_DROP));
assertEquals(HydrologyPackProbe.VerificationFamily.GROTTO,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.SINKHOLE));
assertEquals(HydrologyPackProbe.VerificationFamily.GROTTO,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.COASTAL_GROTTO));
assertEquals(HydrologyPackProbe.VerificationFamily.GROTTO,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.INLAND_GROTTO));
assertEquals(HydrologyPackProbe.VerificationFamily.DEEP,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.DEEP_POOL));
assertEquals(HydrologyPackProbe.VerificationFamily.DEEP,
HydrologyPackProbe.verificationFamily(HydrologyFeatureType.DEEP_CHANNEL));
}
@Test
public void generatedChunkMapsNegativeWorldCoordinatesToLocalBuffers() {
StubPlatform platform = new StubPlatform(new File("/tmp/iris-hydrology-pack-probe-test"));
PlatformBlockState water = StubPlatform.blockStateForTest("minecraft:water[level=8]");
PlatformBiome biome = platform.registries().biome("minecraft:river");
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, 2, 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, 2, 16);
blocks.set(15, 1, 15, water);
biomes.set(15, 1, 15, biome);
RealPackProbeSupport.GeneratedChunk chunk = new RealPackProbeSupport.GeneratedChunk(
-1, -2, 2, blocks, biomes);
assertEquals(water, chunk.blockAt(-1, 1, -17));
assertEquals(biome, chunk.biomeAt(-1, 1, -17));
assertThrows(IllegalArgumentException.class, () -> chunk.blockAt(-17, 1, -17));
assertThrows(IllegalArgumentException.class, () -> chunk.blockAt(-1, 2, -17));
}
@Test
public void generatedSurfaceFluidAcceptsOnlyWaterBearingDecorantsForWaterProfiles() {
PlatformBlockState water = StubPlatform.blockStateForTest("minecraft:water[level=0]");
PlatformBlockState lava = StubPlatform.blockStateForTest("minecraft:lava[level=0]");
PlatformBlockState kelp = StubPlatform.blockStateForTest("minecraft:kelp_plant");
PlatformBlockState coral = StubPlatform.blockStateForTest(
"minecraft:brain_coral_fan[waterlogged=true]");
PlatformBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
assertTrue(HydrologyPackProbe.matchesConfiguredFluid(water, water, false));
assertTrue(HydrologyPackProbe.matchesConfiguredFluid(kelp, water, true));
assertTrue(HydrologyPackProbe.matchesConfiguredFluid(coral, water, true));
assertFalse(HydrologyPackProbe.matchesConfiguredFluid(kelp, water, false));
assertFalse(HydrologyPackProbe.matchesConfiguredFluid(kelp, lava, true));
assertFalse(HydrologyPackProbe.matchesConfiguredFluid(stone, water, true));
}
@Test
public void generatedWitnessColumnsAreSortedOnceBySignedCoordinates() {
HydrologyColumnSample eastern = column(1, -2);
HydrologyColumnSample northwest = column(-1, -4);
HydrologyColumnSample southwest = column(-1, 3);
HydrologyTile tile = new HydrologyTile(
new HydrologyTileKey(0, 0),
1L,
2L,
64,
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
List.of(),
new RiverFootprint(Map.of(
RiverFootprint.pack(eastern.x(), eastern.z()), eastern,
RiverFootprint.pack(northwest.x(), northwest.z()), northwest,
RiverFootprint.pack(southwest.x(), southwest.z()), southwest
))
);
assertEquals(
List.of(northwest, southwest, eastern),
HydrologyPackProbe.orderedFootprintColumns(tile)
);
}
@Test
public void generatedWitnessChunksAreDistinctInFirstEncounterOrder() {
assertEquals(
List.of(
new HydrologyPackProbe.ChunkCoordinate(-1, -2),
new HydrologyPackProbe.ChunkCoordinate(0, 0),
new HydrologyPackProbe.ChunkCoordinate(2, -1)
),
HydrologyPackProbe.generatedWitnessChunks(List.of(
new CavePosition(-1, 40, -17),
new CavePosition(-16, 50, -32),
new CavePosition(0, 60, 0),
new CavePosition(47, 70, -1)
))
);
}
@Test
public void generatedSurfaceBiomeUsesThePlatformDerivativeKey() {
IrisBiome biome = new IrisBiome();
biome.setDerivative("minecraft:river");
assertEquals(
"minecraft:river",
HydrologyPackProbe.generatedSurfaceBiomeKey(
biome,
null,
19L,
"overworld",
32,
-48
)
);
}
@Test
public void generatedWitnessDescriptorRoundTripsUnsignedIdsAndNegativeCoordinates() {
HydrologyPackProbe.GeneratedWitnessDescriptor descriptor =
new HydrologyPackProbe.GeneratedWitnessDescriptor(
HydrologyPackProbe.CoverageSelector.parse("UNDERGROUND_DROP@water"),
new HydrologyTileKey(-3, 7),
"water",
-5L,
new CavePosition(-33, 41, -1),
HydrologyCaveAction.FALLING_FLUID
);
assertEquals(
descriptor,
HydrologyPackProbe.GeneratedWitnessDescriptor.parse(descriptor.encode())
);
}
@Test
public void generatedProcessResultRoundTripsStateAndBiomeKeys() {
HydrologyPackProbe.GeneratedProcessResult result = new HydrologyPackProbe.GeneratedProcessResult(
HydrologyPackProbe.CoverageSelector.parse("SURFACE_POOL@water"),
new HydrologyPackProbe.GeneratedVerification(
"minecraft:water[level=0]",
"minecraft:river"
)
);
assertEquals(result, HydrologyPackProbe.GeneratedProcessResult.parse(result.machineLine()));
}
@Test
public void versionTwoMachineLineReportsGeneratedVerificationCounts() {
HydrologyPackProbe.ProbeResult result = new HydrologyPackProbe.ProbeResult(
"PASS", "overworld", 2, 4, 8, 0, 1, 0, 1, 13, 13, 3, 7, 13, "abc");
String line = result.machineLine();
assertTrue(line.contains("version=2"));
assertTrue(line.contains("generated_chunks=7"));
assertTrue(line.contains("verified_features=13"));
}
@Test
public void reportsEveryMissingExactCoverageSelector() {
Set<HydrologyPackProbe.CoverageKey> observed = Set.of(
new HydrologyPackProbe.CoverageKey(HydrologyFeatureType.SURFACE_POOL, "water"),
new HydrologyPackProbe.CoverageKey(HydrologyFeatureType.DEEP_POOL, "deep_lava")
);
List<HydrologyPackProbe.CoverageSelector> required = List.of(
HydrologyPackProbe.CoverageSelector.parse("SURFACE_POOL@water"),
HydrologyPackProbe.CoverageSelector.parse("WATERFALL@water"),
HydrologyPackProbe.CoverageSelector.parse("DEEP_CHANNEL@deep_lava")
);
assertEquals(
List.of(required.get(1), required.get(2)),
HydrologyPackProbe.missingCoverage(observed, required)
);
}
@Test
public void configuredFamiliesRequireEachEnabledProfileFamily() {
Set<HydrologyPackProbe.ConfiguredCoverage> observed = Set.of(
new HydrologyPackProbe.ConfiguredCoverage(
HydrologyPackProbe.CoverageFamily.SURFACE, "water"),
new HydrologyPackProbe.ConfiguredCoverage(
HydrologyPackProbe.CoverageFamily.UNDERGROUND, "water")
);
LinkedHashSet<HydrologyPackProbe.ConfiguredCoverage> required = new LinkedHashSet<>(List.of(
new HydrologyPackProbe.ConfiguredCoverage(
HydrologyPackProbe.CoverageFamily.SURFACE, "water"),
new HydrologyPackProbe.ConfiguredCoverage(
HydrologyPackProbe.CoverageFamily.UNDERGROUND, "water"),
new HydrologyPackProbe.ConfiguredCoverage(
HydrologyPackProbe.CoverageFamily.DEEP, "deep_lava")
));
assertEquals(
List.of(new HydrologyPackProbe.ConfiguredCoverage(
HydrologyPackProbe.CoverageFamily.DEEP, "deep_lava")),
HydrologyPackProbe.missingConfiguredCoverage(observed, required)
);
}
@Test
public void surfaceOutletRequiresExactlyOneCompleteCourse() {
assertTrue(HydrologyPackProbe.ShapeMetrics.validSurfaceOutletCourseGroup(1, 0));
assertFalse(HydrologyPackProbe.ShapeMetrics.validSurfaceOutletCourseGroup(0, 0));
assertFalse(HydrologyPackProbe.ShapeMetrics.validSurfaceOutletCourseGroup(0, 1));
assertFalse(HydrologyPackProbe.ShapeMetrics.validSurfaceOutletCourseGroup(1, 1));
assertFalse(HydrologyPackProbe.ShapeMetrics.validSurfaceOutletCourseGroup(2, 0));
}
@Test
public void nearDiagonalStickCountsAsGridLocked() {
List<HydrologyPoint> centerline = List.of(
point(0, 0),
point(96, 4),
point(192, 8)
);
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
1L,
centerline,
thickChannel(centerline, 4, 32),
false
));
assertTrue(result.gridLockedFraction() > 0.9D);
assertTrue(result.longestGridLockedRun() > 64D);
assertTrue(result.violations().contains("GRID_LOCKED_FRACTION"));
assertTrue(result.violations().contains("GRID_LOCKED_RUN"));
}
@Test
public void isolatedRightAngleDoglegFailsTurnGates() {
List<HydrologyPoint> centerline = List.of(
point(0, 0),
point(96, 0),
point(96, 96),
point(192, 96)
);
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
2L,
centerline,
thickChannel(centerline, 4, 32),
false
));
assertTrue(result.isolatedTurns() > 0);
assertTrue(result.p95TurnDegrees() > 25D);
assertTrue(result.violations().contains("ISOLATED_TURN"));
assertTrue(result.violations().contains("P95_TURN"));
}
@Test
public void gradualSourceWidthRampPassesEndpointGate() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(80, 24));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
3L,
centerline,
thickChannel(centerline, 4, 32),
false
));
assertEquals(0, result.hardEndpointRamps());
assertEquals(0, result.unexpectedLeaves());
assertFalse(result.violations().contains("HARD_ENDPOINT_RAMP"));
}
@Test
public void widenedSourceBasinFailsEndpointGate() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(80, 24));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
7L,
centerline,
widenedSourceBasin(centerline, 4, 7, 32),
false
));
assertTrue(result.violations().contains("WIDENED_SOURCE_BASIN"));
}
@Test
public void fullWidthSourceCapFailsEndpointRampGate() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(80, 24));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
4L,
centerline,
thickChannel(centerline, 4, 0),
false
));
assertTrue(result.hardEndpointRamps() > 0);
assertTrue(result.violations().contains("HARD_ENDPOINT_RAMP"));
}
@Test
public void fullWidthExposedTerminalFailsEndpointRampGate() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(80, 24));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
5L,
centerline,
thickChannel(centerline, 4, 32),
true
));
assertEquals(1, result.hardEndpointRamps());
assertTrue(result.violations().contains("HARD_ENDPOINT_RAMP"));
}
@Test
public void clippedCourseDoesNotInventEndpointRampFailures() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(160, 48));
Set<Long> observedCells = thickChannel(
List.of(point(40, 12), point(120, 36)),
4,
0
);
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
8L,
centerline,
observedCells,
true
));
assertEquals(0, result.hardEndpointRamps());
assertFalse(result.violations().contains("HARD_ENDPOINT_RAMP"));
}
@Test
public void roundedExposedTerminalCapPassesCapGate() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(160, 0));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
9L,
centerline,
terminalBasinChannel(centerline, 4, 7, 32, false),
true
));
assertEquals(0, result.clippedTerminalCaps());
assertTrue(result.terminalCapLength() >= 3D);
assertFalse(result.violations().contains("CLIPPED_TERMINAL_CAP"));
}
@Test
public void squareExposedTerminalCapFailsCapGateAfterGradualBasinRamp() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(160, 0));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
10L,
centerline,
terminalBasinChannel(centerline, 4, 7, 32, true),
true
));
assertEquals(0, result.hardEndpointRamps());
assertEquals(1, result.clippedTerminalCaps());
assertEquals(0D, result.terminalCapLength(), 0D);
assertTrue(result.violations().contains("CLIPPED_TERMINAL_CAP"));
}
@Test
public void configuredMinimumWidthRejectsNarrowOwnedInterior() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(192, 0));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
11L,
centerline,
thickChannel(centerline, 1, 32),
false
));
assertEquals(3, result.minimumInteriorWidth());
assertTrue(result.violations().contains("NARROW_INTERIOR_WIDTH"));
}
@Test
public void constantOwnedInteriorPassesWidthGates() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(192, 0));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
12L,
centerline,
thickChannel(centerline, 2, 32),
false
));
assertTrue(result.minimumInteriorWidth() >= 4);
assertEquals(0D, result.maximumWidthTroughDepthRatio(), 0D);
assertFalse(result.violations().contains("NARROW_INTERIOR_WIDTH"));
assertFalse(result.violations().contains("CONCAVE_WIDTH_TROUGH"));
}
@Test
public void borePortalRunMarginsAreExcludedFromInteriorWidth() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(320, 0));
HashSet<Long> ownedCells = new HashSet<>(thickChannel(
List.of(point(0, 0), point(128, 0)),
2,
32
));
ownedCells.addAll(thickChannel(
List.of(point(192, 0), point(320, 0)),
2,
32
));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
15L,
centerline,
ownedCells,
false
));
assertTrue(result.minimumInteriorWidth() >= 4);
assertFalse(result.violations().contains("NARROW_INTERIOR_WIDTH"));
}
@Test
public void sustainedMidCourseWidthTroughFailsConcavityGate() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(256, 0));
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(courseInput(
1337L,
13L,
centerline,
troughChannel(centerline, 5, 2, 96, 160),
false
));
assertTrue(result.maximumWidthTroughDepthRatio() >= 0.35D);
assertTrue(result.violations().contains("CONCAVE_WIDTH_TROUGH"));
}
@Test
public void incompleteCourseSkipsCompleteCourseMorphologyGates() {
List<HydrologyPoint> centerline = List.of(point(0, 0), point(160, 0));
HydrologyPackProbe.CourseMorphologyInput input = new HydrologyPackProbe.CourseMorphologyInput(
1337L,
14L,
9,
16,
384,
0D,
false,
true,
centerline,
terminalBasinChannel(centerline, 1, 2, 32, true)
);
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(input);
assertEquals(0, result.clippedTerminalCaps());
assertEquals(0, result.minimumInteriorWidth());
assertEquals(0D, result.maximumWidthTroughDepthRatio(), 0D);
assertFalse(result.violations().contains("CLIPPED_TERMINAL_CAP"));
assertFalse(result.violations().contains("NARROW_INTERIOR_WIDTH"));
assertFalse(result.violations().contains("CONCAVE_WIDTH_TROUGH"));
}
@Test
public void midCourseSpurCreatesUnexpectedLeaf() {
List<HydrologyPoint> centerline = List.of(point(-80, 0), point(80, 0));
HashSet<Long> ownedCells = new HashSet<>(thickChannel(centerline, 2, 16));
ownedCells.addAll(thickChannel(List.of(point(0, 0), point(0, 96)), 2, 0));
HydrologyPackProbe.SeedMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeSeed(
1337L,
List.of(courseInput(1337L, 6L, centerline, ownedCells, false))
);
assertTrue(result.unexpectedLeaves() > 0);
assertTrue(result.violations().stream().anyMatch(
(String violation) -> violation.contains("UNEXPECTED_BRANCH_LEAVES")));
assertTrue(result.courses().getFirst().unexpectedLeaves() > 0);
}
@Test
public void oceanIntegrityHelpersDetectOwnershipAndFloorMutation() {
HydrologyColumnLayer apron = surfaceLayer(true, false);
HydrologyColumnLayer inertLayer = surfaceLayer(false, false);
HydrologyColumnLayer ownedTerrain = surfaceLayer(false, true);
assertFalse(HydrologyPackProbe.ShapeMetrics.ownsOceanTerrain(apron));
assertFalse(HydrologyPackProbe.ShapeMetrics.ownsOceanTerrain(inertLayer));
assertTrue(HydrologyPackProbe.ShapeMetrics.ownsOceanTerrain(ownedTerrain));
assertFalse(HydrologyPackProbe.ShapeMetrics.mutatesOceanTerrain(54, 54));
assertTrue(HydrologyPackProbe.ShapeMetrics.mutatesOceanTerrain(54, 48));
}
@Test
public void surfaceBankIntegrityRequiresFreeboardAroundOwnedFluid() {
assertEquals(
0,
HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
surfaceBankFootprint(71),
Set.of(32L)
)
);
assertEquals(
1,
HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
surfaceBankFootprint(70),
Set.of(32L)
)
);
HashMap<Long, HydrologyColumnSample> missingBank = new HashMap<>(surfaceBankFootprint(71).columns());
missingBank.remove(RiverFootprint.pack(1, 0));
assertEquals(
1,
HydrologyPackProbe.ShapeMetrics.uncontainedSurfaceBankEdges(
new RiverFootprint(missingBank),
Set.of(32L)
)
);
}
@Test
public void seedsWithoutSurfaceCoursesDoNotMaskPublishedFailures() {
HydrologyPackProbe.CourseMorphologyResult acceptedCourse =
new HydrologyPackProbe.CourseMorphologyResult(
642L, 5L, 20, 80D, 80D, 0, 0D, 0D, 0, 0D, 0,
0, 0, 0D, 0, 0, 0D, 0, 0D, 0, 0D, 0, List.of());
HydrologyPackProbe.SeedMorphologyResult accepted =
new HydrologyPackProbe.SeedMorphologyResult(642L, List.of(acceptedCourse), 0, List.of());
HydrologyPackProbe.SeedMorphologyResult empty =
new HydrologyPackProbe.SeedMorphologyResult(1337L, List.of(), 0, List.of());
IllegalStateException perSeedFailure = assertThrows(
IllegalStateException.class,
() -> HydrologyPackProbe.PublishedMorphologyMetrics.requireValidResults(List.of(accepted, empty))
);
assertTrue(perSeedFailure.getMessage().contains("seed=1337"));
IllegalStateException emptyFailure = assertThrows(
IllegalStateException.class,
() -> HydrologyPackProbe.PublishedMorphologyMetrics.requireValidResults(List.of(empty))
);
assertTrue(emptyFailure.getMessage().contains("NO_SURFACE_COURSES"));
HydrologyPackProbe.CourseMorphologyResult rejectedCourse =
new HydrologyPackProbe.CourseMorphologyResult(
52L, 5L, 20, 80D, 80D, 0, 0D, 0D, 0, 0D, 0,
0, 0, 0D, 0, 0, 0D, 0, 0D, 2, 0D, 0,
List.of("NARROW_INTERIOR_WIDTH"));
HydrologyPackProbe.SeedMorphologyResult rejected =
new HydrologyPackProbe.SeedMorphologyResult(
52L, List.of(rejectedCourse), 0, List.of("5:NARROW_INTERIOR_WIDTH"));
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> HydrologyPackProbe.PublishedMorphologyMetrics.requireValidResults(
List.of(accepted, empty, rejected))
);
assertTrue(failure.getMessage().contains("seed=52"));
}
@Test
public void completeSurfaceCourseRequiresTheConfiguredExposedLength() {
List<HydrologyPoint> centerline = List.of(
new HydrologyPoint(0, 72, 0),
new HydrologyPoint(512, 72, 0)
);
HydrologyPackProbe.CourseMorphologyInput input = new HydrologyPackProbe.CourseMorphologyInput(
642L,
5L,
9,
4,
384,
40D,
true,
false,
centerline,
thickChannel(centerline, 4, 24)
);
HydrologyPackProbe.CourseMorphologyResult result =
HydrologyPackProbe.PublishedMorphologyMetrics.analyzeCourse(input);
assertTrue(result.violations().contains("INSUFFICIENT_EXPOSED_SURFACE"));
}
@Test
public void writesTopDownPngAndJsonMetrics() throws Exception {
File reportDirectory = temporaryFolder.newFolder("hydrology-reports");
HydrologyPackProbe.CourseMorphologyResult course =
new HydrologyPackProbe.CourseMorphologyResult(
1337L, 6L, 3, 2D, 2D, 0, 0D, 0D, 0, 0D, 0,
0, 0, 0D, 0, 0, 0D, 0, 0D, 0, 0D, 0, List.of());
HydrologyPackProbe.SeedMorphologyResult result =
new HydrologyPackProbe.SeedMorphologyResult(1337L, List.of(course), 0, List.of());
Map<Long, Long> courseByCell = Map.of(
RiverFootprint.pack(-1, 2), 6L,
RiverFootprint.pack(0, 2), 6L,
RiverFootprint.pack(1, 3), 6L
);
HydrologyPackProbe.PublishedMorphologyMetrics.writeReport(
reportDirectory,
result,
courseByCell
);
File imageFile = new File(reportDirectory, "seed-1337-surface-footprint.png");
File metricsFile = new File(reportDirectory, "seed-1337-surface-morphology.json");
BufferedImage image = ImageIO.read(imageFile);
String metrics = Files.readString(metricsFile.toPath(), StandardCharsets.UTF_8);
assertTrue(imageFile.isFile());
assertTrue(metricsFile.isFile());
assertEquals(3, image.getWidth());
assertEquals(2, image.getHeight());
assertTrue(metrics.contains("\"seed\": 1337"));
assertTrue(metrics.contains("\"clippedTerminalCaps\": 0"));
assertTrue(metrics.contains("\"minimumInteriorWidth\": 0"));
assertTrue(metrics.contains("\"maximumWidthTroughDepthRatio\": 0.000000"));
assertTrue(metrics.contains("\"unexpectedLeaves\": 0"));
}
private static HydrologyPackProbe.CourseMorphologyInput courseInput(
long seed,
long courseId,
List<HydrologyPoint> centerline,
Set<Long> ownedCells,
boolean terminalExposed
) {
return new HydrologyPackProbe.CourseMorphologyInput(
seed,
courseId,
9,
4,
0,
0D,
true,
terminalExposed,
centerline,
ownedCells
);
}
private static Set<Long> thickChannel(List<HydrologyPoint> centerline, int radius, int sourceRamp) {
List<HydrologyPoint> raster = raster(centerline);
HashSet<Long> cells = new HashSet<>();
for (int index = 0; index < raster.size(); index++) {
int effectiveRadius = sourceRamp == 0
? radius
: Math.min(radius, (int) StrictMath.round(radius * Math.min(1D, index / (double) sourceRamp)));
addDisc(cells, raster.get(index), effectiveRadius);
}
return Set.copyOf(cells);
}
private static Set<Long> widenedSourceBasin(
List<HydrologyPoint> centerline,
int channelRadius,
int basinRadius,
int taperLength
) {
List<HydrologyPoint> raster = raster(centerline);
HashSet<Long> cells = new HashSet<>();
for (int index = 0; index < raster.size(); index++) {
double taper = Math.min(1D, index / (double) taperLength);
int radius = channelRadius + (int) StrictMath.round((basinRadius - channelRadius) * (1D - taper));
addDisc(cells, raster.get(index), radius);
}
return Set.copyOf(cells);
}
private static Set<Long> terminalBasinChannel(
List<HydrologyPoint> centerline,
int channelRadius,
int basinRadius,
int profileLength,
boolean clipped
) {
List<HydrologyPoint> raster = raster(centerline);
HashSet<Long> cells = new HashSet<>();
for (int index = 0; index < raster.size(); index++) {
int sourceRadius = Math.min(
channelRadius,
(int) StrictMath.round(channelRadius * Math.min(1D, index / (double) profileLength))
);
int remaining = raster.size() - 1 - index;
double terminalWeight = 1D - Math.min(1D, remaining / (double) profileLength);
int radius = sourceRadius
+ (int) StrictMath.round((basinRadius - sourceRadius) * terminalWeight);
addDisc(cells, raster.get(index), radius);
}
if (!clipped) {
return Set.copyOf(cells);
}
HydrologyPoint terminal = raster.getLast();
cells.removeIf((Long packed) -> RiverFootprint.unpackX(packed) > terminal.x());
return Set.copyOf(cells);
}
private static Set<Long> troughChannel(
List<HydrologyPoint> centerline,
int outerRadius,
int innerRadius,
int troughStart,
int troughEnd
) {
List<HydrologyPoint> raster = raster(centerline);
HashSet<Long> cells = new HashSet<>();
for (int index = 0; index < raster.size(); index++) {
int sourceRadius = Math.min(
outerRadius,
(int) StrictMath.round(outerRadius * Math.min(1D, index / 32D))
);
int radius = index >= troughStart && index <= troughEnd ? innerRadius : outerRadius;
addDisc(cells, raster.get(index), Math.min(sourceRadius, radius));
}
return Set.copyOf(cells);
}
private static HydrologyColumnLayer surfaceLayer(boolean oceanApron, boolean terrainOwned) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
31L,
HydrologyFeatureType.MOUTH,
32L,
33L,
0,
70,
0,
1,
0,
false
);
return new HydrologyColumnLayer(
feature,
54,
63,
63,
true,
false,
false,
true,
false,
false,
terrainOwned,
false,
oceanApron,
"water",
"surface",
"mouth",
"shore",
"dry",
"flooded"
);
}
private static RiverFootprint surfaceBankFootprint(int easternBankHeight) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
31L,
HydrologyFeatureType.RIFFLE,
32L,
33L,
0,
63,
0,
1,
0,
false
);
HydrologyColumnLayer wetLayer = new HydrologyColumnLayer(
feature,
67,
70,
70,
true,
false,
false,
true,
false,
false,
true,
true,
false,
"water",
"surface",
"mouth",
"shore",
"dry",
"flooded"
);
HydrologyColumnSample center = new HydrologyColumnSample(
0, 0, 80, 63, false, "parent", List.of(wetLayer));
HydrologyColumnSample west = bankColumn(-1, 0, 71);
HydrologyColumnSample east = bankColumn(1, 0, easternBankHeight);
HydrologyColumnSample north = bankColumn(0, -1, 71);
HydrologyColumnSample south = bankColumn(0, 1, 71);
return new RiverFootprint(Map.of(
RiverFootprint.pack(center.x(), center.z()), center,
RiverFootprint.pack(west.x(), west.z()), west,
RiverFootprint.pack(east.x(), east.z()), east,
RiverFootprint.pack(north.x(), north.z()), north,
RiverFootprint.pack(south.x(), south.z()), south
));
}
private static HydrologyColumnSample bankColumn(int x, int z, int naturalHeight) {
return new HydrologyColumnSample(x, z, naturalHeight, 63, false, "parent", List.of());
}
private static List<HydrologyPoint> raster(List<HydrologyPoint> points) {
ArrayList<HydrologyPoint> raster = new ArrayList<>();
for (int pair = 0; pair < points.size() - 1; pair++) {
HydrologyPoint start = points.get(pair);
HydrologyPoint end = points.get(pair + 1);
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
for (int step = raster.isEmpty() ? 0 : 1; step <= steps; step++) {
double progress = step / (double) steps;
HydrologyPoint point = new HydrologyPoint(
(int) StrictMath.round(start.x() + (end.x() - start.x()) * progress),
64,
(int) StrictMath.round(start.z() + (end.z() - start.z()) * progress)
);
if (raster.isEmpty()
|| point.x() != raster.getLast().x()
|| point.z() != raster.getLast().z()) {
raster.add(point);
}
}
}
return List.copyOf(raster);
}
private static void addDisc(Set<Long> cells, HydrologyPoint center, int radius) {
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
for (int deltaX = -radius; deltaX <= radius; deltaX++) {
if (deltaX * deltaX + deltaZ * deltaZ <= radius * radius) {
cells.add(RiverFootprint.pack(center.x() + deltaX, center.z() + deltaZ));
}
}
}
}
private static HydrologyPoint point(int x, int z) {
return new HydrologyPoint(x, 64, z);
}
private static HydrologyColumnSample column(int x, int z) {
return new HydrologyColumnSample(x, z, 80, 63, false, "parent", List.of());
}
}
@@ -0,0 +1,23 @@
package art.arcane.iris.probe;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import org.junit.Test;
import java.util.EnumSet;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public final class HydrologyPlannerProbeTest {
@Test
public void deterministicAcceptedCoveragePublishesEveryFeatureType() {
Map<HydrologyFeatureType, HydrologyFeatureRef> coverage =
HydrologyPlannerProbe.deterministicAcceptedCoverage();
assertEquals(EnumSet.allOf(HydrologyFeatureType.class), coverage.keySet());
for (Map.Entry<HydrologyFeatureType, HydrologyFeatureRef> entry : coverage.entrySet()) {
assertEquals(entry.getKey(), entry.getValue().type());
}
}
}
@@ -8,6 +8,7 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public final class StubPlatformStateTest {
@@ -43,6 +44,19 @@ public final class StubPlatformStateTest {
assertEquals("minecraft:oak_leaves[distance=7,persistent=true]", merged.key());
}
@Test
public void noOpStateTransformsReuseTheCanonicalInstance() {
PlatformBlockState stone = state("minecraft:stone");
PlatformBlockState leaves = state("minecraft:oak_leaves[distance=7,persistent=false]");
assertSame(stone, StubPlatform.rotateForTest(IrisObjectRotation.of(0, 90, 0), stone));
assertSame(leaves, leaves.withProperty("distance", "7"));
assertSame(leaves, StubPlatform.mergeForTest(
leaves,
state("minecraft:oak_leaves[persistent=false]")
));
}
@Test
public void classifiesVanillaFluidStates() {
PlatformBlockState water = state("minecraft:water[level=0]");
@@ -59,6 +73,13 @@ public final class StubPlatformStateTest {
assertTrue(stone.isSolid());
}
@Test
public void classifiesCanonicalWaterloggedProperty() {
assertTrue(state("minecraft:brain_coral_fan[waterlogged=true]").isWaterLogged());
assertFalse(state("minecraft:brain_coral_fan[waterlogged=false]").isWaterLogged());
assertFalse(state("minecraft:brain_coral_fan").isWaterLogged());
}
private PlatformBlockState state(String key) {
return IrisPlatforms.get().registries().block(key);
}