mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user