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

@@ -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;
}
}
}