Basin hydrology

- The route-and-reject hydrology system and its probes are removed
- Hydrology is configured under dimension hydrology and per biome, region and dimension hydrologyPolicy
- riverPolicy and the old hydrology config types are gone
- Grid kernels cover priority flood, D8 routing, flow accumulation, basin extraction, breaching and stream burning
- A basin spills at its pour point and an interior bump can never be the spill
- Region plans cover 4096-block regions on a 64-block grid for major rivers and lakes
- Window plans cover 512-block windows on an 8-block grid for reaches and ponds
- Plans are cached in memory, persisted per world under iris/hydrology and keyed by pack, seed, world height and settings
- Only the main runtime of an engine persists plans and stores swap on hotload and default runtime change
- Demand rasters outrank prefetch and stopping pregen withdraws its queued prefetch
- Ponds come from natural basins, seeded cups and chained cups with strictly falling levels
- A pond level never exceeds its rim plus maxFill or the end grade of any inflow
- Pond outlet loops are opened at their lowest pond
- Reaches split at junctions, pond outlets and 256-block grid lines
- Reaches meander, fit the valley, grade to a running minimum and smooth with Chaikin passes
- Drops of two blocks or more get a lip and one-block corner steps become dry rim ledges
- Cascades and waterfalls write falling fluid from the mantle at level eight
- Inflows into region lakes end at the lake level
- River mouths and water beside the sea sit at the sea surface
- The per-chunk raster gives terrain height, water head, role, tier, fluid, cave ceiling and shore distance
- WaterRole codes are stable and exposed to packs through the WATER_ROLE engine stream
- WATER_HEAD replaces the old river engine streams and an unknown stream name fails the pack load
- Banks take SMOOTH, LINEAR, CONCAVE, TERRACED or CLIFF profiles
- Containment berms grow as tall as the water needs
- Columns within two blocks of water stay solid and keep no Terrain3D overhang
- Seam dams hold water where new chunks meet chunks generated earlier
- The transition blend skips hydrology-owned columns
- Caves stay at least two blocks below river and pond beds
- The painter is the only writer of still water
- Objects cannot open water, falls or bank ground and solid objects may stand in water
- On-water objects need fluid at their anchor column
- Floating child islands stay out of channels
- Sea inlets run inland from cliff coasts at sea level and end in a chamber pool
- Inlet passages are sealed against carvers, subterrain features and the post pass
- Cave pools settle into carved lows, keep one fluid and stay out of inlet space
- Pool and inlet cells use their own cave actions in mantle data
- Water vegetation lists sit on tiers, ponds and hydrologyPolicy with SEA_FLOOR, SEA_SURFACE and SHORE_LINE parts
- Pack validation compiles hydrology settings and rejects empty or water-holding palettes
- Chunk generation fails closed when its 3x3 water rasters are not planned
- /iris find covers stream, river, major river, cascade, waterfall, mouth, pond, lake, dam, sea inlet and cave pool
- Generation history records the new feature types and drops old hydrology entries
- The studio map draws water from rasters up close and from plans when zoomed out
- Pregen prepares the current region and one region ahead
- hydrologyProbe, riverTransectProbe, hydrologyBlockProbe, seaInletProbe and cavePoolProbe report containment, rises, seams, tick conversion and missing surface
- Test forks run with a 2 GB heap, a 60-minute timeout and the Mockito agent loaded explicitly
- Generation with hydrology off is unchanged
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-25 15:28:26 -04:00
1 parent 2d211cf106
commit db0f98d103
710 files changed
+41791 -91883

No files matched your search

