mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user