+105 -76
View File
@@ -17,7 +17,7 @@ dependencies {
implementation(project(':core'))
compileOnly(libs.spigot)
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.mockito.core)
testImplementation(libs.gson)
testImplementation(testFixtures(project(':core')))
testImplementation('com.github.VolmitSoftware.VolmLib:volmlib-shared:master-SNAPSHOT') {
@@ -55,15 +55,6 @@ 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')
@@ -74,11 +65,6 @@ Provider<String> probeMulticore = providers.gradleProperty('probeMulticore').orE
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')
@@ -88,17 +74,71 @@ Provider<String> probeMaximumChunkZ = providers.gradleProperty('probeMaximumChun
Provider<String> probeParallelism = providers.gradleProperty('probeParallelism')
Provider<String> probeShuffleSeed = providers.gradleProperty('probeShuffleSeed')
Provider<String> probeJfr = providers.gradleProperty('probeJfr')
Provider<String> probeTileX = providers.gradleProperty('probeTileX')
Provider<String> probeTileZ = providers.gradleProperty('probeTileZ')
Provider<String> probeOutput = providers.gradleProperty('probeOutput')
Provider<String> probeStrictGeometry = providers.gradleProperty('probeStrictGeometry').orElse('false')
Provider<String> probeWindows = providers.gradleProperty('probeWindows')
tasks.register('hydrologyProbe', JavaExec) {
group = 'verification'
description = 'Plans hydrology over a window range of a real pack and reports cold first-chunk latency, plan cost, yield, containment, rising water, seams and fluid-tick checks per window.'
mainClass = 'art.arcane.iris.probe.HydrologyProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector')
minHeapSize = '2g'
maxHeapSize = '6g'
dependsOn(':core:compileJava')
doFirst {
Map<String, Provider<String>> required = [
probePack: probePack,
probeSeed: probeSeed,
probeWindows: probeWindows
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("hydrologyProbe requires -P${name}=<value>")
}
}
args(probePack.get(), probeDimension.getOrElse('-'), probeSeed.get(), probeWindows.get(),
providers.gradleProperty('probeThreads').getOrElse('8'), probeStudio.get())
}
}
tasks.register('riverTransectProbe', JavaExec) {
group = 'verification'
description = 'Plans hydrology over a window range of a real pack and renders each window as a plan, river cross-sections and pond outlines.'
mainClass = 'art.arcane.iris.probe.RiverTransectProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
minHeapSize = '2g'
maxHeapSize = '6g'
dependsOn(':core:compileJava')
doFirst {
Map<String, Provider<String>> required = [
probePack: probePack,
probeSeed: probeSeed,
probeWindows: probeWindows,
probeOutput: probeOutput
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("riverTransectProbe requires -P${name}=<value>")
}
}
java.nio.file.Path output = new File(probeOutput.get()).canonicalFile.toPath()
if (output.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
throw new GradleException('probeOutput must point outside the repository')
}
args(probePack.get(), probeDimension.getOrElse('-'), probeSeed.get(), probeWindows.get(), output.toString(), probeStudio.get())
}
}
tasks.register('hydrologyBlockProbe', JavaExec) {
group = 'verification'
description = 'Generates up to 64 real pack chunks and checks final cane support, channel fluid and mouth ocean connectivity, with block category images.'
description = 'Generates up to 64 real pack chunks and checks the planned water surface, containment, fluid-tick stability and cane support, with block images.'
mainClass = 'art.arcane.iris.probe.HydrologyBlockProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
maxHeapSize = '4g'
dependsOn(':core:compileJava')
doFirst {
Map<String, Provider<String>> required = [
@@ -109,7 +149,6 @@ tasks.register('hydrologyBlockProbe', JavaExec) {
probeMaximumChunkX: probeMaximumChunkX,
probeMinimumChunkZ: probeMinimumChunkZ,
probeMaximumChunkZ: probeMaximumChunkZ,
probeRequiredCoverage: probeRequiredCoverage,
probeOutput: probeOutput
]
required.each { String name, Provider<String> value ->
@@ -117,9 +156,12 @@ tasks.register('hydrologyBlockProbe', JavaExec) {
throw new GradleException("hydrologyBlockProbe requires -P${name}=<value>")
}
}
args(probePack.get(), probeDimension.get(), probeSeed.get(), probeMinimumChunkX.get(),
probeMaximumChunkX.get(), probeMinimumChunkZ.get(), probeMaximumChunkZ.get(),
probeRequiredCoverage.get(), probeOutput.get(), probeStudio.get())
java.nio.file.Path output = new File(probeOutput.get()).canonicalFile.toPath()
if (output.startsWith(rootProject.projectDir.canonicalFile.toPath())) {
throw new GradleException('probeOutput must point outside the repository')
}
args(probePack.get(), probeDimension.get(), probeSeed.get(), probeMinimumChunkX.get(), probeMaximumChunkX.get(),
probeMinimumChunkZ.get(), probeMaximumChunkZ.get(), probeRequiredCoverage.getOrElse('-'), output.toString(), probeStudio.get())
}
}
@@ -237,43 +279,6 @@ tasks.register('genDiff', 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.'
@@ -315,35 +320,58 @@ tasks.register('generationOrderProbe', JavaExec) {
}
}
tasks.register('riverTransectProbe', JavaExec) {
tasks.register('cavePoolProbe', JavaExec) {
group = 'verification'
description = 'Plans one hydrology tile over a real pack and renders every surface river as a plan, cross-sections and a summary with ocean-write and containment gates.'
mainClass = 'art.arcane.iris.probe.RiverTransectProbe'
description = 'Generates real pack chunks with cave pools and reports containment, pool sizes and generation-order hashes.'
mainClass = 'art.arcane.iris.probe.CavePoolProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
jvmArgs('--add-modules', 'jdk.incubator.vector')
minHeapSize = '2g'
maxHeapSize = '8g'
dependsOn(':core:compileJava')
doFirst {
Provider<String> threads = providers.gradleProperty('probeThreads').orElse('8')
Provider<String> work = providers.gradleProperty('probeWork').orElse(System.getProperty('java.io.tmpdir'))
Map<String, Provider<String>> required = [
probePack: probePack,
probeDimension: probeDimension,
probeSeed: probeSeed,
probeTileX: probeTileX,
probeTileZ: probeTileZ,
probeOutput: probeOutput
probeMinimumChunkX: probeMinimumChunkX,
probeMaximumChunkX: probeMaximumChunkX,
probeMinimumChunkZ: probeMinimumChunkZ,
probeMaximumChunkZ: probeMaximumChunkZ
]
required.each { String name, Provider<String> value ->
if (!value.isPresent() || value.get().isBlank()) {
throw new GradleException("riverTransectProbe requires -P${name}=<value>")
throw new GradleException("cavePoolProbe requires -P${name}=<value>")
}
}
args(
probePack.get(),
probeDimension.get(),
probeSeed.get(),
probeTileX.get(),
probeTileZ.get(),
probeOutput.get(),
probeStudio.get())
args(probePack.get(), probeDimension.get(), probeSeed.get(), probeMinimumChunkX.get(), probeMaximumChunkX.get(),
probeMinimumChunkZ.get(), probeMaximumChunkZ.get(), threads.get(), work.get())
}
}
tasks.register('seaInletProbe', JavaExec) {
group = 'verification'
description = 'Plans and generates real pack chunks with and without sea inlets and reports containment, seal, roof, sea level, ocean connection and order determinism.'
mainClass = 'art.arcane.iris.probe.SeaInletProbe'
classpath = sourceSets.main.runtimeClasspath
jvmArgs('--add-modules', 'jdk.incubator.vector', '-Djava.awt.headless=true')
minHeapSize = '2g'
maxHeapSize = '8g'
dependsOn(':core:compileJava')
doFirst {
Provider<String> packOff = providers.gradleProperty('probePackOff')
Provider<String> windows = providers.gradleProperty('probeWindows')
Provider<String> mode = providers.gradleProperty('probeMode').orElse('gate')
if (!probePack.isPresent() || !packOff.isPresent() || !probeDimension.isPresent() || !windows.isPresent() || !probeOutput.isPresent()) {
throw new GradleException('seaInletProbe requires probePack, probePackOff, probeDimension, probeWindows=x0..x1,z0..z1 and probeOutput')
}
String[] axes = windows.get().split(',')
String[] xs = axes[0].split('\\.\\.')
String[] zs = axes[1].split('\\.\\.')
args(probePack.get(), packOff.get(), probeDimension.get(), probeSeed.orElse('1337').get(), xs[0], xs[1], zs[0], zs[1], mode.get(),
probeOutput.get(), providers.gradleProperty('probeThreads').orElse('6').get())
}
}
@@ -371,7 +399,8 @@ tasks.register('transitionProbe', JavaExec) {
}
}
['transitionProbe', 'hydrologyProbe', 'genProbe', 'hydrologyPackProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe', 'hydrologyBlockProbe'].each { String taskName ->
['transitionProbe', 'hydrologyProbe', 'genProbe', 'generationOrderProbe', 'riverTransectProbe', 'terrain3DProbe',
'hydrologyBlockProbe', 'cavePoolProbe', 'seaInletProbe'].each { String taskName ->
tasks.named(taskName, JavaExec).configure { JavaExec task ->
classpath += configurations.studioProbeApi
doFirst {
@@ -0,0 +1,546 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
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 CavePoolProbe {
private static final String LOG_PREFIX = "[cave-pool-probe]";
private static final int[] DX = {1, -1, 0, 0, 0, 0};
private static final int[] DY = {0, 0, 0, 0, -1, 1};
private static final int[] DZ = {0, 0, 1, -1, 0, 0};
private static final int UP = 5;
private CavePoolProbe() {
}
record Configuration(File pack, String dimension, long seed, int minX, int maxX, int minZ, int maxZ, int threads, Path work) {
static Configuration parse(String[] arguments) {
if (arguments.length != 9) {
throw new IllegalArgumentException("Expected: <pack> <dimension> <seed> <minChunkX> <maxChunkX> <minChunkZ> <maxChunkZ> "
+ "<threads> <workParent>");
}
return new Configuration(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]), Path.of(arguments[8]));
}
Area area() {
return new Area(minX, maxX, minZ, maxZ);
}
}
record Area(int minX, int maxX, int minZ, int maxZ) {
boolean inner(int chunkX, int chunkZ) {
return chunkX >= minX && chunkX <= maxX && chunkZ >= minZ && chunkZ <= maxZ;
}
List<long[]> chunks(int margin) {
List<long[]> chunks = new ArrayList<>();
for (int z = minZ - margin; z <= maxZ + margin; z++) {
for (int x = minX - margin; x <= maxX + margin; x++) {
chunks.add(new long[]{x, z});
}
}
return chunks;
}
}
record Counts(int chunks, int pools, int checked, int guards, Map<String, Integer> fluids, List<Integer> areas, List<Integer> depths,
Map<Integer, Integer> chunkSpans, int airLeaks, int openLeaks, int skyLeaks, int foreignFluid, int waterloggableWalls,
int unsupportedGravity, int fluidAbove, int gravityCeilings, Map<String, Integer> replaced, List<String> examples) {
long failures() {
return (long) airLeaks + openLeaks + skyLeaks + foreignFluid + waterloggableWalls + unsupportedGravity + fluidAbove + gravityCeilings;
}
}
static Counts check(Engine engine, Area area, ExecutorService workers) throws Exception {
return count(survey(engine, area, workers), area);
}
public static void main(String[] arguments) {
try {
scan(Configuration.parse(arguments));
System.exit(0);
} catch (Throwable failure) {
System.out.println(LOG_PREFIX + " FAIL");
failure.printStackTrace(System.out);
System.exit(1);
}
}
private static RealPackProbeSupport.Workspace workspace(Configuration configuration) throws Exception {
return RealPackProbeSupport.openWorkspace(new RealPackProbeSupport.WorkspaceOptions(configuration.pack(), configuration.dimension(),
LOG_PREFIX, null, configuration.work()));
}
private static void scan(Configuration configuration) throws Exception {
try (RealPackProbeSupport.Workspace workspace = workspace(configuration)) {
Survey forward;
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), false, "forward")) {
Engine engine = session.engine();
preplan(engine, configuration);
forward = survey(engine, configuration.area(), null);
requireQuiet(engine, "forward");
}
report(count(forward, configuration.area()));
Map<Long, Long> shuffled;
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), false, "shuffled")) {
Engine engine = session.engine();
preplan(engine, configuration);
shuffled = hashes(engine, configuration);
requireQuiet(engine, "shuffled");
}
compare(forward.hashes(), shuffled);
}
}
private static void preplan(Engine engine, Configuration configuration) throws Exception {
HydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
if (hydrology == null || !hydrology.enabled()) {
return;
}
try {
hydrology.prepareWindowsAround(configuration.minX() - 1, configuration.minZ() - 1, configuration.maxX() + 1,
configuration.maxZ() + 1, 1).get();
} catch (ExecutionException failure) {
throw new IllegalStateException("Hydrology preplan failed", failure.getCause());
}
}
private static void requireQuiet(Engine engine, String label) throws InterruptedException {
List<Throwable> reports = RealPackProbeSupport.settleAndDrain(engine);
if (!reports.isEmpty()) {
RealPackProbeSupport.printReports(LOG_PREFIX, label, reports);
IllegalStateException failure = new IllegalStateException(label + " generation reported " + reports.size() + " error(s)");
reports.forEach(failure::addSuppressed);
throw failure;
}
}
record Cell(long position, String fluid) {
}
record ChunkRecord(long[] open, long[] fluidOrAir, long[] gravity, long[] waterloggable, int[] roof) {
boolean open(int localX, int y, int localZ) {
return bit(open, localX, y, localZ);
}
boolean gravity(int localX, int y, int localZ) {
return bit(gravity, localX, y, localZ);
}
boolean waterloggable(int localX, int y, int localZ) {
return bit(waterloggable, localX, y, localZ);
}
boolean fluidOrAir(int localX, int y, int localZ) {
return bit(fluidOrAir, localX, y, localZ);
}
private static boolean bit(long[] bits, int localX, int y, int localZ) {
int index = (y << 8) | (localZ << 4) | localX;
return (bits[index >>> 6] & (1L << (index & 63))) != 0L;
}
}
record Survey(Map<Long, Long> hashes, Map<Long, ChunkRecord> chunks, List<Cell> pool, Map<String, Integer> replaced, int guards) {
}
private static Survey survey(Engine engine, Area area, ExecutorService workers) throws Exception {
List<long[]> chunks = area.chunks(2);
Map<Long, Long> hashes = new HashMap<>();
Map<Long, ChunkRecord> records = new HashMap<>();
List<Cell> pool = new ArrayList<>();
Map<String, Integer> replaced = new TreeMap<>();
int height = engine.getTarget().getHeight();
List<Future<Hunk<NativeBlockState>>> generated = new ArrayList<>(chunks.size());
if (workers != null) {
for (long[] chunk : chunks) {
generated.add(workers.submit(() -> generate(engine, (int) chunk[0], (int) chunk[1], height, hashes)));
}
}
int guards = 0;
for (int index = 0; index < chunks.size(); index++) {
int chunkX = (int) chunks.get(index)[0];
int chunkZ = (int) chunks.get(index)[1];
Hunk<NativeBlockState> blocks;
try {
blocks = workers == null ? generate(engine, chunkX, chunkZ, height, hashes) : generated.get(index).get();
} catch (ExecutionException failure) {
throw new IllegalStateException("Failed to generate " + chunkX + "," + chunkZ, failure.getCause());
}
records.put(key(chunkX, chunkZ), record(blocks, height));
guards += poolCells(engine, chunkX, chunkZ, blocks, pool, replaced);
if (workers == null) {
progress(index + 1, chunks.size());
}
}
return new Survey(hashes, records, pool, replaced, guards);
}
private static Hunk<NativeBlockState> generate(Engine engine, int chunkX, int chunkZ, int height, Map<Long, Long> hashes) {
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<NativeBiome> 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 " + chunkX + "," + chunkZ, failure);
}
long hash = hash(blocks, biomes, height);
synchronized (hashes) {
hashes.put(key(chunkX, chunkZ), hash);
}
return blocks;
}
private static int poolCells(Engine engine, int chunkX, int chunkZ, Hunk<NativeBlockState> blocks, List<Cell> pool,
Map<String, Integer> replaced) {
MantleChunk<Matter> chunk = engine.getMantle().getMantle().useChunk(chunkX, chunkZ);
int[] guards = new int[1];
try {
chunk.iterate(HydrologyCaveCell.class, (x, y, z, cell) -> {
if (cell == null) {
return;
}
if (cell.action() == HydrologyCaveAction.SEAL_GUARD) {
guards[0]++;
return;
}
if (cell.action() != HydrologyCaveAction.POOL_SOURCE) {
return;
}
pool.add(new Cell(pack((chunkX << 4) + x, y, (chunkZ << 4) + z), cell.fluidProfileKey()));
NativeBlockState state = blocks.get(x, y, z);
if (!source(state, cell.fluidProfileKey())) {
replaced.merge(cell.fluidProfileKey() + "->" + (state == null ? "null" : state.key()), 1, Integer::sum);
}
});
} finally {
chunk.release();
}
return guards[0];
}
private static boolean source(NativeBlockState state, String fluid) {
if (state == null || !state.isFluid()) {
return false;
}
String level = IrisProceduralBlocks.propertyValue(state, "level");
boolean still = level == null || "0".equals(level);
boolean matches = "lava".equals(fluid) ? state.key().startsWith("minecraft:lava") : state.isWater();
return still && matches;
}
private static ChunkRecord record(Hunk<NativeBlockState> blocks, int height) {
long[] open = new long[height * 4];
long[] fluidOrAir = new long[height * 4];
long[] gravity = new long[height * 4];
long[] waterloggable = new long[height * 4];
int[] roof = new int[256];
Arrays.fill(roof, -1);
for (int y = 0; y < height; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
NativeBlockState state = blocks.get(x, y, z);
int index = (y << 8) | (z << 4) | x;
boolean air = state == null || state.isAir();
boolean fluid = !air && state.isFluid();
boolean solid = !air && !fluid && state.isSolid();
boolean logged = !air && !fluid && IrisProceduralBlocks.hasProperty(state, "waterlogged");
if (!solid && !fluid && !logged) {
open[index >>> 6] |= 1L << (index & 63);
}
if (logged) {
waterloggable[index >>> 6] |= 1L << (index & 63);
}
if (air || fluid) {
fluidOrAir[index >>> 6] |= 1L << (index & 63);
}
if (solid) {
roof[(z << 4) | x] = y;
if (IrisProceduralBlocks.isGravityAffected(state)) {
gravity[index >>> 6] |= 1L << (index & 63);
}
}
}
}
}
return new ChunkRecord(open, fluidOrAir, gravity, waterloggable, roof);
}
private static long hash(Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes, int height) {
long hash = 0x9e3779b97f4a7c15L;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
NativeBlockState state = blocks.get(x, y, z);
NativeBiome biome = biomes.get(x, y, z);
hash = mix(hash ^ (state == null ? 0 : state.key().hashCode()));
hash = mix(hash ^ (biome == null ? 0 : biome.key().hashCode()));
}
}
}
return hash;
}
private static Map<Long, Long> hashes(Engine engine, Configuration configuration) throws Exception {
List<long[]> chunks = new ArrayList<>(configuration.area().chunks(2));
Collections.shuffle(chunks, new Random(configuration.seed() ^ 0x5DEECE66DL));
ExecutorService executor = Executors.newFixedThreadPool(configuration.threads());
try {
List<Future<long[]>> futures = new ArrayList<>();
for (long[] chunk : chunks) {
futures.add(executor.submit(() -> {
int height = engine.getTarget().getHeight();
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, height, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, height, 16);
engine.generate((int) chunk[0] << 4, (int) chunk[1] << 4, blocks, biomes, true);
return new long[]{chunk[0], chunk[1], hash(blocks, biomes, height)};
}));
}
Map<Long, Long> hashes = new HashMap<>();
for (int index = 0; index < futures.size(); index++) {
long[] result = futures.get(index).get();
hashes.put(key((int) result[0], (int) result[1]), result[2]);
progress(index + 1, futures.size());
}
return hashes;
} finally {
executor.shutdown();
executor.awaitTermination(60L, TimeUnit.SECONDS);
}
}
private static void compare(Map<Long, Long> forward, Map<Long, Long> shuffled) {
int mismatches = 0;
List<String> examples = new ArrayList<>();
for (Map.Entry<Long, Long> entry : forward.entrySet()) {
if (!entry.getValue().equals(shuffled.get(entry.getKey()))) {
mismatches++;
if (examples.size() < 16) {
examples.add((int) (entry.getKey() >> 32) + "," + (int) (long) entry.getKey());
}
}
}
long signature = 0L;
for (Map.Entry<Long, Long> entry : new TreeMap<>(forward).entrySet()) {
signature = mix(signature ^ entry.getKey() ^ mix(entry.getValue()));
}
System.out.printf(Locale.ROOT, "CAVE_POOL_ORDER chunks=%d mismatches=%d signature=%016x examples=%s%n", forward.size(), mismatches,
signature, examples);
}
static Counts count(Survey survey, Area area) {
Map<Long, Integer> index = new HashMap<>();
for (int cell = 0; cell < survey.pool().size(); cell++) {
index.put(survey.pool().get(cell).position(), cell);
}
int[] parent = new int[survey.pool().size()];
for (int cell = 0; cell < parent.length; cell++) {
parent[cell] = cell;
}
for (int cell = 0; cell < parent.length; cell++) {
long position = survey.pool().get(cell).position();
for (int direction = 0; direction < 6; direction++) {
Integer other = index.get(pack(x(position) + DX[direction], y(position) + DY[direction], z(position) + DZ[direction]));
if (other != null) {
parent[find(parent, cell)] = find(parent, other);
}
}
}
Map<Integer, int[]> levels = new HashMap<>();
for (int cell = 0; cell < parent.length; cell++) {
int root = find(parent, cell);
int y = y(survey.pool().get(cell).position());
int[] range = levels.computeIfAbsent(root, ignored -> new int[]{Integer.MAX_VALUE, Integer.MIN_VALUE});
range[0] = Math.min(range[0], y);
range[1] = Math.max(range[1], y);
}
int airLeaks = 0;
int openLeaks = 0;
int skyLeaks = 0;
int foreignFluid = 0;
int unsupportedGravity = 0;
int waterloggableWalls = 0;
int fluidAbove = 0;
int gravityCeilings = 0;
int checked = 0;
Map<Integer, Set<Long>> columns = new HashMap<>();
Map<Integer, Set<Long>> spans = new HashMap<>();
Map<Integer, String> fluids = new HashMap<>();
Set<Integer> innerPools = new HashSet<>();
List<String> examples = new ArrayList<>();
for (int cell = 0; cell < parent.length; cell++) {
Cell pool = survey.pool().get(cell);
long position = pool.position();
int x = x(position);
int y = y(position);
int z = z(position);
int root = find(parent, cell);
columns.computeIfAbsent(root, ignored -> new HashSet<>()).add(key(x, z));
spans.computeIfAbsent(root, ignored -> new HashSet<>()).add(key(x >> 4, z >> 4));
fluids.put(root, pool.fluid());
if (!area.inner(x >> 4, z >> 4)) {
continue;
}
innerPools.add(root);
checked++;
int level = levels.get(root)[1];
ChunkRecord own = survey.chunks().get(key(x >> 4, z >> 4));
if (y >= own.roof()[((z & 15) << 4) | (x & 15)]) {
skyLeaks++;
}
for (int direction = 0; direction < 6; direction++) {
int neighbourY = y + DY[direction];
int neighbourX = x + DX[direction];
int neighbourZ = z + DZ[direction];
if (index.containsKey(pack(neighbourX, neighbourY, neighbourZ))) {
continue;
}
ChunkRecord record = survey.chunks().get(key(neighbourX >> 4, neighbourZ >> 4));
int localX = neighbourX & 15;
int localZ = neighbourZ & 15;
if (direction == UP && neighbourY > level) {
if (record.fluidOrAir(localX, neighbourY, localZ) && !record.open(localX, neighbourY, localZ)) {
fluidAbove++;
if (examples.size() < 12) {
examples.add("fluid-above " + neighbourX + "," + neighbourY + "," + neighbourZ);
}
} else if (record.gravity(localX, neighbourY, localZ)) {
gravityCeilings++;
if (examples.size() < 12) {
examples.add("gravity-ceiling " + neighbourX + "," + neighbourY + "," + neighbourZ);
}
}
continue;
}
if (record.open(localX, neighbourY, localZ)) {
if (record.fluidOrAir(localX, neighbourY, localZ)) {
airLeaks++;
} else {
openLeaks++;
}
if (examples.size() < 12) {
examples.add(x + "," + y + "," + z + "->" + neighbourX + "," + neighbourY + "," + neighbourZ);
}
} else if (record.fluidOrAir(localX, neighbourY, localZ)) {
foreignFluid++;
} else if (record.waterloggable(localX, neighbourY, localZ)) {
waterloggableWalls++;
if (examples.size() < 12) {
examples.add("waterloggable " + neighbourX + "," + neighbourY + "," + neighbourZ);
}
} else if (record.gravity(localX, neighbourY, localZ) && neighbourY > 0
&& (index.containsKey(pack(neighbourX, neighbourY - 1, neighbourZ))
|| record.open(localX, neighbourY - 1, localZ) || record.fluidOrAir(localX, neighbourY - 1, localZ))) {
unsupportedGravity++;
if (examples.size() < 12) {
examples.add("gravity " + neighbourX + "," + neighbourY + "," + neighbourZ);
}
}
}
}
List<Integer> areas = new ArrayList<>();
List<Integer> depths = new ArrayList<>();
Map<String, Integer> byFluid = new TreeMap<>();
Map<Integer, Integer> chunkSpans = new TreeMap<>();
for (int root : innerPools) {
chunkSpans.merge(spans.get(root).size(), 1, Integer::sum);
areas.add(columns.get(root).size());
int[] range = levels.get(root);
depths.add(range[1] - range[0] + 1);
byFluid.merge(fluids.get(root), 1, Integer::sum);
}
Collections.sort(areas);
Collections.sort(depths);
return new Counts(survey.hashes().size(), innerPools.size(), checked, survey.guards(), byFluid, areas, depths, chunkSpans, airLeaks,
openLeaks, skyLeaks, foreignFluid, waterloggableWalls, unsupportedGravity, fluidAbove, gravityCeilings, survey.replaced(),
examples);
}
private static void report(Counts counts) {
List<Integer> areas = counts.areas();
Map<Integer, Integer> depthHistogram = new TreeMap<>();
counts.depths().forEach(depth -> depthHistogram.merge(depth, 1, Integer::sum));
long totalArea = areas.stream().mapToLong(Integer::longValue).sum();
System.out.printf(Locale.ROOT, "CAVE_POOL_SURVEY chunks=%d pools=%d pool_cells_checked=%d guards=%d fluids=%s%n",
counts.chunks(), counts.pools(), counts.checked(), counts.guards(), counts.fluids());
System.out.printf(Locale.ROOT, "CAVE_POOL_AREA total=%d min=%d median=%d mean=%.1f max=%d%n", totalArea,
areas.isEmpty() ? 0 : areas.get(0), areas.isEmpty() ? 0 : areas.get(areas.size() / 2),
areas.isEmpty() ? 0D : totalArea / (double) areas.size(), areas.isEmpty() ? 0 : areas.get(areas.size() - 1));
System.out.printf(Locale.ROOT, "CAVE_POOL_DEPTH histogram=%s%n", depthHistogram);
System.out.printf(Locale.ROOT, "CAVE_POOL_CHUNKS_SPANNED histogram=%s%n", counts.chunkSpans());
System.out.printf(Locale.ROOT, "CAVE_POOL_CONTAINMENT air_leaks=%d open_leaks=%d sky_leaks=%d foreign_fluid_contacts=%d "
+ "waterloggable_walls=%d unsupported_gravity_walls=%d fluid_above_surface=%d gravity_ceilings=%d replaced_cells=%s "
+ "examples=%s%n", counts.airLeaks(), counts.openLeaks(), counts.skyLeaks(), counts.foreignFluid(),
counts.waterloggableWalls(), counts.unsupportedGravity(), counts.fluidAbove(), counts.gravityCeilings(), counts.replaced(),
counts.examples());
}
private static int find(int[] parent, int cell) {
while (parent[cell] != cell) {
parent[cell] = parent[parent[cell]];
cell = parent[cell];
}
return cell;
}
private static void progress(int done, int total) {
if (done == total || done % 1024 == 0) {
System.out.println(LOG_PREFIX + " " + done + "/" + total);
}
}
private static long key(int x, int z) {
return ((long) x << 32) ^ (z & 0xffffffffL);
}
private static long pack(int x, int y, int z) {
return ((long) (x + (1 << 25)) << 38) | ((long) (z + (1 << 25)) << 12) | (y & 4095);
}
private static int x(long position) {
return (int) (position >>> 38) - (1 << 25);
}
private static int z(long position) {
return (int) ((position >>> 12) & ((1L << 26) - 1)) - (1 << 25);
}
private static int y(long position) {
return (int) (position & 4095);
}
private static long mix(long value) {
long mixed = (value ^ (value >>> 30)) * 0xbf58476d1ce4e5b9L;
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
return mixed ^ (mixed >>> 31);
}
}
@@ -38,13 +38,14 @@ import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.GenerationHistory;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.world.history.IrisBoundarySignatureSampler;
import art.arcane.iris.world.pregen.PregenHydrology;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
@@ -540,9 +541,8 @@ public final class GenerationProbe {
return generate(generator, configuration, options);
}
PregenPerformanceProfile.apply(engine);
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
try (HydrologyTileCache.PregenerationScope ignored = hydrology == null ? null
: hydrology.preparePregeneration(pregenerationArea(configuration))) {
try (HydrologyPregeneration pregeneration = PregenHydrology.open(engine)) {
PregenHydrology.prepareAll(pregeneration, hydrologyRegions(configuration));
preplanHydrology(engine, configuration, options);
return generate(generator, configuration, options);
} finally {
@@ -558,10 +558,10 @@ public final class GenerationProbe {
Optional<ProbeHydrologyPreplan> preplan = ProbeHydrologyPreplan.run(engine, configuration);
printDistinctCauses("hydrology preplan reported errors", drainReported());
System.out.println(preplan.map(ProbeHydrologyPreplan::machineLine)
.orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology"));
.orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=2 windows=0 skipped=no-hydrology"));
}
static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) {
static int[] scheduledChunkBounds(ProbeConfiguration configuration) {
List<ChunkCoordinate> coordinates = scheduleCoordinates(
configuration.warmupChunks() + configuration.measuredChunks(),
configuration.centerChunkX(), configuration.centerChunkZ());
@@ -575,13 +575,26 @@ public final class GenerationProbe {
maximumX = Math.max(maximumX, coordinate.x());
maximumZ = Math.max(maximumZ, coordinate.z());
}
int centerBlockX = Math.toIntExact(Math.clamp(((long) configuration.centerChunkX() << 4) + 8L,
(long) minimumX << 4, (((long) maximumX + 1L) << 4) - 1L));
int centerBlockZ = Math.toIntExact(Math.clamp(((long) configuration.centerChunkZ() << 4) + 8L,
(long) minimumZ << 4, (((long) maximumZ + 1L) << 4) - 1L));
return new HydrologyTileCache.PregenerationArea(centerBlockX, centerBlockZ,
(long) minimumX << 4, (long) minimumZ << 4,
(((long) maximumX + 1L) << 4) - 1L, (((long) maximumZ + 1L) << 4) - 1L);
return new int[]{minimumX, minimumZ, maximumX, maximumZ};
}
static List<long[]> hydrologyRegions(ProbeConfiguration configuration) {
int[] bounds = scheduledChunkBounds(configuration);
int chunks = WindowKey.SIZE >> 4;
int minimumX = Math.floorDiv(bounds[0], chunks);
int minimumZ = Math.floorDiv(bounds[1], chunks);
int maximumX = Math.floorDiv(bounds[2], chunks);
int maximumZ = Math.floorDiv(bounds[3], chunks);
int centerX = Math.floorDiv(configuration.centerChunkX(), chunks);
int centerZ = Math.floorDiv(configuration.centerChunkZ(), chunks);
List<long[]> regions = new ArrayList<>();
for (int regionZ = minimumZ; regionZ <= maximumZ; regionZ++) {
for (int regionX = minimumX; regionX <= maximumX; regionX++) {
regions.add(new long[]{regionX, regionZ});
}
}
regions.sort(Comparator.comparingLong((long[] region) -> Math.max(Math.abs(region[0] - centerX), Math.abs(region[1] - centerZ))));
return regions;
}
static List<Integer> spiralOrder(List<ChunkCoordinate> coordinates, int fromIndex, int centerChunkX,
@@ -0,0 +1,133 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Consumer;
final class HydrologyBlockMetrics {
private static final int BED_MARGIN = 2;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private final HydrologyProbe.Inputs inputs;
private final HydrologyRasterMetrics rasters;
private final HydrologyWindows.WindowRange range;
private final Consumer<String> examples;
private final Consumer<String> failures;
record Counts(HydrologyVoxels.WaterScan scan, long generated, long errors) {
}
interface Columns {
boolean known(int x, int z);
WaterRole role(int x, int z);
int terrain(int x, int z);
int natural(int x, int z);
}
HydrologyBlockMetrics(HydrologyProbe.Inputs inputs, HydrologyRasterMetrics rasters, HydrologyWindows.WindowRange range,
Consumer<String> examples, Consumer<String> failures) {
this.inputs = inputs;
this.rasters = rasters;
this.range = range;
this.examples = examples;
this.failures = failures;
}
static boolean scanned(Columns columns, int x, int z) {
return columns.role(x, z) != WaterRole.NONE || besideWater(columns, x, z);
}
static int scanFloor(Columns columns, int seaLevel, int x, int z) {
int floor = columns.terrain(x, z) - BED_MARGIN;
if (columns.role(x, z).wet() || columns.natural(x, z) >= seaLevel || besideWater(columns, x, z)) {
return floor;
}
return Math.max(floor, seaLevel + 1);
}
private static boolean besideWater(Columns columns, int x, int z) {
for (int[] offset : CARDINALS) {
int nx = x + offset[0];
int nz = z + offset[1];
if (columns.known(nx, nz) && columns.role(nx, nz).wet()) {
return true;
}
}
return false;
}
Counts count(WindowKey key) throws InterruptedException {
Columns columns = new Columns() {
@Override
public boolean known(int x, int z) {
return range.containsBlock(x, z);
}
@Override
public WaterRole role(int x, int z) {
return rasters.role(x, z);
}
@Override
public int terrain(int x, int z) {
return rasters.terrain(x, z);
}
@Override
public int natural(int x, int z) {
return inputs.naturalHeight().applyAsInt(x, z);
}
};
int minX = key.wx() * WindowKey.SIZE;
int minZ = key.wz() * WindowKey.SIZE;
List<int[]> scan = new ArrayList<>();
Set<Long> chunks = new LinkedHashSet<>();
for (int z = minZ; z < minZ + WindowKey.SIZE; z++) {
for (int x = minX; x < minX + WindowKey.SIZE; x++) {
if (!scanned(columns, x, z)) {
continue;
}
scan.add(new int[]{x, z, scanFloor(columns, inputs.settings().seaLevel(), x, z)});
chunks.add(HydrologyVoxels.chunkKey(x >> 4, z >> 4));
for (int[] offset : CARDINALS) {
chunks.add(HydrologyVoxels.chunkKey((x + offset[0]) >> 4, (z + offset[1]) >> 4));
}
}
}
List<Callable<HydrologyVoxels.ChunkBlocks>> tasks = new ArrayList<>(chunks.size());
for (long chunk : chunks) {
int chunkX = (int) (chunk >> 32);
int chunkZ = (int) chunk;
tasks.add(() -> inputs.blocks().generate(chunkX, chunkZ));
}
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
long errors = 0L;
for (Future<HydrologyVoxels.ChunkBlocks> future : inputs.workers().invokeAll(tasks)) {
try {
volume.put(future.get());
} catch (ExecutionException failure) {
errors++;
failures.accept("chunk generation: " + HydrologyPlanHarness.cause(failure));
}
}
HydrologyVoxels.WaterScan water = new HydrologyVoxels.WaterScan();
for (int[] column : scan) {
water.column(volume, column[0], column[1], column[2], inputs.worldHeight() - 1);
}
for (String example : water.examples) {
examples.accept(example);
}
return new Counts(water, chunks.size(), errors);
}
}
@@ -1,13 +1,10 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.runtime.IrisComplex;
import com.google.gson.GsonBuilder;
import javax.imageio.ImageIO;
@@ -15,54 +12,45 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public final class HydrologyBlockProbe {
private static final int MAXIMUM_CHUNKS = 64;
private static final long MAXIMUM_VOXELS = 16_777_216L;
private static final int MAXIMUM_EXAMPLES = 16;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Set<String> IMPLICIT_WATER_BLOCKS = Set.of(
"minecraft:seagrass", "minecraft:tall_seagrass", "minecraft:kelp", "minecraft:kelp_plant",
"minecraft:bubble_column");
private HydrologyBlockProbe() {
}
record Configuration(File pack, String dimension, long seed, int minimumChunkX, int maximumChunkX,
int minimumChunkZ, int maximumChunkZ, Set<String> requiredCoverage, File output,
boolean studio) {
record Configuration(File pack, String dimension, long seed, int minimumChunkX, int maximumChunkX, int minimumChunkZ,
int maximumChunkZ, Set<String> requiredCoverage, File output, boolean studio) {
Configuration {
if (pack == null || !pack.isAbsolute() || dimension == null || dimension.isBlank()
|| output == null || !output.isAbsolute()) {
if (pack == null || !pack.isAbsolute() || dimension == null || dimension.isBlank() || output == null || !output.isAbsolute()) {
throw new IllegalArgumentException("Absolute pack/output paths and a dimension key are required.");
}
long width = (long) maximumChunkX - minimumChunkX + 1;
long depth = (long) maximumChunkZ - minimumChunkZ + 1;
if (width < 1 || depth < 1 || width > MAXIMUM_CHUNKS || depth > MAXIMUM_CHUNKS
|| width * depth > MAXIMUM_CHUNKS
|| minimumChunkX < Integer.MIN_VALUE / 16 || minimumChunkZ < Integer.MIN_VALUE / 16
|| maximumChunkX > Integer.MAX_VALUE / 16 - 1 || maximumChunkZ > Integer.MAX_VALUE / 16 - 1) {
if (width < 1 || depth < 1 || width * depth > MAXIMUM_CHUNKS || minimumChunkX < Integer.MIN_VALUE / 16
|| minimumChunkZ < Integer.MIN_VALUE / 16 || maximumChunkX > Integer.MAX_VALUE / 16 - 1
|| maximumChunkZ > Integer.MAX_VALUE / 16 - 1) {
throw new IllegalArgumentException("Bounds must contain 1..64 chunks and fit block coordinates.");
}
requiredCoverage = Set.copyOf(requiredCoverage);
if (!Set.of("cane", "mouth", "channel", "bank").containsAll(requiredCoverage)) {
throw new IllegalArgumentException("Required coverage accepts cane,mouth,channel,bank or -.");
if (!Set.of("channel", "pond", "bank", "cane").containsAll(requiredCoverage)) {
throw new IllegalArgumentException("Required coverage accepts channel,pond,bank,cane or -.");
}
}
static Configuration parse(String[] args) {
if (args.length != 10) {
throw new IllegalArgumentException("Expected <pack> <dimension> <seed> <minChunkX> <maxChunkX> "
+ "<minChunkZ> <maxChunkZ> <requiredCoverage|-> <outputDir> <studio>.");
throw new IllegalArgumentException("Expected <pack> <dimension> <seed> <minChunkX> <maxChunkX> <minChunkZ> <maxChunkZ> "
+ "<requiredCoverage|-> <outputDir> <studio>.");
}
Set<String> required = new LinkedHashSet<>();
if (!args[7].equals("-")) {
@@ -71,38 +59,22 @@ public final class HydrologyBlockProbe {
}
}
return new Configuration(new File(args[0]), args[1], Long.parseLong(args[2]), Integer.parseInt(args[3]),
Integer.parseInt(args[4]), Integer.parseInt(args[5]), Integer.parseInt(args[6]), required,
new File(args[8]), RealPackProbeSupport.parseBoolean(args[9], "studio"));
}
}
record PlannedColumn(int naturalY, boolean ocean, HydrologyColumnLayer surface, NativeBlockState fluid) {
boolean mouthAt(int seaLevel) {
return surface != null && fluid != null && fluid.isWater()
&& surface.feature().type() == HydrologyFeatureType.MOUTH
&& surface.channel() && (surface.fluidOwned() || surface.oceanApron())
&& surface.fluidHeadY() == seaLevel;
Integer.parseInt(args[4]), Integer.parseInt(args[5]), Integer.parseInt(args[6]), required, new File(args[8]),
RealPackProbeSupport.parseBoolean(args[9], "studio"));
}
}
static final class Evidence {
long plannedWetColumns;
long channelColumns;
long pondColumns;
long bankColumns;
long caneRoots;
long validCaneRoots;
long invalidCaneSubstrates;
long dryCaneRoots;
long censoredCaneRoots;
long plannedWetVoxels;
long missingWetVoxels;
long bankColumns;
long bankExcavation;
int maximumBankCut;
long oceanBedWrites;
long drySillCuts;
long invalidDrySillCuts;
int connectedMouthComponents;
int disconnectedMouthComponents;
int censoredMouthComponents;
final Map<String, Long> plannedWetBlockCounts = new TreeMap<>();
HydrologyVoxels.WaterScan scan = new HydrologyVoxels.WaterScan();
final List<String> examples = new ArrayList<>();
void example(String value) {
@@ -112,53 +84,11 @@ public final class HydrologyBlockProbe {
}
}
record Result(String status, Configuration configuration, int worldMinimumY, int seaLevel,
Evidence evidence, List<String> failures, List<String> artifacts) {
record Result(String status, Configuration configuration, int seaLevel, Evidence evidence, List<String> failures,
List<String> artifacts) {
}
static final class Volume {
private final Map<Long, RealPackProbeSupport.GeneratedChunk> chunks;
private final int minimumX;
private final int maximumX;
private final int minimumZ;
private final int maximumZ;
private final int height;
Volume(Map<Long, RealPackProbeSupport.GeneratedChunk> chunks) {
if (chunks.isEmpty()) {
throw new IllegalArgumentException("Generated chunks are required.");
}
this.chunks = Map.copyOf(chunks);
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minZ = Integer.MAX_VALUE;
int maxZ = Integer.MIN_VALUE;
int selectedHeight = chunks.values().iterator().next().height();
for (RealPackProbeSupport.GeneratedChunk chunk : chunks.values()) {
if (chunk.height() != selectedHeight) {
throw new IllegalArgumentException("Generated chunks must have the same height.");
}
minX = Math.min(minX, chunk.chunkX() << 4);
maxX = Math.max(maxX, (chunk.chunkX() << 4) + 15);
minZ = Math.min(minZ, chunk.chunkZ() << 4);
maxZ = Math.max(maxZ, (chunk.chunkZ() << 4) + 15);
}
minimumX = minX;
maximumX = maxX;
minimumZ = minZ;
maximumZ = maxZ;
height = selectedHeight;
}
boolean contains(int x, int z) {
return chunks.containsKey(RiverFootprint.pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
}
NativeBlockState block(int x, int y, int z) {
RealPackProbeSupport.GeneratedChunk chunk = chunks.get(
RiverFootprint.pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
return chunk == null || y < 0 || y >= height ? null : chunk.blockAt(x, y, z);
}
record PlannedColumn(WaterRole role, int terrain, int natural, int head) {
}
public static void main(String[] args) {
@@ -166,10 +96,10 @@ public final class HydrologyBlockProbe {
try {
Result result = run(Configuration.parse(args));
Evidence evidence = result.evidence();
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=1 status=" + result.status()
+ " seed=" + result.configuration().seed() + " cane=" + evidence.validCaneRoots
+ " wetVoxels=" + evidence.plannedWetVoxels + " missingWet=" + evidence.missingWetVoxels
+ " mouths=" + evidence.connectedMouthComponents + " failures=" + result.failures().size()
System.out.println("IRIS_HYDROLOGY_BLOCK_RESULT version=2 status=" + result.status() + " seed=" + result.configuration().seed()
+ " plannedWetColumns=" + evidence.plannedWetColumns + " missingSurfaceColumns=" + evidence.scan.missingSurfaces
+ " containmentLeaks=" + evidence.scan.containmentLeaks + " tickConversionCells=" + evidence.scan.tickConversionCells
+ " cane=" + evidence.validCaneRoots + " failures=" + result.failures().size()
+ " output=" + result.configuration().output().getAbsolutePath());
exitCode = result.failures().isEmpty() ? 0 : 1;
} catch (Throwable failure) {
@@ -179,261 +109,161 @@ public final class HydrologyBlockProbe {
}
static Result run(Configuration configuration) throws Exception {
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(
configuration.pack(), configuration.dimension(), "[hydrology-blocks]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(
configuration.seed(), configuration.studio(), "hydrology-blocks")) {
StubPlatform.vanillaSolidity(true);
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(configuration.pack(), configuration.dimension(),
"[hydrology-blocks]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(configuration.seed(), configuration.studio(), "hydrology-blocks")) {
Engine engine = session.engine();
IrisComplex complex = engine.getComplex();
if (complex.getHydrologyRuntime() == null) {
HydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null || !runtime.enabled()) {
throw new IllegalStateException("The dimension must enable hydrology.");
}
long chunks = ((long) configuration.maximumChunkX() - configuration.minimumChunkX() + 1)
* ((long) configuration.maximumChunkZ() - configuration.minimumChunkZ() + 1);
if (chunks * 256 * engine.getTarget().getHeight() > MAXIMUM_VOXELS) {
throw new IllegalArgumentException("Generated bounds exceed the 16777216 voxel limit.");
}
Map<Long, RealPackProbeSupport.GeneratedChunk> generated = new HashMap<>();
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
for (int z = configuration.minimumChunkZ(); z <= configuration.maximumChunkZ(); z++) {
for (int x = configuration.minimumChunkX(); x <= configuration.maximumChunkX(); x++) {
generated.put(RiverFootprint.pack(x, z), RealPackProbeSupport.generateChunk(engine, x, z));
volume.put(HydrologyVoxels.generate(engine, x, z));
}
}
Volume volume = new Volume(generated);
Map<Long, PlannedColumn> planned = new HashMap<>();
for (int z = volume.minimumZ; z <= volume.maximumZ; z++) {
for (int x = volume.minimumX; x <= volume.maximumX; x++) {
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
int natural = sample == null ? (int) Math.round(complex.getNaturalHeightStream().getDouble(x, z))
: sample.naturalHeight();
HydrologyColumnLayer layer = surfaceForInspection(sample);
NativeBlockState fluid = layer == null ? null : complex.resolveHydrologyFluid(
layer.profileKey(), x, z);
planned.put(RiverFootprint.pack(x, z), new PlannedColumn(natural, sample == null
? complex.getBridgeStream().get(x, z) == InferredType.SEA : sample.ocean(), layer, fluid));
}
}
int seaLevel = complex.getHydrologyRuntime().settings().seaLevel();
Evidence evidence = inspect(volume, planned, seaLevel);
int maximumCut = complex.getHydrologyRuntime().settings().surface().banks().erosion()
.excavation().maximumDepth();
List<String> failures = failures(evidence, maximumCut, configuration.requiredCoverage());
Map<Long, PlannedColumn> planned = planned(complex, runtime, configuration);
int seaLevel = engine.getDimension().getFluidHeight();
Evidence evidence = inspect(configuration, volume, planned, engine);
List<String> failures = failures(evidence, configuration.requiredCoverage());
List<Throwable> reports = RealPackProbeSupport.drainReported();
RealPackProbeSupport.printReports("[hydrology-blocks]", "generation", reports);
if (!reports.isEmpty()) {
failures.add("The engine reported " + reports.size() + " generation errors.");
}
Files.createDirectories(configuration.output().toPath());
List<String> artifacts = writeImages(configuration.output(), volume, planned, seaLevel);
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, engine.getMinHeight(),
seaLevel, evidence, List.copyOf(failures), artifacts);
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(),
json(result), StandardCharsets.UTF_8);
List<String> artifacts = writeImages(configuration, volume, engine);
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, seaLevel, evidence, List.copyOf(failures), artifacts);
Files.writeString(new File(configuration.output(), "hydrology-blocks.json").toPath(), json(result), StandardCharsets.UTF_8);
return result;
}
}
static String json(Result result) {
Configuration configuration = result.configuration();
Map<String, Object> settings = Map.ofEntries(
Map.entry("pack", configuration.pack().getAbsolutePath()),
Map.entry("dimension", configuration.dimension()),
Map.entry("seed", configuration.seed()),
Map.entry("minimumChunkX", configuration.minimumChunkX()),
Map.entry("maximumChunkX", configuration.maximumChunkX()),
Map.entry("minimumChunkZ", configuration.minimumChunkZ()),
Map.entry("maximumChunkZ", configuration.maximumChunkZ()),
Map.entry("requiredCoverage", configuration.requiredCoverage()),
Map.entry("output", configuration.output().getAbsolutePath()),
Map.entry("studio", configuration.studio()));
Map<String, Object> report = Map.of("status", result.status(), "configuration", settings,
"worldMinimumY", result.worldMinimumY(), "seaLevel", result.seaLevel(),
"evidence", result.evidence(), "failures", result.failures(), "artifacts", result.artifacts(),
"coordinateConvention", "Block X/Z are world coordinates; evidence Y and seaLevel are relative to worldMinimumY.",
"imageLegend", "air=light blue,water=blue,lava=orange,cane=lime,vegetation=green,sand=tan,soil=brown,other=gray");
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
}
static HydrologyColumnLayer surfaceForInspection(HydrologyColumnSample sample) {
if (sample == null) {
return null;
}
HydrologyColumnLayer primary = sample.primarySurfaceLayerOrNull();
if (primary != null) {
return primary;
}
for (HydrologyColumnLayer layer : sample.layers()) {
if (layer.oceanApron() && layer.feature().type() == HydrologyFeatureType.MOUTH) {
return layer;
private static Map<Long, PlannedColumn> planned(IrisComplex complex, HydrologyRuntime runtime, Configuration configuration) {
Map<Long, PlannedColumn> planned = new HashMap<>();
for (int chunkZ = configuration.minimumChunkZ(); chunkZ <= configuration.maximumChunkZ(); chunkZ++) {
for (int chunkX = configuration.minimumChunkX(); chunkX <= configuration.maximumChunkX(); chunkX++) {
int[] heads = HydrologyVoxels.effectiveHeads(complex, chunkX, chunkZ);
ChunkWaterRaster own = runtime.raster(chunkX, chunkZ);
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
int x = (chunkX << 4) + lx;
int z = (chunkZ << 4) + lz;
int head = heads[(lz << 4) | lx];
int natural = complex.naturalTrueHeight(x, z);
int terrain = own.empty() ? natural : own.terrainHeight(lx, lz);
planned.put(HydrologyVoxels.columnKey(x, z), new PlannedColumn(own.role(lx, lz), terrain, natural, head));
}
}
}
}
return null;
return planned;
}
static Evidence inspect(Volume volume, Map<Long, PlannedColumn> planned, int seaLevel) {
static Evidence inspect(Configuration configuration, HydrologyVoxels.Volume volume, Map<Long, PlannedColumn> planned, Engine engine) {
Evidence evidence = new Evidence();
for (int z = volume.minimumZ; z <= volume.maximumZ; z++) {
for (int x = volume.minimumX; x <= volume.maximumX; x++) {
if (!volume.contains(x, z)) {
continue;
}
for (int y = 0; y < volume.height; y++) {
NativeBlockState block = volume.block(x, y, z);
if (key(block).equals("minecraft:sugar_cane")
&& !key(volume.block(x, y - 1, z)).equals("minecraft:sugar_cane")) {
inspectCane(volume, block, x, y, z, evidence);
int minimumX = configuration.minimumChunkX() << 4;
int minimumZ = configuration.minimumChunkZ() << 4;
int maximumX = (configuration.maximumChunkX() << 4) + 15;
int maximumZ = (configuration.maximumChunkZ() << 4) + 15;
int height = engine.getTarget().getHeight();
int seaLevel = engine.getDimension().getFluidHeight();
HydrologyBlockMetrics.Columns columns = new HydrologyBlockMetrics.Columns() {
@Override
public boolean known(int x, int z) {
return planned.containsKey(HydrologyVoxels.columnKey(x, z));
}
@Override
public WaterRole role(int x, int z) {
return planned.get(HydrologyVoxels.columnKey(x, z)).role();
}
@Override
public int terrain(int x, int z) {
return planned.get(HydrologyVoxels.columnKey(x, z)).terrain();
}
@Override
public int natural(int x, int z) {
return planned.get(HydrologyVoxels.columnKey(x, z)).natural();
}
};
for (int z = minimumZ; z <= maximumZ; z++) {
for (int x = minimumX; x <= maximumX; x++) {
PlannedColumn column = planned.get(HydrologyVoxels.columnKey(x, z));
inspectCane(volume, x, z, height, evidence);
switch (column.role()) {
case CHANNEL, MOUTH, CASCADE, WATERFALL -> evidence.channelColumns++;
case POND -> evidence.pondColumns++;
case BANK -> evidence.bankColumns++;
default -> {
}
}
PlannedColumn column = planned.get(RiverFootprint.pack(x, z));
if (column == null || column.surface() == null) {
continue;
if (column.head() != HydrologyVoxels.NO_WATER) {
evidence.plannedWetColumns++;
}
HydrologyColumnLayer surface = column.surface();
int cut = Math.max(0, column.naturalY() - surface.bedY());
if (surface.terrainOwned() && (column.ocean() || column.naturalY() < seaLevel)) {
evidence.oceanBedWrites++;
}
if (surface.terrainOwned() && column.naturalY() == seaLevel && cut > 0) {
evidence.drySillCuts++;
if (!surface.channel() || !surface.fluidOwned() || surface.fluidHeadY() != seaLevel) {
evidence.invalidDrySillCuts++;
}
}
if (surface.terrainOwned() && !surface.channel()) {
evidence.bankColumns++;
evidence.bankExcavation += cut;
evidence.maximumBankCut = Math.max(evidence.maximumBankCut, cut);
}
if (!surface.fluidOwned() || !surface.channel() || column.fluid() == null) {
continue;
}
for (int y = Math.max(0, surface.bedY() + 1); y <= surface.fluidHeadY() && y < volume.height; y++) {
evidence.plannedWetVoxels++;
NativeBlockState actual = volume.block(x, y, z);
String actualKey = key(actual);
evidence.plannedWetBlockCounts.merge(actualKey, 1L, Long::sum);
boolean matches = column.fluid().isWater() ? water(actual)
: key(column.fluid()).equals(actualKey);
if (!matches) {
evidence.missingWetVoxels++;
evidence.example("missing wet voxel " + x + "," + y + "," + z + "=" + actualKey);
}
if (HydrologyBlockMetrics.scanned(columns, x, z)) {
evidence.scan.column(volume, x, z, HydrologyBlockMetrics.scanFloor(columns, seaLevel, x, z), height - 1);
}
}
}
inspectMouths(volume, planned, seaLevel, evidence);
return evidence;
}
private static void inspectCane(Volume volume, NativeBlockState cane, int x, int y, int z, Evidence evidence) {
evidence.caneRoots++;
NativeBlockState substrate = volume.block(x, y - 1, z);
if (substrate == null || !cane.canPlaceOnto(substrate)) {
evidence.invalidCaneSubstrates++;
evidence.example("unsupported cane " + x + "," + y + "," + z + " on " + key(substrate));
return;
}
boolean censored = false;
for (int[] offset : CARDINALS) {
int nx = x + offset[0];
int nz = z + offset[1];
if (!volume.contains(nx, nz)) {
censored = true;
private static void inspectCane(HydrologyVoxels.Volume volume, int x, int z, int height, Evidence evidence) {
for (int y = 1; y < height; y++) {
if (volume.at(x, y, z) != HydrologyVoxels.CANE || volume.at(x, y - 1, z) == HydrologyVoxels.CANE) {
continue;
}
NativeBlockState adjacent = volume.block(nx, y - 1, nz);
if (water(adjacent) || key(adjacent).equals("minecraft:frosted_ice")) {
evidence.validCaneRoots++;
return;
}
}
if (censored) {
evidence.censoredCaneRoots++;
} else {
evidence.dryCaneRoots++;
evidence.example("dry cane root " + x + "," + y + "," + z);
}
}
private static void inspectMouths(Volume volume, Map<Long, PlannedColumn> planned, int seaLevel, Evidence evidence) {
Set<Long> visited = new HashSet<>();
TreeMap<Long, PlannedColumn> ordered = new TreeMap<>(planned);
for (Map.Entry<Long, PlannedColumn> entry : ordered.entrySet()) {
if (!entry.getValue().mouthAt(seaLevel) || !visited.add(entry.getKey())) {
evidence.caneRoots++;
byte substrate = volume.at(x, y - 1, z);
if (substrate != HydrologyVoxels.SOLID) {
evidence.invalidCaneSubstrates++;
evidence.example("unsupported cane " + x + "," + y + "," + z);
continue;
}
int startX = (int) (entry.getKey() >> 32);
int startZ = (int) (long) entry.getKey();
if (!water(volume.block(startX, seaLevel, startZ))) {
evidence.disconnectedMouthComponents++;
evidence.example("dry mouth " + startX + "," + seaLevel + "," + startZ);
continue;
}
ArrayDeque<Long> queue = new ArrayDeque<>();
queue.add(entry.getKey());
long courseId = entry.getValue().surface().feature().courseId();
boolean ocean = false;
boolean channel = false;
boolean censored = false;
while (!queue.isEmpty()) {
long packed = queue.removeFirst();
int x = (int) (packed >> 32);
int z = (int) packed;
PlannedColumn column = planned.get(packed);
ocean |= column != null && column.ocean() && column.naturalY() < seaLevel
&& (column.surface() == null || !column.surface().terrainOwned());
channel |= column != null && column.surface() != null && column.surface().channel()
&& column.surface().fluidOwned() && !column.surface().oceanApron()
&& column.surface().fluidHeadY() == seaLevel
&& column.surface().feature().courseId() == courseId;
for (int[] offset : CARDINALS) {
int nx = x + offset[0];
int nz = z + offset[1];
if (!volume.contains(nx, nz)) {
censored = true;
continue;
}
long next = RiverFootprint.pack(nx, nz);
if (water(volume.block(nx, seaLevel, nz)) && visited.add(next)) {
queue.addLast(next);
}
}
boolean wet = false;
for (int[] offset : CARDINALS) {
byte adjacent = volume.at(x + offset[0], y - 1, z + offset[1]);
censored |= adjacent == HydrologyVoxels.UNKNOWN;
wet |= HydrologyVoxels.water(adjacent);
}
if (ocean && channel) {
evidence.connectedMouthComponents++;
if (wet) {
evidence.validCaneRoots++;
} else if (censored) {
evidence.censoredMouthComponents++;
evidence.censoredCaneRoots++;
} else {
evidence.disconnectedMouthComponents++;
evidence.example("mouth without connected course and ocean water "
+ startX + "," + seaLevel + "," + startZ);
evidence.dryCaneRoots++;
evidence.example("dry cane root " + x + "," + y + "," + z);
}
}
}
static List<String> failures(Evidence evidence, int maximumBankCut, Set<String> required) {
static List<String> failures(Evidence evidence, Set<String> required) {
List<String> failures = new ArrayList<>();
if (evidence.invalidCaneSubstrates > 0 || evidence.dryCaneRoots > 0) {
failures.add("Generated sugar cane has invalid substrate or no adjacent supporting water.");
failures.add("Generated sugar cane has invalid substrate or no adjacent water.");
}
if (evidence.missingWetVoxels > 0) {
failures.add("Generated blocks do not retain every planned surface channel fluid voxel.");
if (evidence.scan.missingSurfaces > 0) {
failures.add("Generated blocks miss the planned water surface in " + evidence.scan.missingSurfaces + " columns.");
}
if (evidence.disconnectedMouthComponents > 0) {
failures.add("A fully sampled mouth water component does not connect to untouched natural ocean water.");
if (evidence.scan.containmentLeaks > 0) {
failures.add("Generated water leaks into open ground in " + evidence.scan.containmentLeaks + " cells.");
}
if (evidence.oceanBedWrites > 0 || evidence.invalidDrySillCuts > 0) {
failures.add("Hydrology owns ocean-bed terrain or an invalid dry shoreline sill cut.");
}
if (evidence.maximumBankCut > maximumBankCut) {
failures.add("Published dry-bank cut depth exceeds its configured budget.");
if (evidence.scan.tickConversionCells > 0) {
failures.add("Fluid ticks would move generated water in " + evidence.scan.tickConversionCells + " cells.");
}
for (String coverage : required) {
boolean covered = switch (coverage) {
case "cane" -> evidence.validCaneRoots > 0;
case "mouth" -> evidence.connectedMouthComponents > 0;
case "channel" -> evidence.plannedWetVoxels > 0;
case "channel" -> evidence.channelColumns > 0;
case "pond" -> evidence.pondColumns > 0;
case "bank" -> evidence.bankColumns > 0;
case "cane" -> evidence.validCaneRoots > 0;
default -> false;
};
if (!covered) {
@@ -443,44 +273,84 @@ public final class HydrologyBlockProbe {
return failures;
}
private static List<String> writeImages(File output, Volume volume, Map<Long, PlannedColumn> planned, int seaLevel)
throws Exception {
int width = volume.maximumX - volume.minimumX + 1;
int depth = volume.maximumZ - volume.minimumZ + 1;
static String json(Result result) {
Configuration configuration = result.configuration();
Map<String, Object> settings = new LinkedHashMap<>();
settings.put("pack", configuration.pack().getAbsolutePath());
settings.put("dimension", configuration.dimension());
settings.put("seed", configuration.seed());
settings.put("minimumChunkX", configuration.minimumChunkX());
settings.put("maximumChunkX", configuration.maximumChunkX());
settings.put("minimumChunkZ", configuration.minimumChunkZ());
settings.put("maximumChunkZ", configuration.maximumChunkZ());
settings.put("requiredCoverage", configuration.requiredCoverage());
settings.put("studio", configuration.studio());
Evidence evidence = result.evidence();
Map<String, Object> counts = new LinkedHashMap<>();
counts.put("plannedWetColumns", evidence.plannedWetColumns);
counts.put("missingSurfaceColumns", evidence.scan.missingSurfaces);
counts.put("channelColumns", evidence.channelColumns);
counts.put("pondColumns", evidence.pondColumns);
counts.put("bankColumns", evidence.bankColumns);
counts.put("waterCells", evidence.scan.waterCells);
counts.put("containmentLeaks", evidence.scan.containmentLeaks);
counts.put("tickConversionCells", evidence.scan.tickConversionCells);
counts.put("censoredCells", evidence.scan.censoredCells);
counts.put("caneRoots", evidence.caneRoots);
counts.put("validCaneRoots", evidence.validCaneRoots);
counts.put("dryCaneRoots", evidence.dryCaneRoots);
counts.put("invalidCaneSubstrates", evidence.invalidCaneSubstrates);
counts.put("censoredCaneRoots", evidence.censoredCaneRoots);
List<String> examples = new ArrayList<>(evidence.examples);
examples.addAll(evidence.scan.examples);
Map<String, Object> report = new LinkedHashMap<>();
report.put("status", result.status());
report.put("configuration", settings);
report.put("seaLevel", result.seaLevel());
report.put("evidence", counts);
report.put("examples", examples);
report.put("failures", result.failures());
report.put("artifacts", result.artifacts());
report.put("coordinateConvention", "Block X/Z are world coordinates; Y is relative to the world minimum height.");
report.put("imageLegend", "open=light blue,water=blue,lava=orange,cane=lime,solid=gray");
return new GsonBuilder().setPrettyPrinting().create().toJson(report) + "\n";
}
private static List<String> writeImages(Configuration configuration, HydrologyVoxels.Volume volume, Engine engine) throws Exception {
int minimumX = configuration.minimumChunkX() << 4;
int minimumZ = configuration.minimumChunkZ() << 4;
int width = (configuration.maximumChunkX() - configuration.minimumChunkX() + 1) << 4;
int depth = (configuration.maximumChunkZ() - configuration.minimumChunkZ() + 1) << 4;
int height = engine.getTarget().getHeight();
BufferedImage overhead = new BufferedImage(width, depth, BufferedImage.TYPE_INT_RGB);
int selectedX = volume.minimumX + width / 2;
int selectedZ = volume.minimumZ + depth / 2;
for (int z = volume.minimumZ; z <= volume.maximumZ; z++) {
for (int x = volume.minimumX; x <= volume.maximumX; x++) {
for (int y = volume.height - 1; y >= 0; y--) {
NativeBlockState block = volume.block(x, y, z);
if (block != null && !block.isAir()) {
overhead.setRGB(x - volume.minimumX, z - volume.minimumZ, color(block));
for (int z = 0; z < depth; z++) {
for (int x = 0; x < width; x++) {
for (int y = height - 1; y >= 0; y--) {
byte voxel = volume.at(minimumX + x, y, minimumZ + z);
if (voxel != HydrologyVoxels.OPEN) {
overhead.setRGB(x, z, color(voxel));
break;
}
}
PlannedColumn column = planned.get(RiverFootprint.pack(x, z));
if (column != null && column.mouthAt(seaLevel)) {
selectedX = x;
selectedZ = z;
}
}
}
File plan = new File(output, "generated-overhead.png");
ImageIO.write(overhead, "png", plan);
List<String> artifacts = new ArrayList<>();
File plan = new File(configuration.output(), "generated-overhead.png");
ImageIO.write(overhead, "png", plan);
artifacts.add(plan.getAbsolutePath());
int selectedX = minimumX + width / 2;
int selectedZ = minimumZ + depth / 2;
for (boolean alongX : new boolean[]{true, false}) {
int span = alongX ? width : depth;
BufferedImage section = new BufferedImage(span, volume.height, BufferedImage.TYPE_INT_RGB);
BufferedImage section = new BufferedImage(span, height, BufferedImage.TYPE_INT_RGB);
for (int position = 0; position < span; position++) {
int x = alongX ? volume.minimumX + position : selectedX;
int z = alongX ? selectedZ : volume.minimumZ + position;
for (int y = 0; y < volume.height; y++) {
section.setRGB(position, volume.height - y - 1, color(volume.block(x, y, z)));
int x = alongX ? minimumX + position : selectedX;
int z = alongX ? selectedZ : minimumZ + position;
for (int y = 0; y < height; y++) {
section.setRGB(position, height - y - 1, color(volume.at(x, y, z)));
}
}
File file = new File(output, alongX ? "generated-section-z-" + selectedZ + ".png"
File file = new File(configuration.output(), alongX ? "generated-section-z-" + selectedZ + ".png"
: "generated-section-x-" + selectedX + ".png");
ImageIO.write(section, "png", file);
artifacts.add(file.getAbsolutePath());
@@ -488,42 +358,16 @@ public final class HydrologyBlockProbe {
return List.copyOf(artifacts);
}
private static int color(NativeBlockState block) {
if (block == null || block.isAir()) {
return 0xC9E3F0;
}
if (water(block)) {
private static int color(byte voxel) {
if (HydrologyVoxels.water(voxel)) {
return 0x247AC4;
}
String key = key(block);
if (key.equals("minecraft:lava")) {
if (HydrologyVoxels.fluid(voxel)) {
return 0xEC6F20;
}
if (key.equals("minecraft:sugar_cane")) {
if (voxel == HydrologyVoxels.CANE) {
return 0x9BDD42;
}
if (key.contains("grass") || key.contains("moss") || key.contains("leaves")) {
return 0x4E7845;
}
if (key.contains("sand")) {
return 0xDCCFA5;
}
if (key.contains("dirt") || key.contains("mud")) {
return 0x82634D;
}
return 0x888C90;
}
private static boolean water(NativeBlockState block) {
return block != null && (block.isWater() || block.isWaterLogged() || IMPLICIT_WATER_BLOCKS.contains(key(block)));
}
private static String key(NativeBlockState block) {
if (block == null) {
return "minecraft:air";
}
String key = block.key();
int bracket = key.indexOf('[');
return bracket < 0 ? key : key.substring(0, bracket);
return voxel == HydrologyVoxels.SOLID ? 0x888C90 : 0xC9E3F0;
}
}
@@ -0,0 +1,125 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanners;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.plan.HydrologyPlanCache;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.RegionKey;
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
final class HydrologyColdRuns {
private HydrologyColdRuns() {
}
record ColdRun(long nanos, long naturalSamples, long baseSamples, int depth) {
static final ColdRun FAILED = new ColdRun(-1L, 0L, 0L, 0);
double millis() {
return nanos / 1_000_000D;
}
}
static ColdRun cold(HydrologyProbe.Inputs inputs, WindowKey key) {
HydrologySettings settings = inputs.settings();
HydrologyPlanHarness.CountingTerrain terrain = new HydrologyPlanHarness.CountingTerrain(inputs.terrain());
HydrologyPlanners fresh = HydrologyPlanners.create(settings, terrain);
int chunkX = key.wx() * HydrologyWindows.CHUNKS + HydrologyWindows.CHUNKS / 2;
int chunkZ = key.wz() * HydrologyWindows.CHUNKS + HydrologyWindows.CHUNKS / 2;
long[] demandedSamples = new long[2];
PlanGauge gauge = new PlanGauge(new HashSet<>(HydrologyWindows.neighbourhood(key)), () -> {
demandedSamples[0] = terrain.natural();
demandedSamples[1] = terrain.base();
});
HydrologyPlanCache cache = new HydrologyPlanCache(settings, gauge.region(fresh.regions()::plan), gauge.window(fresh.windows()::plan),
inputs.planningPool(), () -> false);
HydrologyRuntime runtime = new HydrologyRuntime(settings, cache, inputs.naturalHeight(), () -> false, System::nanoTime);
long elapsed;
try {
long started = System.nanoTime();
runtime.raster(chunkX, chunkZ);
elapsed = System.nanoTime() - started;
} finally {
runtime.close();
gauge.awaitIdle(inputs.planningPool());
}
return new ColdRun(elapsed, demandedSamples[0], demandedSamples[1], gauge.deepest());
}
static final class PlanGauge {
private final Map<Object, Integer> planning = new ConcurrentHashMap<>();
private final AtomicInteger active = new AtomicInteger();
private final AtomicInteger deepest = new AtomicInteger();
private final Set<WindowKey> demanded;
private final AtomicInteger demandedLeft;
private final Runnable demandedPlanned;
PlanGauge(Set<WindowKey> demanded, Runnable demandedPlanned) {
this.demanded = Set.copyOf(demanded);
this.demandedLeft = new AtomicInteger(demanded.size());
this.demandedPlanned = demandedPlanned;
}
Function<RegionKey, RegionPlan> region(Function<RegionKey, RegionPlan> planner) {
return key -> run(key, () -> planner.apply(key));
}
BiFunction<WindowKey, List<RegionPlan>, WindowPlan> window(BiFunction<WindowKey, List<RegionPlan>, WindowPlan> planner) {
return (key, plans) -> run(key, () -> planner.apply(key, plans));
}
private <T> T run(Object key, Supplier<T> plan) {
if (awaitedByRunningPlan(key)) {
deepest.accumulateAndGet(1, Math::max);
}
planning.merge(key, 1, Integer::sum);
active.incrementAndGet();
try {
T result = plan.get();
if (key instanceof WindowKey window && demanded.contains(window) && demandedLeft.decrementAndGet() == 0) {
demandedPlanned.run();
}
return result;
} finally {
planning.computeIfPresent(key, (ignored, count) -> count == 1 ? null : count - 1);
active.decrementAndGet();
}
}
private boolean awaitedByRunningPlan(Object key) {
if (!(key instanceof RegionKey region)) {
return false;
}
for (Object running : planning.keySet()) {
if (running instanceof WindowKey window && WindowPlanner.lakeRegions(window).contains(region)) {
return true;
}
}
return false;
}
int deepest() {
return deepest.get();
}
void awaitIdle(ForkJoinPool pool) {
while (active.get() > 0 || !pool.awaitQuiescence(50L, TimeUnit.MILLISECONDS)) {
Thread.onSpinWait();
}
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,159 +0,0 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyTile;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.runtime.Engine;
import com.sun.management.OperatingSystemMXBean;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
/**
* Plans a rectangle of hydrology tiles of a real pack and prints one digest per tile (the prepared-plan
* encoding, owner resolution included) with the planning CPU. Two builds plan identically when every
* digest matches; an expected-digest file makes the probe fail on the first difference. With a pregen
* radius the tiles are planned the way a pregeneration of that radius plans them: prepared-plan store,
* pregeneration scope and speculative roots.
*/
public final class HydrologyPlanDigestProbe {
private static final String LOG_PREFIX = "[hydroplan]";
private HydrologyPlanDigestProbe() {
}
public static void main(String[] arguments) throws Exception {
if (arguments.length < 7) {
throw new IllegalArgumentException("Expected: <pack> <dimension> <seed> <minimumTileX> <maximumTileX> "
+ "<minimumTileZ> <maximumTileZ> [digestOut|-] [expectedDigests|-] [pregenRadius]");
}
File pack = new File(arguments[0]);
String dimension = arguments[1];
long seed = Long.parseLong(arguments[2]);
int minimumTileX = Integer.parseInt(arguments[3]);
int maximumTileX = Integer.parseInt(arguments[4]);
int minimumTileZ = Integer.parseInt(arguments[5]);
int maximumTileZ = Integer.parseInt(arguments[6]);
Path output = arguments.length > 7 && !arguments[7].equals("-") ? Path.of(arguments[7]) : null;
List<String> expected = arguments.length > 8 && !arguments[8].equals("-") ? Files.readAllLines(Path.of(arguments[8])) : null;
Integer pregenRadius = arguments.length > 9 ? Integer.valueOf(arguments[9]) : null;
List<HydrologyTileKey> keys = nearestFirst(minimumTileX, maximumTileX, minimumTileZ, maximumTileZ);
OperatingSystemMXBean system = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
ArrayList<String> lines = new ArrayList<>(keys.size());
int mismatches;
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(pack, dimension, LOG_PREFIX)) {
try (RealPackProbeSupport.EngineSession session = workspace.openEngine(seed, false, "plan-" + seed)) {
Engine engine = session.engine();
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
if (runtime == null) {
throw new IllegalStateException("Dimension '" + dimension + "' has no active hydrology runtime.");
}
Path store = pregenRadius == null ? null : Files.createTempDirectory("iris-hydroplan-store-");
try {
if (store != null) {
runtime.enableSharedCache("probe", store);
}
mismatches = plan(runtime, keys, pregenRadius, expected, lines, system);
} finally {
if (store != null) {
delete(store);
}
}
}
}
if (output != null) {
Files.write(output, lines, StandardCharsets.UTF_8);
}
System.exit(mismatches == 0 ? 0 : 1);
}
private static int plan(IrisHydrologyRuntime runtime, List<HydrologyTileKey> keys, Integer pregenRadius,
List<String> expected, List<String> lines,
OperatingSystemMXBean system) throws Exception {
int mismatches = 0;
long cpuStart = system.getProcessCpuTime();
long wallStart = System.nanoTime();
try (HydrologyTileCache.PregenerationScope ignored = pregenRadius == null ? null
: runtime.preparePregeneration(pregenerationArea(pregenRadius))) {
for (HydrologyTileKey key : keys) {
long tileCpu = system.getProcessCpuTime();
long tileWall = System.nanoTime();
HydrologyTile tile = runtime.tile(key);
String line = key.tileX() + "," + key.tileZ() + " " + digest(tile);
System.out.printf(Locale.ROOT, "%s tile %s courses=%d columns=%d wall_ms=%d cpu_ms=%d%n", LOG_PREFIX,
line, tile.courses().size(), tile.footprint().size(),
(System.nanoTime() - tileWall) / 1_000_000L, (system.getProcessCpuTime() - tileCpu) / 1_000_000L);
if (expected != null && !expected.contains(line)) {
mismatches++;
System.out.println(LOG_PREFIX + " MISMATCH " + line);
}
lines.add(line);
}
}
System.out.printf(Locale.ROOT, "%s RESULT tiles=%d wall_ms=%d cpu_ms=%d mismatches=%d%n", LOG_PREFIX,
keys.size(), (System.nanoTime() - wallStart) / 1_000_000L,
(system.getProcessCpuTime() - cpuStart) / 1_000_000L, mismatches);
return mismatches;
}
private static void delete(Path root) throws Exception {
try (Stream<Path> paths = Files.walk(root)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
/** The region-aligned area a square pregeneration of the radius covers, as the pregenerator reports it. */
static HydrologyTileCache.PregenerationArea pregenerationArea(int radius) {
long minimum = Math.floorDiv(-radius, 512) * 512L;
long maximum = (Math.floorDiv(radius, 512) + 1L) * 512L - 1L;
return new HydrologyTileCache.PregenerationArea(0, 0, minimum, minimum, maximum, maximum);
}
private static List<HydrologyTileKey> nearestFirst(int minimumTileX, int maximumTileX, int minimumTileZ, int maximumTileZ) {
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
keys.add(new HydrologyTileKey(tileX, tileZ));
}
}
keys.sort(Comparator.comparingLong((HydrologyTileKey key) -> distanceSquared(key))
.thenComparingInt(HydrologyTileKey::tileZ).thenComparingInt(HydrologyTileKey::tileX));
return keys;
}
private static long distanceSquared(HydrologyTileKey key) {
long x = key.tileX() * 2L + 1L;
long z = key.tileZ() * 2L + 1L;
return x * x + z * z;
}
private static String digest(HydrologyTile tile) throws Exception {
Class<?> codecType = Class.forName("art.arcane.iris.generation.hydrology.HydrologyTileCodec");
Constructor<?> constructor = codecType.getDeclaredConstructor();
constructor.setAccessible(true);
Method write = codecType.getDeclaredMethod("write", DataOutputStream.class, HydrologyTile.class, String.class);
write.setAccessible(true);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
write.invoke(constructor.newInstance(), output, tile, "probe");
}
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes.toByteArray()));
}
}
@@ -0,0 +1,312 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanners;
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
import art.arcane.iris.generation.hydrology.plan.HydrologyPlanCodec;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.IndexedWindow;
import art.arcane.iris.generation.hydrology.plan.RegionKey;
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
import art.arcane.iris.generation.hydrology.water.Pond;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.IntBinaryOperator;
final class HydrologyPlanHarness {
private static final double TILE_AREA = 1024D * 1024D;
private static final Comparator<RegionKey> REGION_ORDER = Comparator.comparingInt(RegionKey::rz).thenComparingInt(RegionKey::rx);
private HydrologyPlanHarness() {
}
static final class Plans {
private final HydrologySettings settings;
private final IntBinaryOperator naturalHeight;
private final HydrologyWindows.WindowRange planned;
private final CountingTerrain counting;
private final HydrologyPlanners planners;
private final ChunkRasterizer rasterizer;
private final Map<RegionKey, RegionPlan> regions = new HashMap<>();
private final Map<RegionKey, Double> regionCpu = new HashMap<>();
private final Map<WindowKey, IndexedWindow> windows = new HashMap<>();
private final Map<WindowKey, Double> windowCpu = new HashMap<>();
private final List<String> failures = new ArrayList<>();
Plans(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, HydrologyWindows.WindowRange range) {
this.settings = settings;
this.naturalHeight = naturalHeight;
this.planned = range.grown(1);
this.counting = new CountingTerrain(terrain);
this.planners = HydrologyPlanners.create(settings, counting);
this.rasterizer = new ChunkRasterizer(settings);
Set<RegionKey> regionKeys = new TreeSet<>(REGION_ORDER);
for (WindowKey key : planned.keys()) {
regionKeys.addAll(WindowPlanner.lakeRegions(key));
}
for (RegionKey key : regionKeys) {
double started = cpuMillis();
try {
regions.put(key, planners.regions().plan(key));
} catch (RuntimeException failure) {
failures.add("region " + key.rx() + "," + key.rz() + " plan: " + cause(failure));
}
regionCpu.put(key, cpuMillis() - started);
}
for (WindowKey key : planned.keys()) {
List<RegionPlan> needed = new ArrayList<>();
for (RegionKey region : WindowPlanner.lakeRegions(key)) {
needed.add(regions.get(region));
}
if (needed.contains(null)) {
failures.add("window " + key.wx() + "," + key.wz() + " plan: a region plan it needs failed");
continue;
}
double started = cpuMillis();
try {
WindowPlan plan = planners.windows().plan(key, needed);
windows.put(key, new IndexedWindow(plan, ReachIndex.build(plan.reaches(), plan.ponds())));
} catch (RuntimeException failure) {
failures.add("window " + key.wx() + "," + key.wz() + " plan: " + cause(failure));
}
windowCpu.put(key, cpuMillis() - started);
}
}
List<String> failures() {
return List.copyOf(failures);
}
String digest(WindowKey key) {
IndexedWindow window = windows.get(key);
return window == null ? "failed" : digest(List.of(HydrologyPlanCodec.encode(settings, window.plan())));
}
String digest() {
List<byte[]> encoded = new ArrayList<>();
for (WindowKey key : planned.keys()) {
IndexedWindow window = windows.get(key);
encoded.add(window == null ? new byte[0] : HydrologyPlanCodec.encode(settings, window.plan()));
}
for (RegionKey key : regionKeys()) {
encoded.add(HydrologyPlanCodec.encode(settings, regions.get(key)));
}
return digest(encoded);
}
private static String digest(List<byte[]> encoded) {
try {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
for (byte[] bytes : encoded) {
sha.update(bytes);
}
return HexFormat.of().formatHex(sha.digest(), 0, 8);
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException("SHA-256 is unavailable.", failure);
}
}
private List<RegionKey> regionKeys() {
List<RegionKey> keys = new ArrayList<>(regions.keySet());
keys.sort(REGION_ORDER);
return keys;
}
IndexedWindow window(WindowKey key) {
return Objects.requireNonNull(windows.get(key), "window plan " + key);
}
RegionPlan region(RegionKey key) {
return Objects.requireNonNull(regions.get(key), "region plan " + key);
}
Map<WindowKey, WindowPlan> windowPlans() {
Map<WindowKey, WindowPlan> plans = new LinkedHashMap<>();
for (WindowKey key : planned.keys()) {
IndexedWindow window = windows.get(key);
if (window != null) {
plans.put(key, window.plan());
}
}
return plans;
}
List<Pond> lakes() {
List<Pond> lakes = new ArrayList<>();
for (RegionKey key : regionKeys()) {
lakes.addAll(regions.get(key).lakes());
}
return lakes;
}
double windowCpu(WindowKey key) {
return windowCpu.getOrDefault(key, 0D);
}
double regionCpu(RegionKey key) {
return regionCpu.getOrDefault(key, 0D);
}
double meanRegionCpu() {
double total = 0D;
for (double cpu : regionCpu.values()) {
total += cpu;
}
return regionCpu.isEmpty() ? 0D : total / regionCpu.size();
}
int regionCount() {
return regions.size();
}
int windowCount() {
return windows.size();
}
long naturalSamples() {
return counting.natural();
}
long baseSamples() {
return counting.base();
}
double landTiles(WindowKey key) {
int cells = WindowKey.SIZE / WindowPlanner.CELL;
HydrologySampleBuffer samples = new HydrologySampleBuffer(cells * cells);
planners.windowLattice().fill(key.wx() * cells, key.wz() * cells, cells, cells, samples);
int land = 0;
for (int cell = 0; cell < cells * cells; cell++) {
if (!samples.ocean()[cell]) {
land++;
}
}
return land * (double) (WindowPlanner.CELL * WindowPlanner.CELL) / TILE_AREA;
}
ChunkWaterRaster rasterize(int chunkX, int chunkZ) {
WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4);
List<IndexedWindow> needed = new ArrayList<>(9);
for (WindowKey key : HydrologyWindows.neighbourhood(own)) {
IndexedWindow window = windows.get(key);
if (window == null) {
throw new IllegalStateException("Chunk " + chunkX + "," + chunkZ + " needs window " + key + ", which the probe did not plan.");
}
needed.add(window);
}
List<RegionPlan> lakes = new ArrayList<>();
for (RegionKey region : ChunkRasterizer.lakeRegions(chunkX, chunkZ)) {
lakes.add(Objects.requireNonNull(regions.get(region), "region plan " + region));
}
return rasterizer.rasterize(chunkX, chunkZ, naturalHeight, needed, lakes);
}
HydrologySettings settings() {
return settings;
}
}
static final class CountingTerrain implements HydrologyTerrainSource {
private final HydrologyTerrainSource delegate;
private final AtomicLong natural = new AtomicLong();
private final AtomicLong base = new AtomicLong();
CountingTerrain(HydrologyTerrainSource delegate) {
this.delegate = Objects.requireNonNull(delegate, "delegate");
}
@Override
public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) {
(baseOnly ? base : natural).addAndGet(xs.length);
delegate.sample(xs, zs, out, baseOnly);
}
@Override
public int seaLevel() {
return delegate.seaLevel();
}
@Override
public int minY() {
return delegate.minY();
}
@Override
public int maxY() {
return delegate.maxY();
}
long natural() {
return natural.get();
}
long base() {
return base.get();
}
}
static final class RasterCache implements HydrologyRasterMetrics.Rasters {
private final int capacity;
private final BiFunction<Integer, Integer, ChunkWaterRaster> compute;
private final LinkedHashMap<Long, ChunkWaterRaster> entries;
RasterCache(int capacity, BiFunction<Integer, Integer, ChunkWaterRaster> compute) {
this.capacity = capacity;
this.compute = compute;
this.entries = new LinkedHashMap<>(capacity * 2, 0.75f, true);
}
@Override
public ChunkWaterRaster get(int chunkX, int chunkZ) {
long key = HydrologyVoxels.chunkKey(chunkX, chunkZ);
synchronized (entries) {
ChunkWaterRaster known = entries.get(key);
if (known != null) {
return known;
}
}
ChunkWaterRaster raster = compute.apply(chunkX, chunkZ);
synchronized (entries) {
entries.put(key, raster);
if (entries.size() > capacity) {
Long eldest = entries.keySet().iterator().next();
entries.remove(eldest);
}
}
return raster;
}
}
static String cause(Throwable failure) {
Throwable root = failure;
while (root.getCause() != null && root.getCause() != root) {
root = root.getCause();
}
return root.getClass().getSimpleName() + ": " + root.getMessage();
}
static double cpuMillis() {
return ((OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean()).getProcessCpuTime() / 1_000_000D;
}
}
@@ -0,0 +1,295 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.water.Reach;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
final class HydrologyPlanMetrics {
static final float RIVER_LENGTH = 384f;
static final int PERCHED_HEIGHT = 8;
private static final int PERCHED_SEARCH = 1024;
private static final int VERTEX_BUCKET = 64;
private final HydrologySettings settings;
private final Map<WindowKey, WindowPlan> windows;
private final List<Pond> lakes;
private final HydrologyWindows.WindowRange range;
private final Consumer<String> examples;
private final Map<Long, Reach> reaches = new LinkedHashMap<>();
private final Map<Long, WindowKey> reachOwners = new HashMap<>();
private final Map<Long, WindowKey> pondOwners = new HashMap<>();
private final Map<WindowKey, Long> seams = new HashMap<>();
private final Map<Long, Pond> endPonds = new HashMap<>();
private final Map<Long, Set<Long>> pondReaches = new HashMap<>();
private final Map<WindowKey, Integer> rivers;
private final VertexIndex vertices;
HydrologyPlanMetrics(HydrologySettings settings, Map<WindowKey, WindowPlan> windows, List<Pond> lakes, HydrologyWindows.WindowRange range,
Consumer<String> examples) {
this.settings = settings;
this.windows = windows;
this.lakes = List.copyOf(lakes);
this.range = range;
this.examples = examples;
for (Map.Entry<WindowKey, WindowPlan> entry : windows.entrySet()) {
WindowKey key = entry.getKey();
for (Reach reach : entry.getValue().reaches()) {
reaches.putIfAbsent(reach.id(), reach);
WindowKey owner = reachOwners.putIfAbsent(reach.id(), key);
if (owner != null) {
seam(key, "reach " + reach.id() + " owned by " + owner + " and " + key);
}
}
for (Pond pond : entry.getValue().ponds()) {
WindowKey owner = pondOwners.putIfAbsent(pond.id(), key);
if (owner != null) {
seam(key, "pond " + pond.id() + " owned by " + owner + " and " + key);
}
}
}
linkSeams();
pondSeams();
endPonds();
this.rivers = stems();
this.vertices = new VertexIndex(reaches.values());
}
Map<Long, Reach> reaches() {
return reaches;
}
Pond endPond(Reach reach) {
return endPonds.get(reach.id());
}
long seams(WindowKey key) {
return seams.getOrDefault(key, 0L);
}
int rivers(WindowKey key) {
return rivers.getOrDefault(key, 0);
}
double streams(WindowKey key) {
double streams = 0D;
for (Reach reach : windows.get(key).reaches()) {
if (!river(reach)) {
streams += length(reach);
}
}
return streams;
}
int ponds(WindowKey key) {
return windows.get(key).ponds().size();
}
Set<Integer> levels(WindowKey key) {
Set<Integer> levels = new TreeSet<>();
for (Pond pond : windows.get(key).ponds()) {
levels.add(pond.level());
}
return levels;
}
int perched(WindowKey key) {
int perched = 0;
for (Pond pond : windows.get(key).ponds()) {
Set<Long> connected = pondReaches.getOrDefault(pond.id(), Set.of());
int grade = vertices.nearestGrade(pond.floorX(), pond.floorZ(), settings.seaLevel(), connected);
if (pond.level() >= grade + PERCHED_HEIGHT) {
perched++;
}
}
return perched;
}
private void seam(WindowKey owner, String what) {
if (range.contains(owner)) {
seams.merge(owner, 1L, Long::sum);
examples.accept("seam " + what);
}
}
private void linkSeams() {
for (Reach reach : reaches.values()) {
if (reach.downstreamId() != 0L && !reachOwners.containsKey(reach.downstreamId())) {
seam(reachOwners.get(reach.id()), "reach " + reach.id() + " links to missing reach " + reach.downstreamId());
}
}
for (Map.Entry<WindowKey, WindowPlan> entry : windows.entrySet()) {
for (Pond pond : entry.getValue().ponds()) {
if (pond.outletReachId() != 0L && !reachOwners.containsKey(pond.outletReachId())) {
seam(entry.getKey(), "pond " + pond.id() + " drains to missing reach " + pond.outletReachId());
}
}
}
}
private void pondSeams() {
List<WindowKey> keys = new ArrayList<>(windows.keySet());
for (WindowKey key : keys) {
for (WindowKey other : keys) {
if (other.wx() < key.wx() - 1 || other.wx() > key.wx() + 1 || other.wz() < key.wz() - 1 || other.wz() > key.wz() + 1
|| compare(other, key) <= 0) {
continue;
}
for (Pond pond : windows.get(key).ponds()) {
for (Pond neighbour : windows.get(other).ponds()) {
if (pond.mask().overlaps(neighbour.mask())) {
seam(range.contains(other) ? other : key, "ponds " + pond.id() + " and " + neighbour.id() + " from " + key
+ " and " + other + " overlap");
}
}
}
}
}
}
private static int compare(WindowKey left, WindowKey right) {
return left.wz() != right.wz() ? Integer.compare(left.wz(), right.wz()) : Integer.compare(left.wx(), right.wx());
}
private void endPonds() {
List<Pond> ponds = new ArrayList<>();
for (WindowPlan plan : windows.values()) {
ponds.addAll(plan.ponds());
}
ponds.addAll(lakes);
for (Pond pond : ponds) {
if (pond.outletReachId() != 0L) {
pondReaches.computeIfAbsent(pond.id(), ignored -> new HashSet<>()).add(pond.outletReachId());
}
}
for (Reach reach : reaches.values()) {
if (reach.downstreamId() != 0L) {
continue;
}
int last = reach.xs().length - 1;
int x = (int) Math.floor(reach.xs()[last]);
int z = (int) Math.floor(reach.zs()[last]);
for (Pond pond : ponds) {
if (pond.mask().contains(x, z)) {
endPonds.put(reach.id(), pond);
pondReaches.computeIfAbsent(pond.id(), ignored -> new HashSet<>()).add(reach.id());
break;
}
}
}
}
private Reach onward(Reach reach) {
if (reach.downstreamId() != 0L) {
return reaches.get(reach.downstreamId());
}
Pond pond = endPonds.get(reach.id());
return pond == null ? null : reaches.get(pond.outletReachId());
}
private Map<WindowKey, Integer> stems() {
Map<Long, Reach> mainFeeder = new HashMap<>();
for (Reach reach : reaches.values()) {
Reach next = onward(reach);
if (!river(reach) || next == null) {
continue;
}
mainFeeder.merge(next.id(), reach, (current, candidate) -> candidate.catchment() > current.catchment()
|| candidate.catchment() == current.catchment() && candidate.id() < current.id() ? candidate : current);
}
Map<WindowKey, Integer> counts = new HashMap<>();
for (Reach reach : reaches.values()) {
if (!river(reach)) {
continue;
}
Reach next = onward(reach);
if (next != null && river(next) && mainFeeder.get(next.id()) == reach) {
continue;
}
float length = 0f;
Set<Long> seen = new HashSet<>();
for (Reach stem = reach; stem != null && seen.add(stem.id()); stem = mainFeeder.get(stem.id())) {
length += length(stem);
}
WindowKey owner = reachOwners.get(reach.id());
if (length >= RIVER_LENGTH && range.contains(owner)) {
counts.merge(owner, 1, Integer::sum);
}
}
return counts;
}
private boolean river(Reach reach) {
return !reach.tier().equals(settings.tiers().get(0));
}
static float length(Reach reach) {
float length = 0f;
for (int point = 1; point < reach.xs().length; point++) {
length += (float) Math.hypot(reach.xs()[point] - reach.xs()[point - 1], reach.zs()[point] - reach.zs()[point - 1]);
}
return length;
}
private static final class VertexIndex {
private final Map<Long, List<long[]>> buckets = new HashMap<>();
private VertexIndex(Iterable<Reach> reaches) {
for (Reach reach : reaches) {
for (int point = 0; point < reach.xs().length; point++) {
int x = (int) Math.floor(reach.xs()[point]);
int z = (int) Math.floor(reach.zs()[point]);
buckets.computeIfAbsent(bucket(Math.floorDiv(x, VERTEX_BUCKET), Math.floorDiv(z, VERTEX_BUCKET)), ignored -> new ArrayList<>())
.add(new long[]{x, z, reach.grade()[point], reach.id()});
}
}
}
private int nearestGrade(int x, int z, int fallback, Set<Long> excluded) {
int bx = Math.floorDiv(x, VERTEX_BUCKET);
int bz = Math.floorDiv(z, VERTEX_BUCKET);
long best = Long.MAX_VALUE;
int grade = fallback;
int rings = PERCHED_SEARCH / VERTEX_BUCKET;
for (int ring = 0; ring <= rings; ring++) {
long reach = (long) Math.max(0, ring - 1) * VERTEX_BUCKET;
if (best != Long.MAX_VALUE && best < reach * reach) {
break;
}
for (int dz = -ring; dz <= ring; dz++) {
for (int dx = -ring; dx <= ring; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != ring) {
continue;
}
for (long[] vertex : buckets.getOrDefault(bucket(bx + dx, bz + dz), List.of())) {
if (excluded.contains(vertex[3])) {
continue;
}
long distance = (vertex[0] - x) * (vertex[0] - x) + (vertex[1] - z) * (vertex[1] - z);
if (distance < best) {
best = distance;
grade = (int) vertex[2];
}
}
}
}
}
return grade;
}
private static long bucket(int bx, int bz) {
return ((long) bx << 32) | (bz & 0xFFFFFFFFL);
}
}
}
@@ -1,629 +0,0 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.generation.hydrology.HydrologyTile;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.cave.CavePosition;
import art.arcane.iris.generation.hydrology.cave.CaveVoxel;
import art.arcane.iris.generation.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 long COASTAL_SEED = 7014L;
private static final long WATERFALL_SEED = 7015L;
static final Set<HydrologyFeatureType> REQUIRED_FEATURE_TYPES =
// The synthetic probe settings define no standing pools, so that type is not expected either.
Set.copyOf(EnumSet.complementOf(EnumSet.of(HydrologyFeatureType.RIDGE_BORE, HydrologyFeatureType.STANDING_POOL)));
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(),
cache.diagnosticCandidates(tile.key()).size(),
counts
);
cache.close();
}
collectAccepted(firstFeatures, surfaceInlandTile());
collectAccepted(firstFeatures, undergroundInlandTile());
collectAccepted(firstFeatures, surfaceMouthTile());
collectAccepted(firstFeatures, surfaceWaterfallTile());
collectAccepted(firstFeatures, coastalGrottoTile());
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());
collectAccepted(coverage, surfaceWaterfallTile());
collectAccepted(coverage, coastalGrottoTile());
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.copyOf(REQUIRED_FEATURE_TYPES);
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, false),
solidCaveView()
).plan(ORIGIN);
}
private static HydrologyTile undergroundInlandTile() {
return new HydrologyPlanner(
UNDERGROUND_INLAND_SEED,
inlandSettings(0D, 1D),
inlandTerrain(false, true, false),
solidCaveView()
).plan(ORIGIN);
}
static HydrologyTile surfaceMouthTile() {
return new HydrologyPlanner(
COASTAL_SEED,
settings(false),
coastalTerrain(false)
).plan(ORIGIN);
}
static HydrologyTile surfaceWaterfallTile() {
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> Math.clamp(6, request.minimum(), request.maximum());
case SURFACE_DEPTH -> Math.clamp(3, request.minimum(), request.maximum());
default -> request.minimum();
};
return new HydrologyPlanner(WATERFALL_SEED, inlandSettings(0D, 0D), inlandTerrain(true, false, true),
geometry, -4096, surface -> solidCaveView()).plan(ORIGIN);
}
private static HydrologyTile coastalGrottoTile() {
return new HydrologyPlanner(
COASTAL_SEED,
settings(true),
coastalTerrain(true),
solidCaveView()
).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,
512,
256,
32,
16,
0.5D,
12D,
0.5D,
0.1D,
1D,
0, HydrologyPlannerSettings.Regional.disabled()
),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
4,
18,
2,
4,
10,
1.5D,
HydrologyPlannerSettings.Banks.defaults()
),
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(
undergroundDensity > 0D,
undergroundSources,
68,
82,
4,
12,
2,
4,
5,
9,
true,
0
),
HydrologyPlannerSettings.Outlets.of(
false,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
true,
12,
32,
2,
4,
4
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled(),
HydrologyPlannerSettings.SurfacePolicyBounds.NONE
);
}
private static HydrologyTerrainSampler inlandTerrain(
boolean surfaceSource,
boolean undergroundSource,
boolean waterfall
) {
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) - (waterfall && x + z >= 64 ? 10 : 0);
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,
1D,
"parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("water"), List.of()
,
Double.NaN,
null, Double.NaN, true,
SurfaceRiverPolicy.INHERIT
);
};
}
private static HydrologyTerrainSampler coastalTerrain(boolean cliffCoast) {
return (int x, int z) -> {
if (x >= 208) {
return oceanTerrain();
}
int height;
double slope;
if (!cliffCoast) {
height = 100 - Math.floorDiv(x, 6);
slope = 1D;
} else if (x < 104) {
height = 100 - Math.floorDiv(x, 8);
slope = 1D;
} else {
height = 84 - Math.floorDiv(x - 104, 26);
slope = 1D;
}
boolean surfaceSource = x <= 40;
boolean undergroundSource = x <= 64;
return new HydrologyTerrainSample(
height,
slope,
false,
true,
height - 30,
height - 28,
true,
true,
surfaceSource,
surfaceSource,
undergroundSource,
undergroundSource,
0D,
surfaceSource ? 1D : 0D,
undergroundSource ? 1D : 0D,
1D,
1D,
1D,
1D,
1D,
"parent",
"surface",
"mouth",
"shore",
"bank",
"flooded",
List.of("water"), List.of()
,
Double.NaN,
null, Double.NaN, true,
SurfaceRiverPolicy.INHERIT
);
};
}
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,
1_089,
256,
32,
16,
0.5D,
12D,
0.5D,
0.2D,
1D,
0, HydrologyPlannerSettings.Regional.disabled()
),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
4,
20,
2,
5,
12,
1.5D,
HydrologyPlannerSettings.Banks.defaults()
),
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(
true,
undergroundSources,
68,
82,
4,
14,
2,
5,
6,
12,
true,
0
),
HydrologyPlannerSettings.Outlets.of(
true,
new HydrologyPlannerSettings.Grotto(coastalGrottosEnabled, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
true,
12,
48,
2,
8,
8
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(deepFluid), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled(),
HydrologyPlannerSettings.SurfacePolicyBounds.NONE
);
}
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,
1D,
"parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("water"), List.of(),
Double.NaN,
null,
Double.NaN,
true,
SurfaceRiverPolicy.INHERIT
);
};
}
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,
1D,
"ocean_parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("water"), List.of()
,
Double.NaN,
null, Double.NaN, true,
SurfaceRiverPolicy.INHERIT
);
}
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,224 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
import art.arcane.iris.generation.hydrology.engine.HydrologyBinding;
import art.arcane.iris.generation.hydrology.engine.IrisHydrologyTerrainSource;
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.function.IntBinaryOperator;
public final class HydrologyProbe {
private static final int RASTER_CACHE = 8192;
private static final int WARMUP_OFFSET = 8;
private static final int MAXIMUM_WINDOWS = 256;
private HydrologyProbe() {
}
record Configuration(HydrologyWindows.Target target, int threads) {
Configuration {
Objects.requireNonNull(target, "target");
if (target.windows().count() > MAXIMUM_WINDOWS) {
throw new IllegalArgumentException("The probe measures at most " + MAXIMUM_WINDOWS + " windows, got " + target.windows().count() + ".");
}
if (threads < 1) {
throw new IllegalArgumentException("Threads must be positive.");
}
}
static Configuration parse(String[] args) {
if (args.length != 6) {
throw new IllegalArgumentException("Expected <pack> <dimension|-> <seed> <x0..x1,z0..z1> <threads> <studio>.");
}
return new Configuration(HydrologyWindows.Target.parse(args[0], args[1], args[2], args[3], args[5]), Integer.parseInt(args[4]));
}
}
record Inputs(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, ForkJoinPool planningPool,
ExecutorService workers, HydrologyVoxels.ChunkSource blocks, HydrologyUnderground.Source underground, int worldHeight) {
Inputs {
Objects.requireNonNull(settings, "settings");
Objects.requireNonNull(terrain, "terrain");
Objects.requireNonNull(naturalHeight, "naturalHeight");
Objects.requireNonNull(planningPool, "planningPool");
Objects.requireNonNull(workers, "workers");
Objects.requireNonNull(blocks, "blocks");
Objects.requireNonNull(underground, "underground");
}
}
public static void main(String[] args) {
int exitCode = 2;
try {
HydrologyReport.Result result = run(Configuration.parse(args));
exitCode = result.passes() ? 0 : 1;
} catch (Throwable failure) {
failure.printStackTrace(System.out);
}
System.exit(exitCode);
}
static HydrologyReport.Result run(Configuration configuration) throws Exception {
StubPlatform.vanillaSolidity(true);
HydrologyWindows.Target target = configuration.target();
try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace(target.pack(), target.dimension(), "[hydrology]");
RealPackProbeSupport.EngineSession session = workspace.openEngine(target.seed(), target.studio(), "hydrology")) {
Engine engine = session.engine();
IrisDimension dimension = engine.getDimension();
IrisHydrology config = dimension.getHydrology();
if (config == null || !config.isEnabled()) {
throw new IllegalStateException("The dimension " + target.dimension() + " must enable hydrology.");
}
HydrologySettings settings = HydrologySettings.compile(config, engine.getSeedManager().getBodies(), dimension.getFluidHeight(), 0,
engine.getHeight());
IrisHydrologyTerrainSource terrain = new IrisHydrologyTerrainSource(engine.getComplex(), dimension, settings, MultiBurst.hydrology,
MultiBurst.hydrology.parallelism(), HydrologyBinding.UNBOUND);
ForkJoinPool planningPool = MultiBurst.hydrology.submit((Callable<ForkJoinPool>) ForkJoinTask::getPool).get();
ExecutorService workers = Executors.newFixedThreadPool(configuration.threads());
try {
Inputs inputs = new Inputs(settings, terrain, terrain::naturalHeight, planningPool, workers,
(chunkX, chunkZ) -> HydrologyVoxels.generate(engine, chunkX, chunkZ), HydrologyUnderground.generated(engine, workers),
engine.getTarget().getHeight());
HydrologyReport.Result measured = measure(inputs, target.windows());
List<Throwable> reports = RealPackProbeSupport.drainReported();
RealPackProbeSupport.printReports("[hydrology]", "generation", reports);
HydrologyReport.Result result = measured.withGenerationErrors(reports.size());
for (String line : result.lines()) {
System.out.println(line);
}
return result;
} finally {
workers.shutdownNow();
}
}
}
static HydrologyReport.Result measure(Inputs inputs, HydrologyWindows.WindowRange range) throws Exception {
return new Measurement(inputs, range).measure();
}
private static final class Measurement {
private final Inputs inputs;
private final HydrologyWindows.WindowRange range;
private final HydrologyReport.Examples examples = new HydrologyReport.Examples();
private final Set<String> failures = Collections.synchronizedSet(new LinkedHashSet<>());
private HydrologyPlanHarness.Plans plans;
private HydrologyPlanHarness.RasterCache rasters;
private Measurement(Inputs inputs, HydrologyWindows.WindowRange range) {
this.inputs = inputs;
this.range = range;
}
private HydrologyReport.Result measure() throws Exception {
double warmupMs = -1D;
try {
warmupMs = HydrologyColdRuns.cold(inputs, new WindowKey(range.minX() - WARMUP_OFFSET, range.minZ() - WARMUP_OFFSET)).millis();
} catch (RuntimeException failure) {
failures.add("warm-up cold run: " + HydrologyPlanHarness.cause(failure));
}
Map<WindowKey, HydrologyColdRuns.ColdRun> colds = new LinkedHashMap<>();
for (WindowKey key : range.keys()) {
try {
colds.put(key, HydrologyColdRuns.cold(inputs, key));
} catch (RuntimeException failure) {
failures.add("cold window " + key.wx() + "," + key.wz() + ": " + HydrologyPlanHarness.cause(failure));
colds.put(key, HydrologyColdRuns.ColdRun.FAILED);
}
}
plans = new HydrologyPlanHarness.Plans(inputs.settings(), inputs.terrain(), inputs.naturalHeight(), range);
rasters = new HydrologyPlanHarness.RasterCache(RASTER_CACHE, plans::rasterize);
Map<WindowKey, WindowPlan> windows = plans.windowPlans();
HydrologyPlanMetrics planMetrics = new HydrologyPlanMetrics(inputs.settings(), windows, plans.lakes(), range, examples);
HydrologyRasterMetrics rasterMetrics = new HydrologyRasterMetrics(inputs.settings(), planMetrics, rasters, inputs.naturalHeight(), range,
examples);
HydrologyBlockMetrics blockMetrics = new HydrologyBlockMetrics(inputs, rasterMetrics, range, examples, failures::add);
List<HydrologyReport.WindowMetrics> measured = new ArrayList<>();
long generationErrors = 0L;
for (WindowKey key : range.keys()) {
WindowPlan plan = windows.get(key);
if (plan == null) {
continue;
}
try {
prepareRasters(key);
HydrologyBlockMetrics.Counts blocks = blockMetrics.count(key);
generationErrors += blocks.errors();
HydrologyUnderground.Counts underground = underground(key, plan);
HydrologyColdRuns.ColdRun cold = colds.get(key);
measured.add(new HydrologyReport.WindowMetrics(key, cold.millis(), cold.naturalSamples(), cold.baseSamples(),
plans.windowCpu(key), plans.regionCpu(key.region()), cold.depth(), plans.landTiles(key), planMetrics.rivers(key),
planMetrics.streams(key), planMetrics.ponds(key), planMetrics.levels(key), planMetrics.perched(key),
blocks.scan().containmentLeaks, rasterMetrics.rising(key, plan), planMetrics.seams(key) + rasterMetrics.chunkSeams(key),
blocks.scan().tickConversionCells, blocks.scan().missingSurfaces, rasterMetrics.bermOverCap(key), underground.inlets(), underground.inletFailures(),
underground.cavePools(), underground.cavePoolFailures(), blocks.generated(), plans.digest(key)));
} catch (RuntimeException failure) {
failures.add("window " + key.wx() + "," + key.wz() + ": " + HydrologyPlanHarness.cause(failure));
}
}
failures.addAll(plans.failures());
return new HydrologyReport.Result(measured, plans.meanRegionCpu(), plans.regionCount(), plans.windowCount(), plans.naturalSamples(),
plans.baseSamples(), warmupMs, generationErrors, plans.digest(), List.copyOf(failures), examples.values());
}
private HydrologyUnderground.Counts underground(WindowKey key, WindowPlan plan) {
try {
HydrologyUnderground.Counts counts = inputs.underground().measure(key, plan);
for (String example : counts.examples()) {
examples.accept(example);
}
return counts;
} catch (Exception failure) {
failures.add("underground window " + key.wx() + "," + key.wz() + ": " + HydrologyPlanHarness.cause(failure));
return HydrologyUnderground.Counts.NONE;
}
}
private void prepareRasters(WindowKey key) throws InterruptedException {
List<Callable<ChunkWaterRaster>> tasks = new ArrayList<>();
List<String> chunks = new ArrayList<>();
int minChunkX = key.wx() * HydrologyWindows.CHUNKS - 1;
int minChunkZ = key.wz() * HydrologyWindows.CHUNKS - 1;
for (int chunkZ = minChunkZ; chunkZ <= minChunkZ + HydrologyWindows.CHUNKS + 1; chunkZ++) {
for (int chunkX = minChunkX; chunkX <= minChunkX + HydrologyWindows.CHUNKS + 1; chunkX++) {
if (range.containsChunk(chunkX, chunkZ)) {
int x = chunkX;
int z = chunkZ;
tasks.add(() -> rasters.get(x, z));
chunks.add(x + "," + z);
}
}
}
List<Future<ChunkWaterRaster>> futures = inputs.workers().invokeAll(tasks);
for (int index = 0; index < futures.size(); index++) {
try {
futures.get(index).get();
} catch (ExecutionException failure) {
failures.add("raster chunk " + chunks.get(index) + ": " + HydrologyPlanHarness.cause(failure));
}
}
}
}
}
@@ -0,0 +1,194 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import java.util.function.Consumer;
import java.util.function.IntBinaryOperator;
final class HydrologyRasterMetrics {
private final HydrologySettings settings;
private final HydrologyPlanMetrics plans;
private final Rasters rasters;
private final IntBinaryOperator naturalHeight;
private final HydrologyWindows.WindowRange range;
private final Consumer<String> examples;
@FunctionalInterface
interface Rasters {
ChunkWaterRaster get(int chunkX, int chunkZ);
}
HydrologyRasterMetrics(HydrologySettings settings, HydrologyPlanMetrics plans, Rasters rasters, IntBinaryOperator naturalHeight,
HydrologyWindows.WindowRange range, Consumer<String> examples) {
this.settings = settings;
this.plans = plans;
this.rasters = rasters;
this.naturalHeight = naturalHeight;
this.range = range;
this.examples = examples;
}
long rising(WindowKey key, WindowPlan plan) {
long rising = 0L;
for (Reach reach : plan.reaches()) {
int[] heads = heads(reach);
int previous = HydrologyVoxels.NO_WATER;
for (int point = 0; point < heads.length; point++) {
int head = heads[point];
if (head == HydrologyVoxels.NO_WATER) {
continue;
}
if (previous != HydrologyVoxels.NO_WATER && head > previous) {
rising++;
examples.accept("rise along reach " + reach.id() + " from " + previous + " to " + head);
}
if (reach.style()[point] == ReachStyle.MOUTH && head < settings.seaLevel()) {
rising++;
examples.accept("rise from the sea at " + settings.seaLevel() + " into the mouth of reach " + reach.id() + " at " + head);
}
previous = head;
}
if (previous == HydrologyVoxels.NO_WATER) {
continue;
}
Reach downstream = plans.reaches().get(reach.downstreamId());
if (downstream != null) {
int first = first(heads(downstream));
if (first != HydrologyVoxels.NO_WATER && first > previous) {
rising++;
examples.accept("rise from reach " + reach.id() + " at " + previous + " into reach " + downstream.id() + " at " + first);
}
}
Pond end = plans.endPond(reach);
if (end != null && end.level() > previous) {
rising++;
examples.accept("rise from reach " + reach.id() + " at " + previous + " into pond " + end.id() + " at " + end.level());
}
}
for (Pond pond : plan.ponds()) {
Reach outlet = plans.reaches().get(pond.outletReachId());
if (outlet != null) {
int first = first(heads(outlet));
if (first != HydrologyVoxels.NO_WATER && first > pond.level()) {
rising++;
examples.accept("rise from pond " + pond.id() + " at " + pond.level() + " into outlet " + outlet.id() + " at " + first);
}
}
}
return rising;
}
long chunkSeams(WindowKey key) {
long mismatches = 0L;
int minX = key.wx() * WindowKey.SIZE;
int minZ = key.wz() * WindowKey.SIZE;
for (int z = minZ; z < minZ + WindowKey.SIZE; z++) {
for (int x = minX; x < minX + WindowKey.SIZE; x++) {
if ((x & 15) == 15 && range.containsBlock(x + 1, z) && seamMismatch(x, z, x + 1, z)) {
mismatches++;
}
if ((z & 15) == 15 && range.containsBlock(x, z + 1) && seamMismatch(x, z, x, z + 1)) {
mismatches++;
}
}
}
return mismatches;
}
long bermOverCap(WindowKey key) {
long overCap = 0L;
int maxFill = settings.deformation().maxFill();
int minX = key.wx() * WindowKey.SIZE;
int minZ = key.wz() * WindowKey.SIZE;
for (int z = minZ; z < minZ + WindowKey.SIZE; z++) {
for (int x = minX; x < minX + WindowKey.SIZE; x++) {
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
WaterRole role = raster.role(x & 15, z & 15);
if (raster.empty() || role == WaterRole.NONE || role == WaterRole.DAM || role.wet()) {
continue;
}
int terrain = raster.terrainHeight(x & 15, z & 15);
int natural = naturalHeight.applyAsInt(x, z);
if (terrain > natural + maxFill) {
overCap++;
examples.accept("berm raised " + x + "," + z + " from " + natural + " to " + terrain);
}
}
}
return overCap;
}
WaterRole role(int x, int z) {
return rasters.get(x >> 4, z >> 4).role(x & 15, z & 15);
}
int terrain(int x, int z) {
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
return raster.empty() ? naturalHeight.applyAsInt(x, z) : raster.terrainHeight(x & 15, z & 15);
}
private boolean seamMismatch(int x, int z, int nx, int nz) {
ChunkWaterRaster near = rasters.get(x >> 4, z >> 4);
ChunkWaterRaster far = rasters.get(nx >> 4, nz >> 4);
WaterRole nearRole = near.role(x & 15, z & 15);
WaterRole farRole = far.role(nx & 15, nz & 15);
int nearHead = near.waterHead(x & 15, z & 15);
int farHead = far.waterHead(nx & 15, nz & 15);
boolean mismatch;
String what;
if (nearRole.wet() && farRole.wet()) {
mismatch = nearRole == WaterRole.POND && farRole == WaterRole.POND && near.featureId(x & 15, z & 15) == far.featureId(nx & 15, nz & 15)
&& nearHead != farHead;
what = "one pond at two levels";
} else if (nearRole.wet()) {
mismatch = unbermed(nearHead, nx, nz);
what = "dry column left below its berm";
} else if (farRole.wet()) {
mismatch = unbermed(farHead, x, z);
what = "dry column left below its berm";
} else {
return false;
}
if (mismatch) {
examples.accept("seam " + what + " across chunks at " + x + "," + z + " / " + nx + "," + nz);
}
return mismatch;
}
private boolean unbermed(int head, int x, int z) {
int ground = terrain(x, z);
return ground < head && !(head <= settings.seaLevel() && ground < settings.seaLevel());
}
private int[] heads(Reach reach) {
int[] heads = new int[reach.xs().length];
for (int point = 0; point < heads.length; point++) {
int x = (int) Math.floor(reach.xs()[point]);
int z = (int) Math.floor(reach.zs()[point]);
heads[point] = HydrologyVoxels.NO_WATER;
if (range.containsBlock(x, z)) {
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
if (raster.role(x & 15, z & 15).wet() && raster.featureId(x & 15, z & 15) == reach.id()) {
heads[point] = raster.waterHead(x & 15, z & 15);
}
}
}
return heads;
}
private static int first(int[] heads) {
for (int head : heads) {
if (head != HydrologyVoxels.NO_WATER) {
return head;
}
}
return HydrologyVoxels.NO_WATER;
}
}
@@ -0,0 +1,223 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.function.Function;
final class HydrologyReport {
static final double POND_LEVEL_TILES = 9D;
private static final int EXAMPLES_PER_KIND = 8;
private HydrologyReport() {
}
record WindowMetrics(WindowKey key, double coldFirstChunkMs, long coldNaturalSamples, long coldBaseSamples, double windowCpuMs,
double regionCpuMs, int maxDependencyDepth, double landTiles, int rivers, double streamBlocks, int ponds,
Set<Integer> pondLevels, int pondsPerched8, long containmentLeaks, long risingWaterSegments, long seamMismatches,
long tickConversionCells, long missingSurface, long bermOverCap, int inlets, long inletFailures, int cavePools, long cavePoolFailures,
long generatedChunks, String planDigest) {
String line() {
return String.format(Locale.ROOT, "IRIS_HYDROLOGY_WINDOW wx=%d wz=%d coldFirstChunkMs=%.1f windowCpuMs=%.1f regionCpuMs=%.1f "
+ "maxDependencyDepth=%d riversPerTile=%.2f streamBlocksPerTile=%.0f pondsPerTile=%.2f pondLevelsDistinct=%d "
+ "pondsPerched8=%d containmentLeaks=%d risingWaterSegments=%d seamMismatches=%d tickConversionCells=%d "
+ "missingSurface=%d bermOverCap=%d inlets=%d inletFailures=%d cavePools=%d cavePoolFailures=%d landTiles=%.3f rivers=%d ponds=%d "
+ "coldNaturalSamples=%d coldBaseSamples=%d generatedChunks=%d planDigest=%s",
key.wx(), key.wz(), coldFirstChunkMs, windowCpuMs, regionCpuMs, maxDependencyDepth, perTile(rivers, landTiles),
perTile(streamBlocks, landTiles), perTile(ponds, landTiles), pondLevels.size(), pondsPerched8, containmentLeaks,
risingWaterSegments, seamMismatches, tickConversionCells, missingSurface, bermOverCap, inlets, inletFailures, cavePools, cavePoolFailures,
landTiles, rivers, ponds, coldNaturalSamples, coldBaseSamples, generatedChunks, planDigest);
}
}
record Result(List<WindowMetrics> windows, double regionCpuMs, int regionsPlanned, int windowsPlanned, long naturalSamples,
long baseSamples, double warmupMs, long generationErrors, String planDigest, List<String> failures, List<String> examples) {
boolean passes() {
return maxDependencyDepth() == 0 && sum(WindowMetrics::containmentLeaks) == 0 && sum(WindowMetrics::risingWaterSegments) == 0
&& sum(WindowMetrics::seamMismatches) == 0 && sum(WindowMetrics::tickConversionCells) == 0
&& sum(WindowMetrics::missingSurface) == 0
&& sum(WindowMetrics::inletFailures) == 0 && sum(WindowMetrics::cavePoolFailures) == 0 && generationErrors == 0
&& failures.isEmpty();
}
Result withGenerationErrors(long errors) {
return new Result(windows, regionCpuMs, regionsPlanned, windowsPlanned, naturalSamples, baseSamples, warmupMs, generationErrors + errors,
planDigest, failures, examples);
}
int maxDependencyDepth() {
int deepest = 0;
for (WindowMetrics window : windows) {
deepest = Math.max(deepest, window.maxDependencyDepth());
}
return deepest;
}
long sum(Function<WindowMetrics, Long> field) {
long total = 0L;
for (WindowMetrics window : windows) {
total += field.apply(window);
}
return total;
}
double coldMax() {
double max = 0D;
for (WindowMetrics window : windows) {
max = Math.max(max, window.coldFirstChunkMs());
}
return max;
}
double coldMedian() {
double[] values = new double[windows.size()];
for (int index = 0; index < values.length; index++) {
values[index] = windows.get(index).coldFirstChunkMs();
}
Arrays.sort(values);
return values.length == 0 ? 0D : values[values.length / 2];
}
List<String> missedTargets() {
List<String> missed = new ArrayList<>();
if (maxDependencyDepth() != 0) {
missed.add("maxDependencyDepth");
}
if (perTile(riversTotal(), landTiles()) < 1D) {
missed.add("riversPerTile");
}
if (perTile(pondsTotal(), landTiles()) < 6D) {
missed.add("pondsPerTile");
}
if (POND_LEVEL_TILES * perTile(pondLevels().size(), landTiles()) < 6D) {
missed.add("pondLevelsDistinct");
}
for (String gate : List.of("containmentLeaks", "risingWaterSegments", "seamMismatches", "tickConversionCells", "missingSurface",
"bermOverCap", "inletFailures", "cavePoolFailures")) {
if (gate(gate) != 0L) {
missed.add(gate);
}
}
return missed;
}
private long gate(String name) {
return switch (name) {
case "containmentLeaks" -> sum(WindowMetrics::containmentLeaks);
case "risingWaterSegments" -> sum(WindowMetrics::risingWaterSegments);
case "seamMismatches" -> sum(WindowMetrics::seamMismatches);
case "tickConversionCells" -> sum(WindowMetrics::tickConversionCells);
case "missingSurface" -> sum(WindowMetrics::missingSurface);
case "bermOverCap" -> sum(WindowMetrics::bermOverCap);
case "inletFailures" -> sum(WindowMetrics::inletFailures);
case "cavePoolFailures" -> sum(WindowMetrics::cavePoolFailures);
default -> throw new IllegalArgumentException(name);
};
}
double landTiles() {
double total = 0D;
for (WindowMetrics window : windows) {
total += window.landTiles();
}
return total;
}
int riversTotal() {
int total = 0;
for (WindowMetrics window : windows) {
total += window.rivers();
}
return total;
}
int pondsTotal() {
int total = 0;
for (WindowMetrics window : windows) {
total += window.ponds();
}
return total;
}
Set<Integer> pondLevels() {
Set<Integer> levels = new TreeSet<>();
for (WindowMetrics window : windows) {
levels.addAll(window.pondLevels());
}
return levels;
}
List<String> lines() {
List<String> lines = new ArrayList<>();
for (WindowMetrics window : windows) {
lines.add(window.line());
}
double streamBlocks = 0D;
double windowCpu = 0D;
int perched = 0;
long generated = 0L;
int inlets = 0;
int cavePools = 0;
for (WindowMetrics window : windows) {
streamBlocks += window.streamBlocks();
windowCpu += window.windowCpuMs();
perched += window.pondsPerched8();
generated += window.generatedChunks();
inlets += window.inlets();
cavePools += window.cavePools();
}
List<String> missed = missedTargets();
lines.add(String.format(Locale.ROOT, "IRIS_HYDROLOGY_TOTAL version=1 status=%s windows=%d coldFirstChunkMs=%.1f "
+ "coldFirstChunkMedianMs=%.1f windowCpuMs=%.1f regionCpuMs=%.1f maxDependencyDepth=%d riversPerTile=%.2f "
+ "streamBlocksPerTile=%.0f pondsPerTile=%.2f pondLevelsDistinct=%d pondsPerched8=%d containmentLeaks=%d "
+ "risingWaterSegments=%d seamMismatches=%d tickConversionCells=%d missingSurface=%d bermOverCap=%d inlets=%d inletFailures=%d "
+ "cavePools=%d cavePoolFailures=%d landTiles=%.3f "
+ "rivers=%d ponds=%d windowsPlanned=%d regionsPlanned=%d naturalSamples=%d baseSamples=%d coldWarmupMs=%.1f "
+ "generatedChunks=%d generationErrors=%d failures=%d planDigest=%s missedTargets=%s",
passes() ? "PASS" : "FAIL", windows.size(), coldMax(), coldMedian(), windows.isEmpty() ? 0D : windowCpu / windows.size(),
regionCpuMs, maxDependencyDepth(), perTile(riversTotal(), landTiles()), perTile(streamBlocks, landTiles()),
perTile(pondsTotal(), landTiles()), pondLevels().size(), perched, sum(WindowMetrics::containmentLeaks),
sum(WindowMetrics::risingWaterSegments), sum(WindowMetrics::seamMismatches), sum(WindowMetrics::tickConversionCells),
sum(WindowMetrics::missingSurface),
sum(WindowMetrics::bermOverCap), inlets, sum(WindowMetrics::inletFailures), cavePools, sum(WindowMetrics::cavePoolFailures),
landTiles(), riversTotal(), pondsTotal(), windowsPlanned, regionsPlanned, naturalSamples,
baseSamples, warmupMs, generated, generationErrors, failures.size(), planDigest, missed.isEmpty() ? "none" : String.join(",", missed)));
for (String failure : failures) {
lines.add("IRIS_HYDROLOGY_FAILURE " + failure);
}
for (String example : examples) {
lines.add("IRIS_HYDROLOGY_EXAMPLE " + example);
}
return lines;
}
}
static final class Examples implements Consumer<String> {
private final List<String> values = new ArrayList<>();
private final Map<String, Integer> counts = new HashMap<>();
@Override
public synchronized void accept(String value) {
String kind = value.substring(0, Math.max(0, value.indexOf(' ')));
if (counts.merge(kind, 1, Integer::sum) <= EXAMPLES_PER_KIND) {
values.add(value);
}
}
synchronized List<String> values() {
return List.copyOf(values);
}
}
static double perTile(double count, double landTiles) {
return landTiles <= 0D ? 0D : count / landTiles;
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.runtime.Engine;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
final class HydrologyUnderground {
private HydrologyUnderground() {
}
@FunctionalInterface
interface Source {
Counts measure(WindowKey key, WindowPlan plan) throws Exception;
}
record Counts(int inlets, long inletFailures, int cavePools, long cavePoolFailures, List<String> examples) {
static final Counts NONE = new Counts(0, 0L, 0, 0L, List.of());
}
static Source generated(Engine engine, ExecutorService workers) {
return (key, plan) -> {
SeaInletProbe.Checks inlets = SeaInletProbe.check(engine, plan.inlets(), workers);
int minChunkX = key.wx() * HydrologyWindows.CHUNKS;
int minChunkZ = key.wz() * HydrologyWindows.CHUNKS;
CavePoolProbe.Counts pools = CavePoolProbe.check(engine, new CavePoolProbe.Area(minChunkX, minChunkX + HydrologyWindows.CHUNKS - 1,
minChunkZ, minChunkZ + HydrologyWindows.CHUNKS - 1), workers);
List<String> examples = new ArrayList<>();
for (String example : inlets.examples()) {
examples.add("inlet " + example);
}
for (String example : pools.examples()) {
examples.add("pool " + example);
}
return new Counts(inlets.inlets(), inlets.failures(), pools.pools(), pools.failures(), examples);
};
}
}
@@ -0,0 +1,369 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
final class HydrologyVoxels {
static final byte UNKNOWN = -1;
static final byte OPEN = 0;
static final byte SOLID = 1;
static final byte CANE = 2;
static final byte WATER = 16;
static final byte LAVA = 32;
static final int NO_WATER = Integer.MIN_VALUE;
static final int COLUMNS = 256;
private static final int FLUID_LEVELS = 16;
private static final int FULL_SNOW = 8;
private static final int EXAMPLES_PER_KIND = 8;
private static final int GRID = 18;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final Set<String> WATER_SOURCE_BLOCKS = Set.of("minecraft:seagrass", "minecraft:tall_seagrass", "minecraft:kelp",
"minecraft:kelp_plant", "minecraft:bubble_column");
private static final Set<String> OPEN_BLOCKS = Set.of("minecraft:short_grass", "minecraft:grass", "minecraft:tall_grass",
"minecraft:fern", "minecraft:large_fern", "minecraft:dead_bush", "minecraft:bush", "minecraft:firefly_bush",
"minecraft:short_dry_grass", "minecraft:tall_dry_grass", "minecraft:vine", "minecraft:snow", "minecraft:sweet_berry_bush",
"minecraft:cave_vines", "minecraft:cave_vines_plant", "minecraft:dandelion", "minecraft:poppy", "minecraft:blue_orchid",
"minecraft:allium", "minecraft:azure_bluet", "minecraft:oxeye_daisy", "minecraft:cornflower", "minecraft:lily_of_the_valley",
"minecraft:wither_rose", "minecraft:torchflower", "minecraft:sunflower", "minecraft:lilac", "minecraft:rose_bush",
"minecraft:peony", "minecraft:pitcher_plant", "minecraft:wildflowers", "minecraft:leaf_litter", "minecraft:cactus_flower",
"minecraft:open_eyeblossom", "minecraft:closed_eyeblossom", "minecraft:nether_sprouts", "minecraft:crimson_roots",
"minecraft:warped_roots", "minecraft:torch", "minecraft:wall_torch", "minecraft:spore_blossom", "minecraft:pink_petals",
"minecraft:tripwire", "minecraft:cobweb", "minecraft:wheat", "minecraft:carrots", "minecraft:potatoes", "minecraft:beetroots",
"minecraft:melon_stem", "minecraft:pumpkin_stem", "minecraft:attached_melon_stem", "minecraft:attached_pumpkin_stem",
"minecraft:torchflower_crop", "minecraft:pitcher_crop", "minecraft:nether_wart", "minecraft:fire", "minecraft:soul_fire",
"minecraft:redstone_wire");
private static final Map<NativeBlockState, Byte> CLASSES = new ConcurrentHashMap<>();
private HydrologyVoxels() {
}
record ChunkBlocks(int chunkX, int chunkZ, int height, byte[] voxels, int[] heads, byte[] surfaces) {
ChunkBlocks {
if (height < 1 || voxels.length != COLUMNS * height || heads.length != COLUMNS || surfaces.length != COLUMNS) {
throw new IllegalArgumentException("Chunk blocks need 256 x " + height + " voxels and 256 heads and surfaces, got "
+ voxels.length + ", " + heads.length + " and " + surfaces.length);
}
}
static ChunkBlocks empty(int chunkX, int chunkZ, int height) {
int[] heads = new int[COLUMNS];
Arrays.fill(heads, NO_WATER);
byte[] surfaces = new byte[COLUMNS];
Arrays.fill(surfaces, WATER);
return new ChunkBlocks(chunkX, chunkZ, height, new byte[COLUMNS * height], heads, surfaces);
}
byte at(int lx, int y, int lz) {
return y < 0 || y >= height ? UNKNOWN : voxels[(y << 8) | (lz << 4) | lx];
}
void set(int lx, int y, int lz, byte voxel) {
voxels[(y << 8) | (lz << 4) | lx] = voxel;
}
int head(int lx, int lz) {
return heads[(lz << 4) | lx];
}
byte surface(int lx, int lz) {
return surfaces[(lz << 4) | lx];
}
}
@FunctionalInterface
interface ChunkSource {
ChunkBlocks generate(int chunkX, int chunkZ);
}
static ChunkBlocks generate(Engine engine, int chunkX, int chunkZ) {
RealPackProbeSupport.GeneratedChunk chunk = RealPackProbeSupport.generateChunk(engine, chunkX, chunkZ);
ChunkBlocks blocks = ChunkBlocks.empty(chunk.chunkX(), chunk.chunkZ(), chunk.height());
for (int y = 0; y < chunk.height(); y++) {
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
blocks.set(lx, y, lz, classify(chunk.blocks().get(lx, y, lz)));
}
}
}
IrisComplex complex = engine.getComplex();
System.arraycopy(effectiveHeads(complex, chunkX, chunkZ), 0, blocks.heads(), 0, COLUMNS);
ChunkWaterRaster own = complex.getHydrologyRuntime().raster(chunkX, chunkZ);
MantleChunk<Matter> mantle = engine.getMantle().getMantle().useChunk(chunkX, chunkZ);
try {
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
int head = blocks.head(lx, lz);
if (head == NO_WATER) {
continue;
}
int x = (chunkX << 4) + lx;
int z = (chunkZ << 4) + lz;
blocks.surfaces()[(lz << 4) | lx] = covered(complex, own, mantle, lx, head, lz, x, z) ? SOLID
: classify(complex.resolveWaterFluid(own.fluid(lx, lz), x, z));
}
}
} finally {
mantle.release();
}
return blocks;
}
private static boolean covered(IrisComplex complex, ChunkWaterRaster raster, MantleChunk<Matter> mantle, int lx, int head, int lz, int x,
int z) {
NativeBlockState object = mantle.get(lx, head, lz, NativeBlockState.class);
if (object != null && object.isSolid()) {
return true;
}
return raster.role(lx, lz) == WaterRole.INLET
&& complex.getTrueBiomeStream().get(x, z).getSeaLayers().isNotEmpty();
}
static int[] effectiveHeads(IrisComplex complex, int chunkX, int chunkZ) {
int[] interior = new int[COLUMNS];
Arrays.fill(interior, NO_WATER);
HydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null || !runtime.enabled()) {
return interior;
}
ChunkWaterRaster[] around = new ChunkWaterRaster[9];
for (int slot = 0; slot < around.length; slot++) {
around[slot] = runtime.raster(chunkX + slot % 3 - 1, chunkZ + slot / 3 - 1);
}
int[] heads = new int[GRID * GRID];
complex.effectiveWaterHeads(chunkX, chunkZ, around, heads);
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
interior[(lz << 4) | lx] = heads[(lz + 1) * GRID + lx + 1];
}
}
return interior;
}
static final class Volume {
private final Map<Long, ChunkBlocks> chunks = new HashMap<>();
void put(ChunkBlocks blocks) {
chunks.put(chunkKey(blocks.chunkX(), blocks.chunkZ()), blocks);
}
byte at(int x, int y, int z) {
ChunkBlocks chunk = chunks.get(chunkKey(x >> 4, z >> 4));
return chunk == null ? UNKNOWN : chunk.at(x & 15, y, z & 15);
}
int head(int x, int z) {
ChunkBlocks chunk = chunks.get(chunkKey(x >> 4, z >> 4));
return chunk == null ? NO_WATER : chunk.head(x & 15, z & 15);
}
byte surface(int x, int z) {
ChunkBlocks chunk = chunks.get(chunkKey(x >> 4, z >> 4));
return chunk == null ? WATER : chunk.surface(x & 15, z & 15);
}
}
static final class WaterScan {
long waterCells;
long containmentLeaks;
long tickConversionCells;
long missingSurfaces;
long censoredCells;
final List<String> examples = new ArrayList<>();
void column(Volume volume, int x, int z, int fromY, int toY) {
int head = volume.head(x, z);
if (head != NO_WATER && !holdsSurface(volume.surface(x, z), volume.at(x, head, z))) {
missingSurfaces++;
example("missing planned surface at " + x + "," + head + "," + z);
}
for (int y = Math.max(0, fromY); y <= toY; y++) {
byte voxel = volume.at(x, y, z);
if (!fluid(voxel)) {
continue;
}
waterCells++;
byte below = volume.at(x, y - 1, z);
if (source(voxel)) {
if (open(below) || opensSideways(volume, x, y, z)) {
leak(x, y, z, open(below) ? "water over open ground" : "source beside open ground");
}
continue;
}
if (open(below)) {
leak(x, y, z, "flowing water over open ground");
} else if (y > head && converts(volume, x, y, z, voxel, below)) {
tick(x, y, z, "flowing cell converts to a source");
} else if (!sameFluid(voxel, below) && amount(voxel) > dropOff(voxel) && opensSideways(volume, x, y, z)) {
tick(x, y, z, "flowing cell spreads beside lower dry ground");
}
}
}
private static boolean holdsSurface(byte planned, byte voxel) {
return planned == SOLID ? voxel == SOLID || fluid(voxel) : sameFluid(planned, voxel);
}
private boolean opensSideways(Volume volume, int x, int y, int z) {
boolean open = false;
for (int[] offset : CARDINALS) {
byte neighbour = volume.at(x + offset[0], y, z + offset[1]);
if (neighbour == UNKNOWN) {
censoredCells++;
} else if (open(neighbour)) {
open = true;
}
}
return open;
}
private static boolean converts(Volume volume, int x, int y, int z, byte voxel, byte below) {
if (!water(voxel)) {
return false;
}
boolean supported = below == SOLID || (sameFluid(voxel, below) && source(below));
if (!supported) {
return false;
}
int sources = 0;
for (int[] offset : CARDINALS) {
byte neighbour = volume.at(x + offset[0], y, z + offset[1]);
if (sameFluid(voxel, neighbour) && source(neighbour)) {
sources++;
}
}
return sources >= 2;
}
private void leak(int x, int y, int z, String what) {
containmentLeaks++;
example("leak " + what + " at " + x + "," + y + "," + z);
}
private void tick(int x, int y, int z, String what) {
tickConversionCells++;
example("tick " + what + " at " + x + "," + y + "," + z);
}
private void example(String value) {
String kind = value.substring(0, Math.max(0, value.indexOf(' ')));
int seen = 0;
for (String example : examples) {
if (example.startsWith(kind + " ")) {
seen++;
}
}
if (seen < EXAMPLES_PER_KIND) {
examples.add(value);
}
}
}
static byte classify(NativeBlockState state) {
if (state == null || state.isAir()) {
return OPEN;
}
Byte known = CLASSES.get(state);
if (known != null) {
return known;
}
byte resolved = resolve(state);
CLASSES.putIfAbsent(state, resolved);
return resolved;
}
private static byte resolve(NativeBlockState state) {
String key = state.key();
int bracket = key.indexOf('[');
String block = bracket < 0 ? key : key.substring(0, bracket);
if (block.equals("minecraft:water")) {
return (byte) (WATER + level(key));
}
if (block.equals("minecraft:lava")) {
return (byte) (LAVA + level(key));
}
if (state.isWaterLogged() || key.contains("waterlogged=true") || WATER_SOURCE_BLOCKS.contains(block)) {
return WATER;
}
if (key.contains("waterlogged=")) {
return SOLID;
}
if (block.equals("minecraft:sugar_cane")) {
return CANE;
}
if (block.equals("minecraft:snow") && number(key, "layers=", 1) >= FULL_SNOW) {
return SOLID;
}
if (OPEN_BLOCKS.contains(block) || block.endsWith("_sapling") || block.endsWith("_tulip") || block.endsWith("_carpet")
|| block.endsWith("_mushroom") || block.endsWith("_fungus") || block.endsWith("_button") || block.endsWith("_pressure_plate")) {
return OPEN;
}
return SOLID;
}
private static int level(String key) {
return Math.min(FLUID_LEVELS - 1, number(key, "level=", 0));
}
private static int number(String key, String property, int fallback) {
int start = key.indexOf(property);
if (start < 0) {
return fallback;
}
int end = start + property.length();
while (end < key.length() && Character.isDigit(key.charAt(end))) {
end++;
}
return end == start + property.length() ? fallback : Integer.parseInt(key.substring(start + property.length(), end));
}
static boolean open(byte voxel) {
return voxel == OPEN;
}
static boolean fluid(byte voxel) {
return voxel >= WATER && voxel < LAVA + FLUID_LEVELS;
}
static boolean water(byte voxel) {
return voxel >= WATER && voxel < WATER + FLUID_LEVELS;
}
static boolean source(byte voxel) {
return voxel == WATER || voxel == LAVA;
}
static boolean sameFluid(byte voxel, byte other) {
return fluid(other) && water(voxel) == water(other);
}
static int amount(byte voxel) {
int level = (voxel - WATER) % FLUID_LEVELS;
return level == 0 || level >= 8 ? 8 : 8 - level;
}
private static int dropOff(byte voxel) {
return water(voxel) ? 1 : 2;
}
static long columnKey(int x, int z) {
return ((long) x << 32) | (z & 0xFFFFFFFFL);
}
static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
}
}
@@ -0,0 +1,103 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
final class HydrologyWindows {
static final int CHUNKS = WindowKey.SIZE >> 4;
private HydrologyWindows() {
}
record WindowRange(int minX, int maxX, int minZ, int maxZ) {
WindowRange {
if (maxX < minX || maxZ < minZ) {
throw new IllegalArgumentException("Window range " + minX + ".." + maxX + "," + minZ + ".." + maxZ + " is empty.");
}
}
static WindowRange parse(String text) {
String[] axes = text.trim().split(",", -1);
if (axes.length != 2) {
throw new IllegalArgumentException("Windows must read x0..x1,z0..z1, got '" + text + "'.");
}
int[] x = bounds(axes[0], text);
int[] z = bounds(axes[1], text);
return new WindowRange(x[0], x[1], z[0], z[1]);
}
private static int[] bounds(String axis, String text) {
String[] parts = axis.trim().split("\\.\\.", -1);
if (parts.length != 2) {
throw new IllegalArgumentException("Windows must read x0..x1,z0..z1, got '" + text + "'.");
}
return new int[]{Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim())};
}
int count() {
return (maxX - minX + 1) * (maxZ - minZ + 1);
}
boolean contains(WindowKey key) {
return key.wx() >= minX && key.wx() <= maxX && key.wz() >= minZ && key.wz() <= maxZ;
}
boolean containsChunk(int chunkX, int chunkZ) {
return contains(new WindowKey(Math.floorDiv(chunkX, CHUNKS), Math.floorDiv(chunkZ, CHUNKS)));
}
boolean containsBlock(int x, int z) {
return contains(WindowKey.containing(x, z));
}
WindowRange grown(int rings) {
return new WindowRange(minX - rings, maxX + rings, minZ - rings, maxZ + rings);
}
List<WindowKey> keys() {
List<WindowKey> keys = new ArrayList<>(count());
for (int wz = minZ; wz <= maxZ; wz++) {
for (int wx = minX; wx <= maxX; wx++) {
keys.add(new WindowKey(wx, wz));
}
}
return keys;
}
}
record Target(File pack, String dimension, long seed, WindowRange windows, boolean studio) {
Target {
if (pack == null || !pack.isAbsolute() || dimension == null || dimension.isBlank() || windows == null) {
throw new IllegalArgumentException("An absolute pack path, a dimension key and a window range are required.");
}
}
static Target parse(String pack, String dimension, String seed, String windows, String studio) {
File file = new File(pack);
return new Target(file, dimension.equals("-") ? onlyDimension(file) : dimension, Long.parseLong(seed), WindowRange.parse(windows),
RealPackProbeSupport.parseBoolean(studio, "studio"));
}
private static String onlyDimension(File pack) {
File[] files = new File(pack, "dimensions").listFiles((directory, name) -> name.endsWith(".json"));
if (files == null || files.length != 1) {
throw new IllegalArgumentException("The pack " + pack + " must hold exactly one dimension, or name one with probeDimension.");
}
String name = files[0].getName();
return name.substring(0, name.length() - ".json".length());
}
}
static List<WindowKey> neighbourhood(WindowKey key) {
List<WindowKey> keys = new ArrayList<>(9);
for (int dz = -1; dz <= 1; dz++) {
for (int dx = -1; dx <= 1; dx++) {
keys.add(new WindowKey(key.wx() + dx, key.wz() + dz));
}
}
return keys;
}
}
@@ -18,83 +18,28 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.runtime.Engine;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) {
private static final int PLANNING_THREADS = 4;
record ProbeHydrologyPreplan(int windows, long elapsedNanos) {
static final int RINGS = 1;
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
static Optional<ProbeHydrologyPreplan> run(Engine engine, GenerationProbe.ProbeConfiguration configuration)
throws Exception {
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
if (hydrology == null) {
HydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
if (hydrology == null || !hydrology.enabled()) {
return Optional.empty();
}
HydrologyPlannerSettings settings = hydrology.settings();
int tileSize = settings.routing().tileSize();
int publicationRadius = settings.publicationRadius();
int reachChunks = Math.max(engine.getMantle().getRadius(), engine.getMantle().getRealRadius());
List<HydrologyTileKey> keys = tiles(configuration, tileSize, publicationRadius, reachChunks);
int[] bounds = GenerationProbe.scheduledChunkBounds(configuration);
long started = System.nanoTime();
ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), PLANNING_THREADS));
try {
List<Future<?>> plans = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
plans.add(executor.submit(() -> hydrology.tile(key)));
}
for (Future<?> plan : plans) {
await(plan);
}
} finally {
executor.shutdown();
}
return Optional.of(new ProbeHydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks,
System.nanoTime() - started));
}
static List<HydrologyTileKey> tiles(GenerationProbe.ProbeConfiguration configuration, int tileSize,
int publicationRadius, int reachChunks) {
HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration);
long reach = (long) reachChunks * 16L + publicationRadius;
int minimumX = Math.toIntExact(Math.floorDiv(area.minimumBlockX() - reach, tileSize)) - 1;
int minimumZ = Math.toIntExact(Math.floorDiv(area.minimumBlockZ() - reach, tileSize)) - 1;
int maximumX = Math.toIntExact(Math.floorDiv(area.maximumBlockX() + reach, tileSize)) + 1;
int maximumZ = Math.toIntExact(Math.floorDiv(area.maximumBlockZ() + reach, tileSize)) + 1;
int centerX = Math.floorDiv(area.centerBlockX(), tileSize);
int centerZ = Math.floorDiv(area.centerBlockZ(), tileSize);
List<HydrologyTileKey> keys = new ArrayList<>((maximumX - minimumX + 1) * (maximumZ - minimumZ + 1));
for (int tileZ = minimumZ; tileZ <= maximumZ; tileZ++) {
for (int tileX = minimumX; tileX <= maximumX; tileX++) {
keys.add(new HydrologyTileKey(tileX, tileZ));
}
}
keys.sort(Comparator.comparingInt((HydrologyTileKey key) ->
Math.max(Math.abs(key.tileX() - centerX), Math.abs(key.tileZ() - centerZ))));
return keys;
}
String machineLine() {
return String.format(Locale.ROOT,
"IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=%d tile_size=%d publication_radius=%d reach_chunks=%d planned_ms=%.3f",
tiles, tileSize, publicationRadius, reachChunks, elapsedNanos / 1_000_000D);
}
private static void await(Future<?> plan) throws Exception {
try {
plan.get();
hydrology.prepareWindowsAround(bounds[0], bounds[1], bounds[2], bounds[3], RINGS).get();
} catch (ExecutionException failure) {
Throwable cause = failure.getCause();
if (cause instanceof Error error) {
@@ -105,5 +50,17 @@ record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int
}
throw new IllegalStateException(cause);
}
return Optional.of(new ProbeHydrologyPreplan(windows(bounds), System.nanoTime() - started));
}
static int windows(int[] chunkBounds) {
int width = Math.floorDiv(chunkBounds[2], CHUNKS_PER_WINDOW) - Math.floorDiv(chunkBounds[0], CHUNKS_PER_WINDOW) + 1 + 2 * RINGS;
int depth = Math.floorDiv(chunkBounds[3], CHUNKS_PER_WINDOW) - Math.floorDiv(chunkBounds[1], CHUNKS_PER_WINDOW) + 1 + 2 * RINGS;
return width * depth;
}
String machineLine() {
return String.format(Locale.ROOT, "IRIS_GENPROBE_HYDROLOGY_PREPLAN version=2 windows=%d planned_ms=%.3f", windows,
elapsedNanos / 1_000_000D);
}
}
@@ -16,8 +16,6 @@ import art.arcane.iris.generation.runtime.EngineWorldManager;
import art.arcane.iris.generation.runtime.EngineWorldManagerProvider;
import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.mantle.MantleComponent;
import art.arcane.iris.generation.mantle.MantleObjectComponent;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.GenerationHistory;
@@ -84,34 +82,9 @@ final class RealPackProbeSupport {
File workRoot = Files.createTempDirectory(options.parent(), "iris-real-pack-probe-").toFile();
try {
File pack = clonePack(packSource, workRoot);
configureRuntime(new File(workRoot, "platform-validation"), false, bindings);
configureRuntime(new File(workRoot, "platform-validation"), bindings);
validatePack(pack, logPrefix);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, false, bindings);
} catch (Throwable failure) {
clearRuntime();
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, null);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, hydrologyGeneratedVerification, null);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, bindings);
} catch (Throwable failure) {
clearRuntime();
Throwable cleanupFailure = deleteWorkRoot(workRoot);
@@ -292,7 +265,6 @@ final class RealPackProbeSupport {
private final File pack;
private final String dimensionKey;
private final String logPrefix;
private final boolean hydrologyGeneratedVerification;
private final RuntimeBindings bindings;
private int sessionSequence;
private boolean sessionOpen;
@@ -303,14 +275,12 @@ final class RealPackProbeSupport {
File pack,
String dimensionKey,
String logPrefix,
boolean hydrologyGeneratedVerification,
RuntimeBindings bindings
) {
this.workRoot = workRoot;
this.pack = pack;
this.dimensionKey = dimensionKey;
this.logPrefix = logPrefix;
this.hydrologyGeneratedVerification = hydrologyGeneratedVerification;
this.bindings = bindings;
}
@@ -328,7 +298,7 @@ final class RealPackProbeSupport {
IrisData data = null;
Engine engine = null;
try {
configureRuntime(platformRoot, hydrologyGeneratedVerification, bindings);
configureRuntime(platformRoot, bindings);
data = IrisData.openRuntime(pack);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
@@ -551,16 +521,6 @@ final class RealPackProbeSupport {
}
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 clearRuntime() {
@@ -572,7 +532,7 @@ final class RealPackProbeSupport {
StubPlatform.errorSink(null);
}
private static void configureRuntime(File platformRoot, boolean hydrologyGeneratedVerification, RuntimeBindings bindings) {
private static void configureRuntime(File platformRoot, RuntimeBindings bindings) {
REPORTED.clear();
StubPlatform.bindGenerationStateHandlers();
StubPlatform.verbose(false);
@@ -582,7 +542,7 @@ final class RealPackProbeSupport {
(EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new);
IrisServices.register(EnginePlatformHooks.class, bindings == null
? new InertPlatformHooks(hydrologyGeneratedVerification) : bindings);
? new InertPlatformHooks() : bindings);
IrisPlatforms.unbind();
IrisPlatforms.bind(bindings == null ? new StubPlatform(platformRoot) : bindings.create(platformRoot));
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -49,11 +49,16 @@ import java.util.function.Consumer;
public final class StubPlatform implements IrisPlatform {
private static volatile boolean VERBOSE = false;
private static volatile Consumer<Throwable> ERROR_SINK = null;
private static volatile boolean VANILLA_SOLIDITY = false;
public static void verbose(boolean verbose) {
VERBOSE = verbose;
}
public static void vanillaSolidity(boolean vanilla) {
VANILLA_SOLIDITY = vanilla;
}
public static void errorSink(Consumer<Throwable> sink) {
ERROR_SINK = sink;
}
@@ -138,7 +143,14 @@ public final class StubPlatform implements IrisPlatform {
@Override
public boolean isSolid() {
return !isAir() && !isFluid();
if (isAir() || isFluid()) {
return false;
}
if (!VANILLA_SOLIDITY) {
return true;
}
byte voxel = HydrologyVoxels.classify(this);
return voxel == HydrologyVoxels.SOLID || voxel == HydrologyVoxels.WATER && isWaterLogged();
}
@Override
@@ -0,0 +1,24 @@
package art.arcane.iris.probe;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class CavePoolChecksTest {
@Test
public void everyContainmentCountFailsIncludingForeignFluidContacts() {
CavePoolProbe.Counts foreign = counts(0, 0, 0, 1, 0, 0, 0, 0);
assertEquals("a pool touching another fluid fails as the standalone probe does", 1L, foreign.failures());
assertEquals(36L, counts(1, 2, 3, 4, 5, 6, 7, 8).failures());
assertEquals(0L, counts(0, 0, 0, 0, 0, 0, 0, 0).failures());
}
private static CavePoolProbe.Counts counts(int air, int open, int sky, int foreign, int waterlogged, int gravity, int fluidAbove,
int gravityCeilings) {
return new CavePoolProbe.Counts(1, 1, 1, 1, Map.of(), List.of(), List.of(), Map.of(), air, open, sky, foreign, waterlogged, gravity,
fluidAbove, gravityCeilings, Map.of(), List.of());
}
}
@@ -1,7 +1,6 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.runtime.EngineMetrics;
import org.junit.Test;
@@ -101,16 +100,27 @@ public final class GenerationProbeTest {
}
@Test
public void pregenerationAreaCoversEveryScheduledChunk() {
public void hydrologyRegionsCoverEveryScheduledChunkCentreFirst() {
GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
new File("/tmp/pack"), "overworld", 64, 256, 2048, 2048, true, false, 1337L);
HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration);
new File("/tmp/pack"), "overworld", 64, 2048, 2048, 2048, true, false, 1337L);
List<long[]> regions = GenerationProbe.hydrologyRegions(configuration);
Set<Long> keys = new HashSet<>();
for (long[] region : regions) {
assertTrue(keys.add(region[0] << 32 | (region[1] & 0xFFFFFFFFL)));
}
for (GenerationProbe.ChunkCoordinate coordinate : GenerationProbe.scheduleCoordinates(320, 2048, 2048)) {
assertTrue(((long) coordinate.x() << 4) >= area.minimumBlockX());
assertTrue((((long) coordinate.x() << 4) + 15L) <= area.maximumBlockX());
assertTrue(((long) coordinate.z() << 4) >= area.minimumBlockZ());
assertTrue((((long) coordinate.z() << 4) + 15L) <= area.maximumBlockZ());
for (GenerationProbe.ChunkCoordinate coordinate : GenerationProbe.scheduleCoordinates(2112, 2048, 2048)) {
long regionX = Math.floorDiv(coordinate.x(), WindowKey.SIZE >> 4);
long regionZ = Math.floorDiv(coordinate.z(), WindowKey.SIZE >> 4);
assertTrue(keys.contains(regionX << 32 | (regionZ & 0xFFFFFFFFL)));
}
assertEquals(64L, regions.getFirst()[0]);
assertEquals(64L, regions.getFirst()[1]);
long previousRing = 0L;
for (long[] region : regions) {
long ring = Math.max(Math.abs(region[0] - 64L), Math.abs(region[1] - 64L));
assertTrue(ring >= previousRing);
previousRing = ring;
}
}
@@ -194,35 +204,21 @@ public final class GenerationProbeTest {
}
@Test
public void hydrologyPreplanCoversEveryTileAScheduledChunkComposesFromPlusTheNeighbourRing() {
public void hydrologyPreplanCountsTheScheduleWindowsPlusTheirNeighbourRing() {
GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
new File("/tmp/pack"), "overworld", 256, 1024, 2048, 2048, false, false, 1337L);
int tileSize = 512;
int publicationRadius = 96;
int reachChunks = 13;
List<HydrologyTileKey> keys = ProbeHydrologyPreplan.tiles(configuration, tileSize, publicationRadius, reachChunks);
Set<HydrologyTileKey> planned = new HashSet<>(keys);
int[] bounds = GenerationProbe.scheduledChunkBounds(configuration);
assertEquals(keys.size(), planned.size());
for (GenerationProbe.ChunkCoordinate chunk : GenerationProbe.scheduleCoordinates(1280, 2048, 2048)) {
for (int offsetX = -reachChunks; offsetX <= reachChunks; offsetX++) {
for (int offsetZ = -reachChunks; offsetZ <= reachChunks; offsetZ++) {
long minimumX = (long) (chunk.x() + offsetX) * 16L;
long minimumZ = (long) (chunk.z() + offsetZ) * 16L;
int firstX = Math.floorDiv(Math.toIntExact(minimumX - publicationRadius), tileSize) - 1;
int lastX = Math.floorDiv(Math.toIntExact(minimumX + 15L + publicationRadius), tileSize) + 1;
int firstZ = Math.floorDiv(Math.toIntExact(minimumZ - publicationRadius), tileSize) - 1;
int lastZ = Math.floorDiv(Math.toIntExact(minimumZ + 15L + publicationRadius), tileSize) + 1;
for (int tileX = firstX; tileX <= lastX; tileX++) {
for (int tileZ = firstZ; tileZ <= lastZ; tileZ++) {
assertTrue(planned.contains(new HydrologyTileKey(tileX, tileZ)));
}
}
}
}
assertTrue(chunk.x() >= bounds[0] && chunk.x() <= bounds[2]);
assertTrue(chunk.z() >= bounds[1] && chunk.z() <= bounds[3]);
}
HydrologyTileKey first = keys.getFirst();
assertEquals(new HydrologyTileKey(Math.floorDiv(2048 * 16 + 8, tileSize), Math.floorDiv(2048 * 16 + 8, tileSize)), first);
int chunks = WindowKey.SIZE >> 4;
int width = Math.floorDiv(bounds[2], chunks) - Math.floorDiv(bounds[0], chunks) + 3;
int depth = Math.floorDiv(bounds[3], chunks) - Math.floorDiv(bounds[1], chunks) + 3;
assertEquals(width * depth, ProbeHydrologyPreplan.windows(bounds));
assertEquals("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=2 windows=16 planned_ms=1.500",
new ProbeHydrologyPreplan(16, 1_500_000L).machineLine());
}
@Test
@@ -1,283 +1,24 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.File;
import java.util.HashMap;
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 HydrologyBlockProbeTest {
@ClassRule
public static final PlatformLeakGuard LEAK_GUARD = PlatformLeakGuard.clean();
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.bind(new StubPlatform());
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
public class HydrologyBlockProbeTest {
@Test
public void generatedCaneChecksRootSubstrateAndActualWaterAcrossNegativeChunkBoundary() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(-1, 0);
set(chunks, -1, 4, 8, "sugar_cane[age=0]");
set(chunks, -1, 5, 8, "sugar_cane[age=0]");
set(chunks, -1, 3, 8, "sand");
set(chunks, 0, 3, 8, "water[level=0]");
HydrologyBlockProbe.Evidence valid = inspect(chunks, Map.of());
assertEquals(1, valid.caneRoots);
assertEquals(1, valid.validCaneRoots);
assertEquals(0, valid.censoredCaneRoots);
set(chunks, -1, 3, 8, "stone");
HydrologyBlockProbe.Evidence stone = inspect(chunks, Map.of());
assertEquals(1, stone.invalidCaneSubstrates);
assertFalse(HydrologyBlockProbe.failures(stone, 8, Set.of()).isEmpty());
set(chunks, -1, 3, 8, "sand");
set(chunks, 0, 3, 8, "ice");
assertEquals(1, inspect(chunks, Map.of()).dryCaneRoots);
set(chunks, 0, 3, 8, "frosted_ice[age=0]");
assertEquals(1, inspect(chunks, Map.of()).validCaneRoots);
}
@Test
public void outerCaneBoundaryIsCensoredWithoutPretendingItPassed() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
set(chunks, 0, 4, 8, "sugar_cane");
set(chunks, 0, 3, 8, "dirt");
HydrologyBlockProbe.Evidence evidence = inspect(chunks, Map.of());
assertEquals(1, evidence.censoredCaneRoots);
assertEquals(0, evidence.dryCaneRoots);
assertFalse(HydrologyBlockProbe.failures(evidence, 8, Set.of("cane")).isEmpty());
}
@Test
public void retainedWaterloggedVoxelsCountButSolidAndDryChannelVoxelsFail() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer layer = layer(HydrologyFeatureType.RIFFLE, 2, 4, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = Map.of(
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("water")));
set(chunks, 8, 3, 8, "stone");
set(chunks, 8, 4, 8, "brain_coral[waterlogged=true]");
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(2, evidence.plannedWetVoxels);
assertEquals(1, evidence.missingWetVoxels);
set(chunks, 8, 3, 8, "water[level=0]");
assertEquals(0, inspect(chunks, planned).missingWetVoxels);
}
@Test
public void implicitAquaticWaterCountsWhileDryPlantsAndOtherFluidsStillFail() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer layer = layer(HydrologyFeatureType.RIFFLE, 2, 3, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = Map.of(
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("water")));
for (String block : List.of("seagrass", "tall_seagrass[half=lower]", "tall_seagrass[half=upper]",
"kelp[age=3]", "kelp_plant", "bubble_column[drag=false]")) {
set(chunks, 8, 3, 8, block);
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(block, 1, evidence.plannedWetVoxels);
assertEquals(block, 0, evidence.missingWetVoxels);
assertTrue(block, HydrologyBlockProbe.failures(evidence, 8, Set.of("channel")).isEmpty());
}
for (String block : List.of("short_grass", "tall_grass[half=lower]", "fern", "lily_pad",
"sugar_cane", "dead_bush", "brain_coral[waterlogged=false]")) {
set(chunks, 8, 3, 8, block);
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(block, 1, evidence.missingWetVoxels);
assertFalse(block, HydrologyBlockProbe.failures(evidence, 8, Set.of("channel")).isEmpty());
}
set(chunks, 8, 3, 8, "kelp_plant");
Map<Long, HydrologyBlockProbe.PlannedColumn> lava = Map.of(
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("lava")));
HydrologyBlockProbe.Evidence rejected = inspect(chunks, lava);
assertEquals(1, rejected.missingWetVoxels);
assertEquals(Map.of("minecraft:kelp_plant", 1L), rejected.plannedWetBlockCounts);
}
@Test
public void wetBlockCountsCoverEveryVoxelBeyondTheExampleLimit() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 4; z++) {
HydrologyColumnLayer layer = layer(HydrologyFeatureType.RIFFLE, 2, 3, true);
planned.put(RiverFootprint.pack(x, z), new HydrologyBlockProbe.PlannedColumn(8, false, layer, state("water")));
set(chunks, x, 3, z, x < 8 ? "seagrass" : "short_grass");
}
}
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(64, evidence.plannedWetVoxels);
assertEquals(32, evidence.missingWetVoxels);
assertEquals(16, evidence.examples.size());
assertEquals(Map.of("minecraft:seagrass", 32L, "minecraft:short_grass", 32L), evidence.plannedWetBlockCounts);
}
@Test
public void mouthRequiresActualCardinalWaterToAnOceanColumn() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer mouth = layer(HydrologyFeatureType.MOUTH, 5, 6, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, mouth, state("water")));
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, true, null, null));
set(chunks, 8, 6, 8, "water");
set(chunks, 9, 6, 8, "water");
HydrologyBlockProbe.Evidence connected = inspect(chunks, planned);
assertEquals(1, connected.connectedMouthComponents);
assertEquals(1, connected.drySillCuts);
assertEquals(0, connected.oceanBedWrites);
assertTrue(HydrologyBlockProbe.failures(connected, 8, Set.of("mouth", "channel")).isEmpty());
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, false, null, null));
assertEquals(1, inspect(chunks, planned).disconnectedMouthComponents);
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, true, null, null));
set(chunks, 9, 6, 8, "stone");
assertEquals(1, inspect(chunks, planned).disconnectedMouthComponents);
}
@Test
public void oceanApronRequiresConnectedOwnedWaterWhenAPoolOwnsTheShoreline() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer pool = layer(HydrologyFeatureType.SURFACE_POOL, 5, 6, true);
HydrologyFeatureRef mouth = new HydrologyFeatureRef(4L, HydrologyFeatureType.MOUTH,
2L, 5L, 9, 6, 8, 1, 0, false);
HydrologyColumnLayer apron = new HydrologyColumnLayer(mouth, 6, 6, 6,
true, false, false, true, false, false, false, false, true,
"water", "surface", "mouth", "shore", "bank", "cave");
HydrologyColumnSample ocean = new HydrologyColumnSample(9, 8, 5, 6, true, "ocean", List.of(apron));
assertTrue(ocean.primarySurfaceLayer().isEmpty());
assertEquals(apron, HydrologyBlockProbe.surfaceForInspection(ocean));
assertEquals(pool, HydrologyBlockProbe.surfaceForInspection(
new HydrologyColumnSample(8, 8, 6, 6, false, "shore", List.of(pool, apron))));
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, pool, state("water")));
planned.put(RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(5, true, apron, state("water")));
set(chunks, 8, 6, 8, "water");
set(chunks, 9, 6, 8, "water");
assertEquals(1, inspect(chunks, planned).connectedMouthComponents);
set(chunks, 8, 6, 8, "stone");
HydrologyBlockProbe.Evidence disconnected = inspect(chunks, planned);
assertEquals(0, disconnected.connectedMouthComponents);
assertEquals(1, disconnected.disconnectedMouthComponents);
HydrologyFeatureRef other = new HydrologyFeatureRef(6L, HydrologyFeatureType.SURFACE_POOL,
99L, 7L, 8, 6, 8, 1, 0, false);
HydrologyColumnLayer unrelated = new HydrologyColumnLayer(other, 5, 6, 6,
true, false, false, true, false, false, true, true, false,
"water", "surface", "mouth", "shore", "bank", "cave");
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, unrelated, state("water")));
set(chunks, 8, 6, 8, "water");
assertEquals(0, inspect(chunks, planned).connectedMouthComponents);
}
@Test
public void diagonalOceanWaterAndOutOfBoundsWaterDoNotProveMouthCoverage() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
HydrologyColumnLayer mouth = layer(HydrologyFeatureType.MOUTH, 5, 6, true);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = new HashMap<>();
planned.put(RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(6, false, mouth, state("water")));
planned.put(RiverFootprint.pack(9, 9), new HydrologyBlockProbe.PlannedColumn(5, true, null, null));
set(chunks, 8, 6, 8, "water");
set(chunks, 9, 6, 9, "water");
assertEquals(1, inspect(chunks, planned).disconnectedMouthComponents);
planned.clear();
planned.put(RiverFootprint.pack(0, 8), new HydrologyBlockProbe.PlannedColumn(6, false, mouth, state("water")));
set(chunks, 0, 6, 8, "water");
HydrologyBlockProbe.Evidence censored = inspect(chunks, planned);
assertEquals(1, censored.censoredMouthComponents);
assertFalse(HydrologyBlockProbe.failures(censored, 8, Set.of("mouth")).isEmpty());
}
@Test
public void bankBudgetsAndOceanBedOwnershipRemainIndependentGates() {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = chunks(0, 0);
Map<Long, HydrologyBlockProbe.PlannedColumn> planned = Map.of(
RiverFootprint.pack(8, 8), new HydrologyBlockProbe.PlannedColumn(
12, false, layer(HydrologyFeatureType.RIFFLE, 3, 3, false), null),
RiverFootprint.pack(9, 8), new HydrologyBlockProbe.PlannedColumn(
5, true, layer(HydrologyFeatureType.MOUTH, 3, 6, true), state("water")));
HydrologyBlockProbe.Evidence evidence = inspect(chunks, planned);
assertEquals(9, evidence.maximumBankCut);
assertEquals(9, evidence.bankExcavation);
assertEquals(1, evidence.oceanBedWrites);
assertTrue(HydrologyBlockProbe.failures(evidence, 8, Set.of()).stream()
.anyMatch(value -> value.contains("budget")));
}
@Test
public void configurationBoundsAndRequiredCoveragePreventUnboundedOrVacuousProof() {
assertThrows(IllegalArgumentException.class, () -> new HydrologyBlockProbe.Configuration(
new File("/pack"), "overworld", 1L, 0, 8, 0, 8, Set.of("mouth"), new File("/output"), false));
assertThrows(IllegalArgumentException.class, () -> new HydrologyBlockProbe.Configuration(
new File("/pack"), "overworld", 1L, 0, 0, 0, 0, Set.of("river"), new File("/output"), false));
assertEquals(4, HydrologyBlockProbe.failures(new HydrologyBlockProbe.Evidence(), 8,
Set.of("mouth", "cane", "channel", "bank")).size());
}
@Test
public void reportSerializesAbsolutePathsWithoutReflectingIntoFile() {
HydrologyBlockProbe.Configuration configuration = new HydrologyBlockProbe.Configuration(
new File("/pack"), "overworld", 1L, 0, 0, 0, 0, Set.of(), new File("/output"), false);
String json = HydrologyBlockProbe.json(new HydrologyBlockProbe.Result("PASS", configuration,
-64, 127, new HydrologyBlockProbe.Evidence(), List.of(), List.of()));
assertTrue(json.contains("\"pack\": \"/pack\""));
assertTrue(json.contains("worldMinimumY"));
}
private static HydrologyBlockProbe.Evidence inspect(Map<Long, RealPackProbeSupport.GeneratedChunk> chunks,
Map<Long, HydrologyBlockProbe.PlannedColumn> planned) {
return HydrologyBlockProbe.inspect(new HydrologyBlockProbe.Volume(chunks), planned, 6);
}
private static Map<Long, RealPackProbeSupport.GeneratedChunk> chunks(int minimumX, int maximumX) {
Map<Long, RealPackProbeSupport.GeneratedChunk> chunks = new HashMap<>();
for (int x = minimumX; x <= maximumX; x++) {
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(16, 16, 16);
Hunk<NativeBiome> biomes = Hunk.newArrayHunk(16, 16, 16);
chunks.put(RiverFootprint.pack(x, 0), new RealPackProbeSupport.GeneratedChunk(x, 0, 16, blocks, biomes));
}
return chunks;
}
private static void set(Map<Long, RealPackProbeSupport.GeneratedChunk> chunks, int x, int y, int z, String key) {
chunks.get(RiverFootprint.pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)))
.blocks().set(Math.floorMod(x, 16), y, Math.floorMod(z, 16), state(key));
}
private static NativeBlockState state(String key) {
return IrisPlatforms.get().registries().block("minecraft:" + key);
}
private static HydrologyColumnLayer layer(HydrologyFeatureType type, int bed, int head, boolean channel) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(1L, type, 2L, 3L, 8, head, 8, 1, 0, false);
return new HydrologyColumnLayer(feature, bed, head, head, channel, false, !channel, channel,
false, false, true, channel, false, "water", "surface", "mouth", "shore", "bank", "cave");
public void failuresNameMissingSurfaceLeaksTicksAndCoverage() {
HydrologyBlockProbe.Evidence evidence = new HydrologyBlockProbe.Evidence();
evidence.scan.missingSurfaces = 2;
evidence.scan.containmentLeaks = 3;
evidence.scan.tickConversionCells = 4;
evidence.channelColumns = 5;
assertEquals(List.of(
"Generated blocks miss the planned water surface in 2 columns.",
"Generated water leaks into open ground in 3 cells.",
"Fluid ticks would move generated water in 4 cells.",
"Required generated coverage is absent: pond"), HydrologyBlockProbe.failures(evidence, Set.of("channel", "pond")));
}
}
@@ -0,0 +1,340 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.RegionKey;
import art.arcane.iris.generation.hydrology.plan.RegionPlan;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.water.PondKind;
import art.arcane.iris.generation.hydrology.water.PondMask;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.BiFunction;
import java.util.function.Function;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HydrologyMetricsTest {
private static final int SEA = 40;
private static final int NATURAL = 70;
private static final HydrologySettings SETTINGS = HydrologySettings.compile(new IrisHydrology().setEnabled(true), 1L, SEA, 0, 320);
private static final HydrologyTier STREAM = SETTINGS.tiers().get(0);
private static final HydrologyTier RIVER = SETTINGS.tiers().get(1);
private static final WindowKey WEST = new WindowKey(0, 0);
private static final WindowKey EAST = new WindowKey(1, 0);
private static final HydrologyWindows.WindowRange BOTH = new HydrologyWindows.WindowRange(0, 1, 0, 0);
private static final HydrologyWindows.WindowRange WEST_ONLY = new HydrologyWindows.WindowRange(0, 0, 0, 0);
@Test
public void planSeamsFireOnSharedOwnershipMissingLinksAndOverlappingPonds() {
Reach shared = reach(1L, STREAM, 99L, 1000f, new int[]{60, 59}, 10f, 10f, 20f, 10f);
Pond west = pond(10L, 55, square(100, 100, 3), 98L, 104, 104);
Pond east = pond(11L, 55, square(100, 100, 3), 0L, 108, 108);
List<String> examples = new ArrayList<>();
HydrologyPlanMetrics metrics = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(shared), List.of(west)),
plan(EAST, List.of(shared), List.of(east))), List.of(), BOTH, examples::add);
assertEquals(examples.toString(), 2L, metrics.seams(WEST));
assertEquals(examples.toString(), 2L, metrics.seams(EAST));
assertTrue(examples.toString(), examples.contains("seam reach 1 owned by " + WEST + " and " + EAST));
assertTrue(examples.toString(), examples.contains("seam reach 1 links to missing reach 99"));
assertTrue(examples.toString(), examples.contains("seam pond 10 drains to missing reach 98"));
assertTrue(examples.toString(), examples.contains("seam ponds 10 and 11 from " + WEST + " and " + EAST + " overlap"));
HydrologyPlanMetrics clean = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(reach(1L, STREAM, 0L, 1000f, new int[]{60, 59},
10f, 10f, 20f, 10f)), List.of()), plan(EAST, List.of(), List.of())), List.of(), BOTH, examples::add);
assertEquals(0L, clean.seams(WEST) + clean.seams(EAST));
}
@Test
public void aStemFollowsItsLargestFeederThroughAJunction() {
Reach small = reach(1L, RIVER, 3L, 5000f, new int[]{90, 80}, 0f, 0f, 200f, 0f);
Reach large = reach(2L, RIVER, 3L, 6000f, new int[]{90, 80}, 200f, 300f, 200f, 0f);
Reach trunk = reach(3L, RIVER, 0L, 11000f, new int[]{80, 70}, 200f, 0f, 350f, 0f);
HydrologyPlanMetrics metrics = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(small, large, trunk), List.of()),
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
});
assertEquals(1, metrics.rivers(WEST));
}
@Test
public void aStemContinuesThroughThePondItFillsIntoThePondOutlet() {
Pond pond = pond(10L, 75, square(240, -16, 8), 2L, 256, 0);
Reach inflow = reach(1L, RIVER, 0L, 6000f, new int[]{90, 76}, 0f, 0f, 250f, 0f);
Reach outlet = reach(2L, RIVER, 0L, 6000f, new int[]{75, 70}, 270f, 0f, 470f, 0f);
HydrologyPlanMetrics metrics = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(inflow, outlet), List.of(pond)),
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
});
assertEquals(pond, metrics.endPond(inflow));
assertEquals(1, metrics.rivers(WEST));
}
@Test
public void aStemIsCountedOnlyByTheWindowThatOwnsItsLowestReach() {
Reach upper = reach(1L, RIVER, 2L, 6000f, new int[]{90, 80}, 100f, 0f, 500f, 0f);
Reach lower = reach(2L, RIVER, 0L, 6000f, new int[]{80, 70}, 500f, 0f, 700f, 0f);
Map<WindowKey, WindowPlan> plans = windows(plan(WEST, List.of(upper), List.of()), plan(EAST, List.of(lower), List.of()));
HydrologyPlanMetrics west = new HydrologyPlanMetrics(SETTINGS, plans, List.of(), WEST_ONLY, ignored -> {
});
HydrologyPlanMetrics both = new HydrologyPlanMetrics(SETTINGS, plans, List.of(), BOTH, ignored -> {
});
assertEquals(0, west.rivers(WEST));
assertEquals(0, both.rivers(WEST));
assertEquals(1, both.rivers(EAST));
}
@Test
public void aPondIsPerchedAgainstReachesOtherThanItsOwnInflowsAndOutlet() {
Pond high = pond(10L, 60, square(96, 96, 3), 2L, 100, 100);
Reach outlet = reach(2L, STREAM, 0L, 1000f, new int[]{60, 59}, 100f, 100f, 100f, 140f);
Reach below = reach(3L, STREAM, 0L, 1000f, new int[]{50, 49}, 200f, 100f, 200f, 140f);
HydrologyPlanMetrics perched = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(outlet, below), List.of(high)),
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
});
assertEquals(1, perched.perched(WEST));
Pond low = pond(10L, 55, square(96, 96, 3), 2L, 100, 100);
HydrologyPlanMetrics level = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(outlet, below), List.of(low)),
plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
});
assertEquals(0, level.perched(WEST));
}
@Test
public void risingWaterFiresAlongReachesAcrossLinksOutOfPondsIntoPondsAndAtMouths() {
Reach along = reach(1L, STREAM, 0L, 1000f, new int[]{61, 60, 59}, 10f, 10f, 14f, 10f, 18f, 10f);
Map<Long, Column> columns = new HashMap<>();
wet(columns, along, 60, 59, 61);
assertRises(1L, "rise along reach 1 from 59 to 61", List.of(along), List.of(), columns);
Reach upper = reach(1L, STREAM, 2L, 1000f, new int[]{60, 55}, 10f, 10f, 14f, 10f);
Reach lower = reach(2L, STREAM, 0L, 1000f, new int[]{55, 54}, 14f, 20f, 14f, 24f);
Map<Long, Column> linked = new HashMap<>();
wet(linked, upper, 60, 55);
wet(linked, lower, 57, 54);
assertRises(1L, "rise from reach 1 at 55 into reach 2 at 57", List.of(upper, lower), List.of(), linked);
Pond pond = pond(10L, 50, square(96, 96, 3), 3L, 100, 100);
Reach outlet = reach(3L, STREAM, 0L, 1000f, new int[]{50, 49}, 120f, 100f, 130f, 100f);
Map<Long, Column> outflow = new HashMap<>();
wet(outflow, outlet, 52, 49);
assertRises(1L, "rise from pond 10 at 50 into outlet 3 at 52", List.of(outlet), List.of(pond), outflow);
Reach inflow = reach(4L, STREAM, 0L, 1000f, new int[]{60, 50, 50}, 60f, 100f, 80f, 100f, 100f, 100f);
Map<Long, Column> backwater = new HashMap<>();
wet(backwater, inflow, 60, 48, HydrologyVoxels.NO_WATER);
assertRises(1L, "rise from reach 4 at 48 into pond 10 at 50", List.of(inflow), List.of(pond(10L, 50, square(96, 96, 3), 0L, 100, 100)),
backwater);
Reach mouth = reach(5L, STREAM, 0L, 1000f, new int[]{45, 40}, 10f, 10f, 14f, 10f);
mouth.style()[1] = ReachStyle.MOUTH;
Map<Long, Column> sea = new HashMap<>();
wet(sea, mouth, 45, 39);
assertRises(1L, "rise from the sea at 40 into the mouth of reach 5 at 39", List.of(mouth), List.of(), sea);
Map<Long, Column> level = new HashMap<>();
wet(level, mouth, 45, 40);
assertRises(0L, null, List.of(mouth), List.of(), level);
}
@Test
public void chunkSeamsFireOnDryColumnsLeftBelowTheirBermAndOnePondAtTwoLevels() {
HydrologyPlanMetrics plans = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(), List.of()), plan(EAST, List.of(), List.of())),
List.of(), BOTH, ignored -> {
});
Map<Long, Column> columns = new HashMap<>();
columns.put(HydrologyVoxels.columnKey(15, 4), new Column(WaterRole.CHANNEL, 72, NATURAL, 1L));
columns.put(HydrologyVoxels.columnKey(16, 4), new Column(WaterRole.BANK, HydrologyVoxels.NO_WATER, NATURAL - 1, 0L));
columns.put(HydrologyVoxels.columnKey(31, 8), new Column(WaterRole.POND, 60, 58, 7L));
columns.put(HydrologyVoxels.columnKey(32, 8), new Column(WaterRole.POND, 61, 58, 7L));
List<String> examples = new ArrayList<>();
HydrologyRasterMetrics metrics = new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL, BOTH, examples::add);
assertEquals(examples.toString(), 2L, metrics.chunkSeams(WEST));
assertTrue(examples.toString(), examples.contains("seam dry column left below its berm across chunks at 15,4 / 16,4"));
assertTrue(examples.toString(), examples.contains("seam one pond at two levels across chunks at 31,8 / 32,8"));
columns.put(HydrologyVoxels.columnKey(16, 4), new Column(WaterRole.BANK, HydrologyVoxels.NO_WATER, 72, 0L));
columns.put(HydrologyVoxels.columnKey(32, 8), new Column(WaterRole.POND, 60, 58, 7L));
assertEquals(0L, new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL, BOTH, ignored -> {
}).chunkSeams(WEST));
}
@Test
public void aChunkSeamCountsAnyDryColumnBelowTheWaterBesideItExceptOverTheSea() {
HydrologyPlanMetrics plans = new HydrologyPlanMetrics(SETTINGS, windows(plan(WEST, List.of(), List.of()), plan(EAST, List.of(), List.of())),
List.of(), BOTH, ignored -> {
});
Map<Long, Column> columns = new HashMap<>();
columns.put(HydrologyVoxels.columnKey(15, 4), new Column(WaterRole.CHANNEL, 90, 88, 1L));
columns.put(HydrologyVoxels.columnKey(16, 4), new Column(WaterRole.BANK, HydrologyVoxels.NO_WATER, 85, 0L));
columns.put(HydrologyVoxels.columnKey(15, 20), new Column(WaterRole.MOUTH, SEA + 2, SEA - 3, 2L));
columns.put(HydrologyVoxels.columnKey(16, 20), new Column(WaterRole.SHORE, HydrologyVoxels.NO_WATER, SEA - 2, 0L));
columns.put(HydrologyVoxels.columnKey(15, 36), new Column(WaterRole.MOUTH, SEA, SEA - 3, 3L));
columns.put(HydrologyVoxels.columnKey(16, 36), new Column(WaterRole.SHORE, HydrologyVoxels.NO_WATER, SEA - 2, 0L));
List<String> examples = new ArrayList<>();
HydrologyRasterMetrics metrics = new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL - 30, BOTH, examples::add);
assertEquals(examples.toString(), 2L, metrics.chunkSeams(WEST));
assertTrue(examples.toString(), examples.contains("seam dry column left below its berm across chunks at 15,4 / 16,4"));
assertTrue(examples.toString(), examples.contains("seam dry column left below its berm across chunks at 15,20 / 16,20"));
}
@Test
public void aPlanMadeWhileAPlanThatNeedsItRunsIsDepthOneOnAnyThread() throws Exception {
RegionKey needed = WindowPlanner.lakeRegions(WEST).get(0);
RegionKey unrelated = new RegionKey(needed.rx() + 100, needed.rz() + 100);
assertEquals(1, depth(regions -> regions.apply(needed)));
ExecutorService other = Executors.newSingleThreadExecutor();
try {
assertEquals(1, depth(regions -> {
Future<RegionPlan> elsewhere = other.submit(() -> regions.apply(needed));
try {
return elsewhere.get();
} catch (Exception failure) {
throw new IllegalStateException(failure);
}
}));
} finally {
other.shutdownNow();
}
assertEquals(0, depth(regions -> regions.apply(unrelated)));
HydrologyColdRuns.PlanGauge sequential = new HydrologyColdRuns.PlanGauge(Set.of(), () -> {
});
sequential.region(key -> null).apply(needed);
sequential.window((key, plans) -> null).apply(WEST, List.of());
assertEquals(0, sequential.deepest());
}
@Test
public void missedTargetsIgnoreColdLatencyAndNeedSixPondLevelsPerNineTiles() {
HydrologyReport.Result spread = result(1000_000D, 2.25D, Set.of(50, 51));
assertTrue(spread.missedTargets().toString(), !spread.missedTargets().contains("coldFirstChunkMs"));
assertTrue(spread.missedTargets().toString(), !spread.missedTargets().contains("pondLevelsDistinct"));
HydrologyReport.Result flat = result(10D, 9D, Set.of(50, 51, 52, 53, 54));
assertTrue(flat.missedTargets().toString(), flat.missedTargets().contains("pondLevelsDistinct"));
}
private static HydrologyReport.Result result(double coldMs, double landTiles, Set<Integer> levels) {
HydrologyReport.WindowMetrics window = new HydrologyReport.WindowMetrics(WEST, coldMs, 0L, 0L, 0D, 0D, 0, landTiles, 100, 0D, 100,
levels, 0, 0L, 0L, 0L, 0L, 0L, 0L, 0, 0L, 0, 0L, 0L, "digest");
return new HydrologyReport.Result(List.of(window), 0D, 0, 1, 0L, 0L, 0D, 0L, "digest", List.of(), List.of());
}
private static int depth(Function<Function<RegionKey, RegionPlan>, RegionPlan> body) {
HydrologyColdRuns.PlanGauge gauge = new HydrologyColdRuns.PlanGauge(Set.of(), () -> {
});
Function<RegionKey, RegionPlan> regions = gauge.region(key -> null);
BiFunction<WindowKey, List<RegionPlan>, WindowPlan> windows = gauge.window((key, plans) -> {
body.apply(regions);
return null;
});
windows.apply(WEST, List.of());
return gauge.deepest();
}
private static void assertRises(long expected, String example, List<Reach> reaches, List<Pond> ponds, Map<Long, Column> columns) {
WindowPlan plan = plan(WEST, reaches, ponds);
HydrologyPlanMetrics plans = new HydrologyPlanMetrics(SETTINGS, windows(plan, plan(EAST, List.of(), List.of())), List.of(), BOTH, ignored -> {
});
List<String> examples = new ArrayList<>();
HydrologyRasterMetrics metrics = new HydrologyRasterMetrics(SETTINGS, plans, rasters(columns), (x, z) -> NATURAL, BOTH, examples::add);
assertEquals(examples.toString(), expected, metrics.rising(WEST, plan));
if (example != null) {
assertTrue(examples.toString(), examples.contains(example));
}
}
private static void wet(Map<Long, Column> columns, Reach reach, int... heads) {
for (int point = 0; point < heads.length; point++) {
if (heads[point] == HydrologyVoxels.NO_WATER) {
continue;
}
int x = (int) Math.floor(reach.xs()[point]);
int z = (int) Math.floor(reach.zs()[point]);
columns.put(HydrologyVoxels.columnKey(x, z), new Column(WaterRole.CHANNEL, heads[point], heads[point] - 2, reach.id()));
}
}
private static HydrologyRasterMetrics.Rasters rasters(Map<Long, Column> columns) {
Map<Long, ChunkWaterRaster> chunks = new HashMap<>();
return (chunkX, chunkZ) -> chunks.computeIfAbsent(HydrologyVoxels.chunkKey(chunkX, chunkZ), ignored -> raster(columns, chunkX, chunkZ));
}
private static ChunkWaterRaster raster(Map<Long, Column> columns, int chunkX, int chunkZ) {
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
when(raster.empty()).thenReturn(false);
when(raster.role(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1)).role());
when(raster.waterHead(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1)).head());
when(raster.terrainHeight(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1))
.terrain());
when(raster.featureId(anyInt(), anyInt())).thenAnswer(call -> column(columns, chunkX, chunkZ, call.getArgument(0), call.getArgument(1))
.featureId());
return raster;
}
private static Column column(Map<Long, Column> columns, int chunkX, int chunkZ, int lx, int lz) {
Column column = columns.get(HydrologyVoxels.columnKey((chunkX << 4) + lx, (chunkZ << 4) + lz));
return column == null ? new Column(WaterRole.NONE, HydrologyVoxels.NO_WATER, NATURAL, 0L) : column;
}
private record Column(WaterRole role, int head, int terrain, long featureId) {
}
private static Map<WindowKey, WindowPlan> windows(WindowPlan... plans) {
Map<WindowKey, WindowPlan> windows = new LinkedHashMap<>();
for (WindowPlan plan : plans) {
windows.put(plan.key(), plan);
}
return windows;
}
private static WindowPlan plan(WindowKey key, List<Reach> reaches, List<Pond> ponds) {
return new WindowPlan(key, reaches, ponds, List.of());
}
private static Reach reach(long id, HydrologyTier tier, long downstream, float catchment, int[] grade, float... coordinates) {
int points = coordinates.length / 2;
float[] xs = new float[points];
float[] zs = new float[points];
float[] halfWidth = new float[points];
float[] depth = new float[points];
ReachStyle[] style = new ReachStyle[points];
for (int point = 0; point < points; point++) {
xs[point] = coordinates[2 * point];
zs[point] = coordinates[2 * point + 1];
halfWidth[point] = 1f;
depth[point] = 1f;
}
Arrays.fill(style, ReachStyle.RUN);
return new Reach(id, tier, downstream, xs, zs, grade, halfWidth, depth, style, catchment);
}
private static Pond pond(long id, int level, PondMask mask, long outlet, int floorX, int floorZ) {
return new Pond(id, PondKind.NATURAL, level, level - 4, floorX, floorZ, level + 1f, mask, outlet, 0, "minecraft:water", floorX, floorZ);
}
private static PondMask square(int originX, int originZ, int nodes) {
long[] bits = new long[(nodes * nodes + 63) >>> 6];
for (int index = 0; index < nodes * nodes; index++) {
bits[index >>> 6] |= 1L << index;
}
return new PondMask(originX, originZ, 8, nodes, nodes, bits);
}
}
File diff suppressed because it is too large. Load diff
@@ -1,22 +0,0 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public final class HydrologyPlanDigestProbeTest {
@Test
public void pregenerationAreaCoversTheRegionsOfTheRadius() {
assertArea(HydrologyPlanDigestProbe.pregenerationArea(1000), -1024L, 1023L);
assertArea(HydrologyPlanDigestProbe.pregenerationArea(1500), -1536L, 1535L);
assertArea(HydrologyPlanDigestProbe.pregenerationArea(2000), -2048L, 2047L);
}
private static void assertArea(HydrologyTileCache.PregenerationArea area, long minimum, long maximum) {
assertEquals(minimum, area.minimumBlockX());
assertEquals(minimum, area.minimumBlockZ());
assertEquals(maximum, area.maximumBlockX());
assertEquals(maximum, area.maximumBlockZ());
}
}
@@ -1,52 +0,0 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.HydrologyTile;
import org.junit.Test;
import java.util.EnumSet;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class HydrologyPlannerProbeTest {
@Test
public void coastalFixturePublishesAnAcceptedOceanMouth() {
HydrologyTile tile = HydrologyPlannerProbe.surfaceMouthTile();
assertTrue(tile.localDiagnosticCandidates().stream().limit(16).toList().toString(),
tile.features().stream().anyMatch(feature -> feature.type() == HydrologyFeatureType.MOUTH));
}
@Test
public void inlandFixturePublishesAContainedWaterfall() {
HydrologyTile tile = HydrologyPlannerProbe.surfaceWaterfallTile();
assertTrue(tile.localDiagnosticCandidates().stream().limit(16).toList().toString(),
tile.features().stream().anyMatch(feature -> feature.type() == HydrologyFeatureType.WATERFALL));
assertTrue(tile.courses().stream().anyMatch(course -> course.segments().stream().anyMatch(segment -> segment.fallingFluid()
&& segment.type() == HydrologyFeatureType.WATERFALL)
&& tile.cavePlans().stream().anyMatch(plan -> plan.source().sourceId() == course.id())));
}
@Test
public void deterministicAcceptedCoveragePublishesEveryReachableFeatureType() {
Map<HydrologyFeatureType, HydrologyFeatureRef> coverage =
HydrologyPlannerProbe.deterministicAcceptedCoverage();
assertEquals(HydrologyPlannerProbe.REQUIRED_FEATURE_TYPES, coverage.keySet());
for (Map.Entry<HydrologyFeatureType, HydrologyFeatureRef> entry : coverage.entrySet()) {
assertEquals(entry.getKey(), entry.getValue().type());
}
}
@Test
public void requiredCoverageExcludesTheUnproducedRidgeBore() {
assertFalse(HydrologyPlannerProbe.REQUIRED_FEATURE_TYPES.contains(HydrologyFeatureType.RIDGE_BORE));
assertEquals(
EnumSet.complementOf(EnumSet.of(HydrologyFeatureType.RIDGE_BORE, HydrologyFeatureType.STANDING_POOL)),
HydrologyPlannerProbe.REQUIRED_FEATURE_TYPES
);
}
}
@@ -0,0 +1,241 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class HydrologyProbeTest {
private static final HydrologyWindows.WindowRange LAND = new HydrologyWindows.WindowRange(4, 4, 4, 4);
private static final List<String> METRICS = List.of("coldFirstChunkMs", "windowCpuMs", "regionCpuMs", "maxDependencyDepth",
"riversPerTile", "streamBlocksPerTile", "pondsPerTile", "pondLevelsDistinct", "pondsPerched8", "containmentLeaks",
"risingWaterSegments", "seamMismatches", "tickConversionCells", "missingSurface", "bermOverCap", "inlets", "inletFailures",
"cavePools", "cavePoolFailures");
@Test
public void reportsAllMetricFields() throws Exception {
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
List<String> lines = HydrologyProbe.measure(fixture.inputs(), LAND).lines();
Map<String, String> window = fields(line(lines, "IRIS_HYDROLOGY_WINDOW"));
Map<String, String> total = fields(line(lines, "IRIS_HYDROLOGY_TOTAL"));
assertEquals("4", window.get("wx"));
assertEquals("4", window.get("wz"));
for (String metric : METRICS) {
assertNumber(metric, window.get(metric));
assertNumber(metric, total.get(metric));
}
assertEquals("1", total.get("windows"));
assertEquals("0", total.get("maxDependencyDepth"));
assertEquals("0", total.get("inlets"));
assertEquals("0", total.get("cavePools"));
assertTrue("land " + total.get("landTiles"), Double.parseDouble(total.get("landTiles")) > 0D);
assertTrue("streams " + total.get("streamBlocksPerTile"), Double.parseDouble(total.get("streamBlocksPerTile")) > 0D);
assertTrue("ponds " + total.get("pondsPerTile"), Double.parseDouble(total.get("pondsPerTile")) > 0D);
assertTrue("cold " + total.get("coldFirstChunkMs"), Double.parseDouble(total.get("coldFirstChunkMs")) > 0D);
assertTrue("generated " + total.get("generatedChunks"), Long.parseLong(total.get("generatedChunks")) > 0L);
}
}
@Test
public void failsOnContainmentLeak() throws Exception {
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
HydrologyReport.Result clean = HydrologyProbe.measure(fixture.inputs(), LAND);
assertEquals(clean.lines().toString(), 0L, clean.sum(HydrologyReport.WindowMetrics::containmentLeaks));
int[] hole = bankBesideWater(fixture);
assertNotNull("no bank column beside water in window " + LAND, hole);
HydrologyReport.Result leaky = HydrologyProbe.measure(fixture.inputs(blocks -> {
if (blocks.chunkX() == hole[0] >> 4 && blocks.chunkZ() == hole[1] >> 4) {
blocks.set(hole[0] & 15, hole[2], hole[1] & 15, HydrologyVoxels.OPEN);
}
return blocks;
}), LAND);
Map<String, String> total = fields(line(leaky.lines(), "IRIS_HYDROLOGY_TOTAL"));
assertTrue(leaky.lines().toString(), Long.parseLong(total.get("containmentLeaks")) > 0L);
assertEquals("FAIL", total.get("status"));
assertTrue(total.get("missedTargets"), total.get("missedTargets").contains("containmentLeaks"));
assertTrue(leaky.lines().toString(), leaky.lines().stream().anyMatch(line -> line.startsWith("IRIS_HYDROLOGY_EXAMPLE leak")
&& line.endsWith(" at " + (hole[0] - hole[3]) + "," + hole[2] + "," + (hole[1] - hole[4]))));
}
}
@Test
public void failsOnMissingPlannedSurface() throws Exception {
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
HydrologyReport.Result clean = HydrologyProbe.measure(fixture.inputs(), LAND);
assertEquals(clean.lines().toString(), 0L, clean.sum(HydrologyReport.WindowMetrics::missingSurface));
int[] wet = bankBesideWater(fixture);
assertNotNull("no wet column in window " + LAND, wet);
int x = wet[0] - wet[3];
int z = wet[1] - wet[4];
HydrologyReport.Result covered = HydrologyProbe.measure(fixture.inputs(blocks -> {
if (blocks.chunkX() == x >> 4 && blocks.chunkZ() == z >> 4) {
blocks.set(x & 15, wet[2], z & 15, HydrologyVoxels.SOLID);
}
return blocks;
}), LAND);
Map<String, String> total = fields(line(covered.lines(), "IRIS_HYDROLOGY_TOTAL"));
assertEquals(covered.lines().toString(), "1", total.get("missingSurface"));
assertEquals("FAIL", total.get("status"));
assertTrue(total.get("missedTargets"), total.get("missedTargets").contains("missingSurface"));
assertTrue(covered.lines().toString(), covered.lines().contains("IRIS_HYDROLOGY_EXAMPLE missing planned surface at " + x + ","
+ wet[2] + "," + z));
}
}
@Test
public void seaFillOverNaturalSeaFloorIsNotAHydrologyLeak() throws Exception {
HydrologyWindows.WindowRange coast = new HydrologyWindows.WindowRange(3, 3, 0, 0);
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
int[] sea = dryColumnInTheSea(fixture, coast);
assertNotNull("no dry hydrology column over natural sea floor in window " + coast, sea);
HydrologyReport.Result result = HydrologyProbe.measure(fixture.inputs(blocks -> {
if (blocks.chunkX() == sea[2] >> 4 && blocks.chunkZ() == sea[3] >> 4) {
blocks.set(sea[2] & 15, SyntheticHydrology.SEA_LEVEL, sea[3] & 15, HydrologyVoxels.OPEN);
}
return blocks;
}), coast);
String leak = "IRIS_HYDROLOGY_EXAMPLE leak source beside open ground at " + sea[0] + "," + SyntheticHydrology.SEA_LEVEL + "," + sea[1];
assertTrue(result.lines().toString(), result.lines().stream().noneMatch(leak::equals));
}
}
@Test
public void aMouthOneBelowTheSeaBesideASeaFilledShoreLeaks() throws Exception {
HydrologyWindows.WindowRange coast = new HydrologyWindows.WindowRange(3, 3, 0, 0);
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
int[] shore = seaShoreBesideWater(fixture, coast);
assertNotNull("no sea-filled column beside hydrology water in window " + coast, shore);
HydrologyReport.Result result = HydrologyProbe.measure(fixture.inputs(blocks -> {
if (blocks.chunkX() == shore[2] >> 4 && blocks.chunkZ() == shore[3] >> 4) {
blocks.set(shore[2] & 15, SyntheticHydrology.SEA_LEVEL, shore[3] & 15, HydrologyVoxels.OPEN);
}
return blocks;
}), coast);
String leak = "IRIS_HYDROLOGY_EXAMPLE leak source beside open ground at " + shore[0] + "," + SyntheticHydrology.SEA_LEVEL + "," + shore[1];
assertTrue(result.lines().toString(), result.lines().contains(leak));
}
}
private static int[] seaShoreBesideWater(SyntheticHydrology fixture, HydrologyWindows.WindowRange range) {
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int z = range.minZ() * WindowKey.SIZE + 1; z < (range.maxZ() + 1) * WindowKey.SIZE - 1; z++) {
for (int x = range.minX() * WindowKey.SIZE + 1; x < (range.maxX() + 1) * WindowKey.SIZE - 1; x++) {
ChunkWaterRaster raster = fixture.raster(x >> 4, z >> 4);
if (raster.empty() || raster.role(x & 15, z & 15).wet() || fixture.naturalHeight(x, z) >= SyntheticHydrology.SEA_LEVEL
|| raster.terrainHeight(x & 15, z & 15) >= SyntheticHydrology.SEA_LEVEL) {
continue;
}
for (int[] offset : offsets) {
int nx = x + offset[0];
int nz = z + offset[1];
ChunkWaterRaster neighbour = fixture.raster(nx >> 4, nz >> 4);
if (neighbour.role(nx & 15, nz & 15).wet() && neighbour.waterHead(nx & 15, nz & 15) == SyntheticHydrology.SEA_LEVEL
&& fixture.terrainHeight(nx, nz) < SyntheticHydrology.SEA_LEVEL) {
return new int[]{x, z, nx, nz};
}
}
}
}
return null;
}
private static int[] dryColumnInTheSea(SyntheticHydrology fixture, HydrologyWindows.WindowRange range) {
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int z = range.minZ() * WindowKey.SIZE + 1; z < (range.maxZ() + 1) * WindowKey.SIZE - 1; z++) {
for (int x = range.minX() * WindowKey.SIZE + 1; x < (range.maxX() + 1) * WindowKey.SIZE - 1; x++) {
ChunkWaterRaster raster = fixture.raster(x >> 4, z >> 4);
WaterRole role = raster.role(x & 15, z & 15);
if (raster.empty() || role == WaterRole.NONE || role.wet() || fixture.naturalHeight(x, z) >= SyntheticHydrology.SEA_LEVEL
|| raster.terrainHeight(x & 15, z & 15) >= SyntheticHydrology.SEA_LEVEL) {
continue;
}
for (int[] offset : offsets) {
int nx = x + offset[0];
int nz = z + offset[1];
if (fixture.raster(nx >> 4, nz >> 4).role(nx & 15, nz & 15) == WaterRole.NONE
&& fixture.terrainHeight(nx, nz) < SyntheticHydrology.SEA_LEVEL) {
return new int[]{x, z, nx, nz};
}
}
}
}
return null;
}
@Test
public void inletAndCavePoolChecksFeedTheWindowAndFailTheProbe() throws Exception {
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
HydrologyReport.Result result = HydrologyProbe.measure(fixture.inputs(UnaryOperator.identity(),
(key, plan) -> new HydrologyUnderground.Counts(3, 2L, 5, 1L, List.of("inlet leak at 1,2,3", "pool air at 4,5,6"))), LAND);
Map<String, String> total = fields(line(result.lines(), "IRIS_HYDROLOGY_TOTAL"));
assertEquals("3", total.get("inlets"));
assertEquals("2", total.get("inletFailures"));
assertEquals("5", total.get("cavePools"));
assertEquals("1", total.get("cavePoolFailures"));
assertEquals("FAIL", total.get("status"));
assertTrue(total.get("missedTargets"), total.get("missedTargets").contains("inletFailures")
&& total.get("missedTargets").contains("cavePoolFailures"));
assertTrue(result.lines().toString(), result.lines().contains("IRIS_HYDROLOGY_EXAMPLE inlet leak at 1,2,3"));
}
}
private static int[] bankBesideWater(SyntheticHydrology fixture) {
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int min = LAND.minX() * HydrologyWindows.CHUNKS;
for (int chunkZ = min; chunkZ < min + HydrologyWindows.CHUNKS; chunkZ++) {
for (int chunkX = min; chunkX < min + HydrologyWindows.CHUNKS; chunkX++) {
ChunkWaterRaster raster = fixture.raster(chunkX, chunkZ);
for (int lz = 1; lz < 15; lz++) {
for (int lx = 1; lx < 15; lx++) {
int head = raster.waterHead(lx, lz);
if (!raster.role(lx, lz).wet() || head <= SyntheticHydrology.SEA_LEVEL) {
continue;
}
for (int[] offset : offsets) {
int nx = lx + offset[0];
int nz = lz + offset[1];
if (raster.waterHead(nx, nz) == Integer.MIN_VALUE && raster.terrainHeight(nx, nz) >= head) {
return new int[]{(chunkX << 4) + nx, (chunkZ << 4) + nz, head, offset[0], offset[1]};
}
}
}
}
}
}
return null;
}
private static String line(List<String> lines, String prefix) {
for (String line : lines) {
if (line.startsWith(prefix + " ")) {
return line;
}
}
throw new AssertionError("No " + prefix + " line in " + lines);
}
private static Map<String, String> fields(String line) {
Map<String, String> fields = new LinkedHashMap<>();
for (String token : line.trim().split(" ")) {
int equals = token.indexOf('=');
if (equals > 0) {
fields.put(token.substring(0, equals), token.substring(equals + 1));
}
}
return fields;
}
private static void assertNumber(String metric, String value) {
assertNotNull(metric + " missing", value);
Double.parseDouble(value);
}
}
@@ -0,0 +1,232 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HydrologyVoxelsTest {
private static final int HEIGHT = 32;
private static final int FLOOR = 9;
private static final byte FALLING_WATER = (byte) (HydrologyVoxels.WATER + 8);
@Test
public void classifiesFluidLevelsPlantsCaneAndSolids() {
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:water[level=0]", false)));
assertEquals(FALLING_WATER, HydrologyVoxels.classify(state("minecraft:water[level=8]", false)));
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:water", false)));
assertEquals(HydrologyVoxels.LAVA + 3, HydrologyVoxels.classify(state("minecraft:lava[level=3]", false)));
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:seagrass", false)));
assertEquals(HydrologyVoxels.WATER, HydrologyVoxels.classify(state("minecraft:oak_stairs[waterlogged=true]", true)));
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(state("minecraft:short_grass", false)));
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(state("minecraft:red_tulip", false)));
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(state("minecraft:snow[layers=1]", false)));
assertEquals(HydrologyVoxels.CANE, HydrologyVoxels.classify(state("minecraft:sugar_cane[age=0]", false)));
assertEquals(HydrologyVoxels.SOLID, HydrologyVoxels.classify(state("minecraft:grass_block[snowy=false]", false)));
assertEquals(HydrologyVoxels.OPEN, HydrologyVoxels.classify(null));
}
@Test
public void waterloggableBlocksFullSnowAndMangroveRootsHoldWaterWhileCropsFireAndWireDoNot() {
for (String key : new String[]{"minecraft:glow_lichen[down=true,waterlogged=false]", "minecraft:hanging_roots[waterlogged=false]",
"minecraft:small_dripleaf[facing=north,half=lower,waterlogged=false]", "minecraft:big_dripleaf_stem[facing=north,waterlogged=false]",
"minecraft:rail[shape=north_south,waterlogged=false]", "minecraft:glow_lichen", "minecraft:hanging_roots",
"minecraft:mangrove_roots[waterlogged=false]", "minecraft:muddy_mangrove_roots[axis=y]", "minecraft:snow[layers=8]"}) {
assertEquals(key, HydrologyVoxels.SOLID, HydrologyVoxels.classify(state(key, false)));
}
for (String key : new String[]{"minecraft:wheat[age=3]", "minecraft:pumpkin_stem[age=2]", "minecraft:fire[age=0]",
"minecraft:redstone_wire[power=0]", "minecraft:crimson_roots", "minecraft:vine[up=true]"}) {
assertEquals(key, HydrologyVoxels.OPEN, HydrologyVoxels.classify(state(key, false)));
}
}
@Test
public void containedPondHasNoLeaks() {
HydrologyVoxels.ChunkBlocks chunk = pond(10);
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
assertEquals(9L, scan.waterCells);
assertEquals(0L, scan.containmentLeaks);
assertEquals(0L, scan.tickConversionCells);
}
@Test
public void sourceBesideOpenGroundOrOverOpenGroundLeaks() {
HydrologyVoxels.ChunkBlocks side = pond(10);
side.set(6, 10, 4, HydrologyVoxels.OPEN);
HydrologyVoxels.WaterScan sideScan = scan(volume(side));
assertEquals(1L, sideScan.containmentLeaks);
assertEquals("leak source beside open ground at 5,10,4", sideScan.examples.get(0));
HydrologyVoxels.ChunkBlocks under = pond(10);
under.set(4, FLOOR, 4, HydrologyVoxels.OPEN);
HydrologyVoxels.WaterScan underScan = scan(volume(under));
assertEquals(1L, underScan.containmentLeaks);
assertEquals("leak water over open ground at 4,10,4", underScan.examples.get(0));
}
@Test
public void sugarCaneBesideOrUnderWaterHoldsIt() {
HydrologyVoxels.ChunkBlocks chunk = pond(10);
chunk.set(6, 10, 4, HydrologyVoxels.CANE);
chunk.set(4, FLOOR, 4, HydrologyVoxels.CANE);
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
assertEquals(0L, scan.containmentLeaks);
}
@Test
public void sourceAboveItsHeadOverWaterBesideAirLeaks() {
HydrologyVoxels.ChunkBlocks chunk = pond(10);
clearAbove(chunk);
chunk.set(4, 11, 4, HydrologyVoxels.WATER);
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
assertEquals(1L, scan.containmentLeaks);
assertEquals("leak source beside open ground at 4,11,4", scan.examples.get(0));
}
@Test
public void fallingWaterOverWaterNeverSpreadsSideways() {
HydrologyVoxels.ChunkBlocks chunk = pond(10);
clearAbove(chunk);
for (int y = 11; y <= 13; y++) {
chunk.set(4, y, 4, FALLING_WATER);
}
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
assertEquals(0L, scan.containmentLeaks);
assertEquals(0L, scan.tickConversionCells);
}
@Test
public void flowingCellsAboveThePlannedHeadThatWouldConvertOrSpreadAreTickCells() {
HydrologyVoxels.ChunkBlocks converting = pond(10);
converting.set(4, 10, 4, FALLING_WATER);
HydrologyVoxels.WaterScan conversion = scan(volume(converting));
assertEquals(1L, conversion.tickConversionCells);
assertEquals("tick flowing cell converts to a source at 4,10,4", conversion.examples.get(0));
HydrologyVoxels.ChunkBlocks atHead = pond(10);
atHead.set(4, 10, 4, FALLING_WATER);
atHead.heads()[(4 << 4) | 4] = 10;
assertEquals(0L, scan(volume(atHead)).tickConversionCells);
HydrologyVoxels.ChunkBlocks spreading = solid();
spreading.set(4, 10, 4, (byte) (HydrologyVoxels.WATER + 3));
spreading.set(5, 10, 4, HydrologyVoxels.OPEN);
spreading.set(8, 10, 8, (byte) (HydrologyVoxels.WATER + 7));
spreading.set(9, 10, 8, HydrologyVoxels.OPEN);
HydrologyVoxels.Volume volume = volume(spreading);
HydrologyVoxels.WaterScan spread = new HydrologyVoxels.WaterScan();
spread.column(volume, 4, 4, FLOOR, HEIGHT - 1);
spread.column(volume, 8, 8, FLOOR, HEIGHT - 1);
assertEquals(1L, spread.tickConversionCells);
assertEquals("tick flowing cell spreads beside lower dry ground at 4,10,4", spread.examples.get(0));
assertEquals(0L, spread.containmentLeaks);
}
@Test
public void lavaNeverConvertsAndSpreadsOnlyWithMoreThanTwoLevelsLeft() {
HydrologyVoxels.ChunkBlocks pool = solid();
for (int z = 3; z <= 5; z++) {
for (int x = 3; x <= 5; x++) {
pool.set(x, 10, z, HydrologyVoxels.LAVA);
}
}
pool.set(4, 10, 4, (byte) (HydrologyVoxels.LAVA + 8));
assertEquals(0L, scan(volume(pool)).tickConversionCells);
HydrologyVoxels.ChunkBlocks spreading = solid();
spreading.set(4, 10, 4, (byte) (HydrologyVoxels.LAVA + 6));
spreading.set(5, 10, 4, HydrologyVoxels.OPEN);
spreading.set(8, 10, 8, (byte) (HydrologyVoxels.LAVA + 4));
spreading.set(9, 10, 8, HydrologyVoxels.OPEN);
HydrologyVoxels.Volume volume = volume(spreading);
HydrologyVoxels.WaterScan spread = new HydrologyVoxels.WaterScan();
spread.column(volume, 4, 4, FLOOR, HEIGHT - 1);
spread.column(volume, 8, 8, FLOOR, HEIGHT - 1);
assertEquals(1L, spread.tickConversionCells);
assertEquals("tick flowing cell spreads beside lower dry ground at 8,10,8", spread.examples.get(0));
}
@Test
public void plannedHeadsWithoutTheirFluidMissTheSurface() {
HydrologyVoxels.ChunkBlocks chunk = pond(10);
for (int z = 3; z <= 5; z++) {
for (int x = 3; x <= 5; x++) {
chunk.heads()[(z << 4) | x] = 10;
}
}
assertEquals(0L, scan(volume(chunk)).missingSurfaces);
chunk.set(4, 10, 4, HydrologyVoxels.SOLID);
chunk.surfaces()[(5 << 4) | 5] = HydrologyVoxels.LAVA;
HydrologyVoxels.WaterScan scan = scan(volume(chunk));
assertEquals(2L, scan.missingSurfaces);
assertEquals("missing planned surface at 4,10,4", scan.examples.get(0));
assertEquals("missing planned surface at 5,10,5", scan.examples.get(1));
chunk.surfaces()[(4 << 4) | 4] = HydrologyVoxels.SOLID;
chunk.surfaces()[(3 << 4) | 3] = HydrologyVoxels.SOLID;
assertEquals(1L, scan(volume(chunk)).missingSurfaces);
}
@Test
public void unknownNeighboursAreCensoredNotLeaks() {
HydrologyVoxels.ChunkBlocks chunk = solid();
chunk.set(15, 10, 4, HydrologyVoxels.WATER);
HydrologyVoxels.WaterScan scan = new HydrologyVoxels.WaterScan();
scan.column(volume(chunk), 15, 4, FLOOR, HEIGHT - 1);
assertEquals(0L, scan.containmentLeaks);
assertEquals(1L, scan.censoredCells);
}
private static HydrologyVoxels.WaterScan scan(HydrologyVoxels.Volume volume) {
HydrologyVoxels.WaterScan scan = new HydrologyVoxels.WaterScan();
for (int z = 2; z <= 6; z++) {
for (int x = 2; x <= 6; x++) {
scan.column(volume, x, z, FLOOR, HEIGHT - 1);
}
}
return scan;
}
private static HydrologyVoxels.Volume volume(HydrologyVoxels.ChunkBlocks chunk) {
HydrologyVoxels.Volume volume = new HydrologyVoxels.Volume();
volume.put(chunk);
return volume;
}
private static void clearAbove(HydrologyVoxels.ChunkBlocks chunk) {
for (int y = 11; y <= 14; y++) {
for (int z = 3; z <= 5; z++) {
for (int x = 3; x <= 5; x++) {
chunk.set(x, y, z, HydrologyVoxels.OPEN);
}
}
}
}
private static HydrologyVoxels.ChunkBlocks pond(int level) {
HydrologyVoxels.ChunkBlocks chunk = solid();
for (int z = 3; z <= 5; z++) {
for (int x = 3; x <= 5; x++) {
chunk.set(x, level, z, HydrologyVoxels.WATER);
}
}
return chunk;
}
private static HydrologyVoxels.ChunkBlocks solid() {
HydrologyVoxels.ChunkBlocks chunk = HydrologyVoxels.ChunkBlocks.empty(0, 0, HEIGHT);
for (int y = 0; y <= 14; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
chunk.set(x, y, z, HydrologyVoxels.SOLID);
}
}
}
return chunk;
}
private static NativeBlockState state(String key, boolean waterlogged) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
when(state.isAir()).thenReturn(false);
when(state.isWaterLogged()).thenReturn(waterlogged);
return state;
}
}
@@ -1,130 +1,107 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import art.arcane.iris.generation.hydrology.HydrologyPoint;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class RiverTransectProbeTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void sectionStationsSitAtTenThirtyFiftySeventyAndNinetyPercent() {
assertArrayEquals(new int[] {2, 6, 10, 14, 18}, RiverTransectProbe.sectionStations(20));
assertArrayEquals(new int[] {0, 0, 0, 0, 0}, RiverTransectProbe.sectionStations(1));
assertArrayEquals(new int[] {0, 0, 1, 2, 2}, RiverTransectProbe.sectionStations(3));
public void stationsSitAtTenThirtyFiftySeventyAndNinetyPercentOfTheLength() {
assertArrayEquals(new int[]{2, 6, 10, 14, 18}, RiverTransectProbe.stations(straight(21, 0f, 100f, 4f)));
assertArrayEquals(new int[]{1, 1, 1, 1, 1}, RiverTransectProbe.stations(straight(2, 0f, 100f, 4f)));
}
@Test
public void summaryCountsCutsBankStepsOceanWritesAndSpillingChannels() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
put(columns, 0, 0, 72, 68, 70, RiverTransectProbe.Role.CHANNEL);
put(columns, 1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, -1, 0, 72, 69, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, 0, 1, 72, 70, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
put(columns, 0, -1, 72, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
put(columns, 2, 0, 72, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
put(columns, 5, 5, 60, 58, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.NONE);
put(columns, 6, 6, 60, 60, 63, RiverTransectProbe.Role.APRON);
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(7L, 12), columns);
assertEquals(7L, summary.id());
assertEquals(12, summary.stations());
assertEquals(4, summary.ownedColumns());
assertEquals(1, summary.minimumCut());
assertEquals(4, summary.maximumCut());
assertEquals(1, summary.maximumBankStep());
assertEquals(1, summary.oceanWrites());
assertEquals(1, summary.uncontainedWetCells());
assertFalse(summary.passes());
assertEquals(3, summary.details().size());
assertTrue(summary.details().get(0).startsWith("uncontained 0,0"));
assertTrue(summary.details().get(1).startsWith("oceanWrite 5,5"));
assertTrue(summary.details().get(2).startsWith("bankStep 1"));
public void sectionReadsColumnsAcrossTheFlow() {
Reach reach = straight(11, 0f, 100f, 4f);
RiverTransectProbe.Section section = RiverTransectProbe.section(reach, 5, (x, z) -> Math.abs(z - 100) <= 2
? new RiverTransectProbe.Column(70, 66, 68, WaterRole.CHANNEL)
: z > 140 ? null : new RiverTransectProbe.Column(70, 70, RiverTransectProbe.NO_WATER, WaterRole.BANK));
RiverTransectProbe.Column[] columns = section.columns();
assertEquals(2 * RiverTransectProbe.SECTION_HALF + 1, columns.length);
assertEquals(0.5D, section.station(), 1e-6D);
assertEquals(68, columns[RiverTransectProbe.SECTION_HALF].head());
assertEquals(68, columns[RiverTransectProbe.SECTION_HALF + 2].head());
assertEquals(RiverTransectProbe.NO_WATER, columns[RiverTransectProbe.SECTION_HALF + 3].head());
assertEquals(RiverTransectProbe.NO_WATER, columns[RiverTransectProbe.SECTION_HALF - 3].head());
assertNull(columns[2 * RiverTransectProbe.SECTION_HALF]);
assertEquals(WaterRole.BANK, columns[0].role());
}
@Test
public void containedCourseAwayFromTheOceanPasses() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
put(columns, 0, 0, 72, 68, 70, RiverTransectProbe.Role.CHANNEL);
put(columns, 1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, -1, 0, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
put(columns, 0, 1, 72, 70, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
put(columns, 0, -1, 72, 71, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.SHORE);
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(1L, 3), columns);
assertTrue(summary.passes());
assertEquals(0, summary.oceanWrites());
assertEquals(0, summary.uncontainedWetCells());
public void summaryListsReachesPondsAndSections() {
HydrologySettings settings = HydrologySettings.compile(new IrisHydrology().setEnabled(true), 1L, 40, 0, 320);
Reach reach = straight(11, 0f, 100f, 4f);
RiverTransectProbe.Section section = RiverTransectProbe.section(reach, 5,
(x, z) -> new RiverTransectProbe.Column(70, 66, Math.abs(z - 100) <= 2 ? 68 : RiverTransectProbe.NO_WATER, WaterRole.CHANNEL));
List<String> summary = RiverTransectProbe.summary(new WindowKey(0, 0),
new WindowPlan(new WindowKey(0, 0), List.of(reach), List.of(), List.of()), List.of(),
List.of(section), settings);
assertEquals("window 0,0 reaches=1 ponds=0 lakes=0 sections=1", summary.get(0));
assertTrue(summary.get(1), summary.get(1).startsWith("reach 7 tier=stream length=40 from=0,100@70 to=40,100@60"));
assertEquals("section reach=7 station=50% at=20,100 wetColumns=5 deepest=2", summary.get(2));
}
@Test
public void drySillMouthCutsConnectToUntouchedOceanWithoutCountingAsOceanBedWrites() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
columns.put(RiverFootprint.pack(0, 0), new RiverTransectProbe.ColumnView(
0, 0, 63, 60, 63, RiverTransectProbe.Role.CHANNEL,
new RiverTransectProbe.ColumnPlan(false, true, true, true, 60, 1L)));
put(columns, 1, 0, 60, 60, 63, RiverTransectProbe.Role.APRON);
RiverTransectProbe.CourseSummary connected = RiverTransectProbe.summarize(options(1L, 2), columns);
assertEquals(1, connected.drySillCuts());
assertEquals(0, connected.oceanWrites());
assertEquals(1, connected.connectedMouthColumns());
assertTrue(connected.passes());
columns.remove(RiverFootprint.pack(1, 0));
assertEquals(1, RiverTransectProbe.summarize(options(1L, 2), columns).disconnectedMouthColumns());
}
@Test
public void bankCutBudgetUsesPublishedLowerFloorEvenWhenUpperTerrainSpanIsRetained() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
columns.put(RiverFootprint.pack(0, 0), new RiverTransectProbe.ColumnView(
0, 0, 80, 80, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK,
new RiverTransectProbe.ColumnPlan(false, true, false, false, 70, 1L)));
RiverTransectProbe.CourseSummary summary = RiverTransectProbe.summarize(options(1L, 2), columns);
assertEquals(10, summary.maximumBankCut());
assertEquals(10, summary.bankExcavation());
assertEquals(1, summary.bankBudgetViolations());
assertFalse(summary.passes());
}
@Test
public void reconstructedReachVolumesUseTheSharedSixteenBlockMetric() {
Map<Long, RiverTransectProbe.ColumnView> columns = new HashMap<>();
for (int x = 0; x <= 32; x++) {
put(columns, x, 2, 80, 72, RiverTransectProbe.NO_WATER, RiverTransectProbe.Role.BANK);
public void rendersPlanSectionsAndSummaryForASyntheticWindow() throws Exception {
try (SyntheticHydrology fixture = new SyntheticHydrology()) {
HydrologyProbe.Inputs inputs = fixture.inputs();
HydrologyWindows.WindowRange range = new HydrologyWindows.WindowRange(4, 4, 4, 4);
HydrologyPlanHarness.Plans plans = new HydrologyPlanHarness.Plans(inputs.settings(), inputs.terrain(), inputs.naturalHeight(), range);
File output = folder.newFolder("transect");
RiverTransectProbe.Rendered rendered = RiverTransectProbe.render(plans, new WindowKey(4, 4), inputs.naturalHeight(), range, output);
assertEquals(plans.window(new WindowKey(4, 4)).plan().reaches().size(), rendered.reaches());
assertEquals(plans.window(new WindowKey(4, 4)).plan().ponds().size(), rendered.ponds());
assertTrue("reaches " + rendered.reaches(), rendered.reaches() > 0);
assertTrue("sections " + rendered.sections(), rendered.sections() > 0);
BufferedImage plan = ImageIO.read(new File(rendered.directory(), "plan.png"));
assertEquals(WindowKey.SIZE, plan.getWidth());
assertEquals(WindowKey.SIZE, plan.getHeight());
assertTrue(new File(rendered.directory(), "sections.png").isFile());
List<String> summary = Files.readAllLines(new File(rendered.directory(), "summary.txt").toPath());
assertTrue(summary.get(0), summary.get(0).startsWith("window 4,4 reaches=" + rendered.reaches() + " ponds=" + rendered.ponds()));
assertTrue(rendered.line(), rendered.line().startsWith("IRIS_RIVER_TRANSECT version=2 wx=4 wz=4 "));
}
assertEquals(9, RiverTransectProbe.estimatedBankVolumePerBlock(options(1L, 33), columns));
assertEquals(0, RiverTransectProbe.estimatedBankVolumePerBlock(options(2L, 33), columns));
}
private static RiverTransectProbe.SummaryOptions options(long id, int stations) {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 70, 0), new HydrologyPoint(stations - 1, 70, 0)));
return new RiverTransectProbe.SummaryOptions(id, centerline, 63,
new HydrologyPlannerSettings.Excavation(8, 16, 128));
}
private static void put(
Map<Long, RiverTransectProbe.ColumnView> columns,
int x,
int z,
int natural,
int terrain,
int water,
RiverTransectProbe.Role role
) {
columns.put(RiverFootprint.pack(x, z), new RiverTransectProbe.ColumnView(x, z, natural, terrain, water, role,
new RiverTransectProbe.ColumnPlan(role == RiverTransectProbe.Role.APRON,
role.owned(), role == RiverTransectProbe.Role.CHANNEL, false, terrain, 1L)));
private static Reach straight(int points, float x0, float z, float spacing) {
HydrologySettings settings = HydrologySettings.compile(new IrisHydrology().setEnabled(true), 1L, 40, 0, 320);
float[] xs = new float[points];
float[] zs = new float[points];
int[] grade = new int[points];
float[] halfWidth = new float[points];
float[] depth = new float[points];
ReachStyle[] style = new ReachStyle[points];
for (int point = 0; point < points; point++) {
xs[point] = x0 + point * spacing;
zs[point] = z;
grade[point] = 70 - point;
halfWidth[point] = 2f;
depth[point] = 1f;
}
Arrays.fill(style, ReachStyle.RUN);
return new Reach(7L, settings.tiers().get(0), 0L, xs, zs, grade, halfWidth, depth, style, 1000f);
}
}
@@ -0,0 +1,39 @@
package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SeaInletChecksTest {
@Test
public void roofBreachesAndLooseBedsAreReportedButOnlyContainmentAndSealChecksFail() {
SeaInletProbe.Checks reported = new SeaInletProbe.Checks(3, 0L, 0L, 0L, 0L, 1939L, 0L, 0L, 0L, 0L, 952L, List.of());
assertEquals(0L, reported.failures());
SeaInletProbe.Checks failing = new SeaInletProbe.Checks(3, 1L, 2L, 3L, 4L, 0L, 5L, 6L, 7L, 8L, 0L, List.of());
assertEquals(36L, failing.failures());
}
@Test
public void inletCellsAreClassifiedAgainstTheDimensionSeaFluid() {
assertEquals(SeaInletProbe.classify(state("minecraft:water"), "minecraft:water"), SeaInletProbe.classify(state("minecraft:lava"),
"minecraft:lava"));
assertEquals(SeaInletProbe.classify(state("minecraft:water[level=8]"), "minecraft:water"),
SeaInletProbe.classify(state("minecraft:lava[level=8]"), "minecraft:lava"));
assertEquals(SeaInletProbe.classify(state("minecraft:lava"), "minecraft:water"), SeaInletProbe.classify(state("minecraft:water"),
"minecraft:lava"));
assertNotEquals(SeaInletProbe.classify(state("minecraft:lava"), "minecraft:lava"), SeaInletProbe.classify(state("minecraft:lava"),
"minecraft:water"));
}
private static NativeBlockState state(String key) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn(key);
return state;
}
}
@@ -85,6 +85,28 @@ public final class StubPlatformStateTest {
assertTrue(stone.isSolid());
}
@Test
public void vanillaSolidityLeavesPlantsVinesAndCaneOpenAndTheDefaultUnchanged() {
String[] open = {"minecraft:vine[east=false,north=true,south=false,up=false,west=false]", "minecraft:short_grass",
"minecraft:sugar_cane[age=0]", "minecraft:seagrass", "minecraft:oak_sapling[stage=0]"};
String[] solid = {"minecraft:oak_log[axis=y]", "minecraft:oak_leaves[distance=1,persistent=false,waterlogged=false]",
"minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=true]"};
for (String key : open) {
assertTrue(key, state(key).isSolid());
}
StubPlatform.vanillaSolidity(true);
try {
for (String key : open) {
assertFalse(key, state(key).isSolid());
}
for (String key : solid) {
assertTrue(key, state(key).isSolid());
}
} finally {
StubPlatform.vanillaSolidity(false);
}
}
@Test
public void classifiesCanonicalWaterloggedProperty() {
assertTrue(state("minecraft:brain_coral_fan[waterlogged=true]").isWaterLogged());
@@ -0,0 +1,180 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.config.IrisHydrology;
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanners;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.grid.CellHash;
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
import art.arcane.iris.generation.hydrology.plan.HydrologyPlanCache;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.function.UnaryOperator;
final class SyntheticHydrology implements AutoCloseable {
static final long SEED = 1337L;
static final int SEA_LEVEL = 40;
static final int HEIGHT = 320;
private static final byte ALLOW_ALL = HydrologySampleBuffer.ALLOW_STREAMS | HydrologySampleBuffer.ALLOW_PONDS;
private static final long SALT_NOISE = 0x4e4f495345L;
private static final byte FALLING_WATER = (byte) (HydrologyVoxels.WATER + 8);
private final HydrologySettings settings;
private final Terrain terrain;
private final ForkJoinPool planningPool;
private final ExecutorService workers;
private final HydrologyRuntime runtime;
SyntheticHydrology() {
this.settings = HydrologySettings.compile(new IrisHydrology().setEnabled(true), SEED, SEA_LEVEL, 0, HEIGHT);
this.terrain = new Terrain();
this.planningPool = new ForkJoinPool(4);
this.workers = Executors.newFixedThreadPool(4);
HydrologyPlanners planners = HydrologyPlanners.create(settings, terrain);
this.runtime = new HydrologyRuntime(settings, HydrologyPlanCache.create(settings, planners.regions(), planners.windows(), planningPool,
() -> false), terrain::naturalHeight, () -> false, System::nanoTime);
}
HydrologyProbe.Inputs inputs() {
return inputs(UnaryOperator.identity());
}
HydrologyProbe.Inputs inputs(UnaryOperator<HydrologyVoxels.ChunkBlocks> edit) {
return inputs(edit, (key, plan) -> new HydrologyUnderground.Counts(plan.inlets().size(), 0L, 0, 0L, List.of()));
}
HydrologyProbe.Inputs inputs(UnaryOperator<HydrologyVoxels.ChunkBlocks> edit, HydrologyUnderground.Source underground) {
return new HydrologyProbe.Inputs(settings, terrain, terrain::naturalHeight, planningPool, workers,
(chunkX, chunkZ) -> edit.apply(blocks(chunkX, chunkZ)), underground, HEIGHT);
}
ChunkWaterRaster raster(int chunkX, int chunkZ) {
return runtime.raster(chunkX, chunkZ);
}
int naturalHeight(int x, int z) {
return terrain.naturalHeight(x, z);
}
int terrainHeight(int x, int z) {
ChunkWaterRaster raster = raster(x >> 4, z >> 4);
return raster.empty() ? terrain.naturalHeight(x, z) : raster.terrainHeight(x & 15, z & 15);
}
private HydrologyVoxels.ChunkBlocks blocks(int chunkX, int chunkZ) {
HydrologyVoxels.ChunkBlocks blocks = HydrologyVoxels.ChunkBlocks.empty(chunkX, chunkZ, HEIGHT);
ChunkWaterRaster own = raster(chunkX, chunkZ);
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
int x = (chunkX << 4) + lx;
int z = (chunkZ << 4) + lz;
int ground = terrainHeight(x, z);
int head = own.waterHead(lx, lz);
blocks.heads()[(lz << 4) | lx] = head;
for (int y = 0; y <= ground; y++) {
blocks.set(lx, y, lz, HydrologyVoxels.SOLID);
}
int surface = head != Integer.MIN_VALUE ? head : ground < SEA_LEVEL ? SEA_LEVEL : ground;
for (int y = ground + 1; y <= surface; y++) {
blocks.set(lx, y, lz, HydrologyVoxels.WATER);
}
if (head != Integer.MIN_VALUE) {
for (int y = head + 1; y <= highestNeighbour(x, z); y++) {
blocks.set(lx, y, lz, FALLING_WATER);
}
}
}
}
return blocks;
}
private int highestNeighbour(int x, int z) {
int highest = Integer.MIN_VALUE;
for (int[] offset : new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}) {
int nx = x + offset[0];
int nz = z + offset[1];
highest = Math.max(highest, raster(nx >> 4, nz >> 4).waterHead(nx & 15, nz & 15));
}
return highest;
}
@Override
public void close() {
runtime.close();
workers.shutdownNow();
planningPool.shutdownNow();
}
static final class Terrain implements HydrologyTerrainSource {
@Override
public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) {
for (int index = 0; index < xs.length; index++) {
float value = (float) height(xs[index], zs[index]);
out.height()[index] = value;
out.ocean()[index] = value <= SEA_LEVEL;
out.rainfall()[index] = 1f;
out.pondDensity()[index] = 1f;
out.permission()[index] = ALLOW_ALL;
out.fluidProfile()[index] = null;
}
}
int naturalHeight(int x, int z) {
return Math.round((float) height(x, z));
}
@Override
public int seaLevel() {
return SEA_LEVEL;
}
@Override
public int minY() {
return 0;
}
@Override
public int maxY() {
return HEIGHT;
}
private static double height(double x, double z) {
double height = 70D;
double amplitude = 60D;
double wavelength = 4096D;
for (int octave = 0; octave < 6; octave++) {
height += amplitude * noise(SEED + octave, x / wavelength, z / wavelength);
amplitude *= 0.5D;
wavelength *= 0.5D;
}
return height;
}
private static double noise(long seed, double x, double z) {
int x0 = (int) Math.floor(x);
int z0 = (int) Math.floor(z);
double fx = smooth(x - x0);
double fz = smooth(z - z0);
double top = lerp(corner(seed, x0, z0), corner(seed, x0 + 1, z0), fx);
double bottom = lerp(corner(seed, x0, z0 + 1), corner(seed, x0 + 1, z0 + 1), fx);
return lerp(top, bottom, fz);
}
private static double corner(long seed, int x, int z) {
return 2D * CellHash.unit(CellHash.mix(seed, SALT_NOISE, x, z)) - 1D;
}
private static double smooth(double t) {
return t * t * (3D - 2D * t);
}
private static double lerp(double a, double b, double t) {
return a + (b - a) * t;
}
}
}