Improved riverbed generation

- Pond levels follow intersecting lower channels
- Pond sampling covers requested flood margins
- Water steps align with sea level
- River crests grade into surrounding beds
- Riverbeds include varied cross sections and finer noise
- Submerged soils use stable block types
- Hydrology probes inspect wet bed geometry
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-26 18:01:26 -04:00
1 parent 494cdb1a5c
commit 75837d757b
25 files changed
+1566 -144

No files matched your search

@@ -4,6 +4,7 @@ import art.arcane.iris.generation.hydrology.config.IrisHydrology;
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import art.arcane.iris.generation.hydrology.water.CupSet;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.water.PondKind;
import art.arcane.iris.generation.hydrology.water.PondMask;
@@ -24,6 +25,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -32,6 +34,8 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
public class WindowPlannerTest {
@@ -49,6 +53,51 @@ public class WindowPlannerTest {
assertTrue(plan.ponds().isEmpty());
}
@Test
public void shrinkingPondReplansUntilItsChangedOutletStopsLowering() {
HydrologySettings settings = settings(new IrisHydrology());
SyntheticTerrain terrain = SyntheticTerrain.flat(100D).withPit(64D, 64D, 20D, 20D);
int width = 41;
long[] bits = new long[(width * width + 63) >>> 6];
for (int z = -20; z <= 20; z++) {
for (int x = -20; x <= 20; x++) {
if (x * x + z * z < 400) {
int cell = (z + 20) * width + x + 20;
bits[cell >>> 6] |= 1L << cell;
}
}
}
Pond initial = new Pond(19L, PondKind.NATURAL, 100, 80, 84, 64, 100f,
new PondMask(44, 44, 1, width, width, bits), 0L, 0, "water", 64, 64);
PondPlanner ponds = spy(new PondPlanner(settings));
doReturn(CupSet.EMPTY).when(ponds).subtractCups(any(), any(), any(), any());
doReturn(List.of(initial)).when(ponds).ponds(any(), any(), any(), any(), any(), any());
ReachShaper shaper = mock(ReachShaper.class);
AtomicInteger passes = new AtomicInteger();
doAnswer(call -> {
List<Pond> water = call.getArgument(4);
Pond pond = single(water);
passes.incrementAndGet();
int head = pond.level() > 96 ? 96 : 94;
Reach outlet = new Reach(20L, settings.tiers().get(0), 0L, new float[]{64f, 64f}, new float[]{32f, 96f},
new int[]{head, head}, new float[]{2f, 2f}, new float[]{2f, 2f}, new ReachStyle[]{ReachStyle.RUN, ReachStyle.RUN}, 1000f);
return new ShapedNetwork(List.of(outlet), List.of(pond.withOutletReachId(outlet.id())));
}).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong());
WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18),
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 18), ponds, shaper);
WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN));
Pond pond = single(settled.allPonds());
assertEquals(3, passes.get());
assertEquals(94, pond.level());
assertEquals(20L, pond.outletReachId());
assertTrue(initial.mask().contains(83, 64));
assertFalse(pond.mask().contains(83, 64));
assertTrue(pond.mask().contains(64, 64));
assertEquals(94, settled.allReaches().get(0).grade()[0]);
}
@Test
public void burnedTrunkCapturesLocalDrainage() {
HydrologySettings settings = settings(new IrisHydrology());
@@ -0,0 +1,96 @@
package art.arcane.iris.generation.hydrology.raster;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class BedProfileTest {
@Test
public void fineDetailHasBoundedAmplitudeAndAdjacentSteps() {
double minimum = Double.POSITIVE_INFINITY;
double maximum = Double.NEGATIVE_INFINITY;
for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE}) {
for (int x = -48; x <= 48; x++) {
for (int z = -48; z <= 48; z++) {
double detail = BedProfile.detail(seed, x, z);
minimum = Math.min(minimum, detail);
maximum = Math.max(maximum, detail);
assertTrue(Math.abs(detail) <= 0.35D);
assertTrue(Math.abs(detail - BedProfile.detail(seed, x + 1, z)) <= 0.35D);
assertTrue(Math.abs(detail - BedProfile.detail(seed, x, z + 1)) <= 0.35D);
}
}
}
assertTrue(minimum < -0.3D);
assertTrue(maximum > 0.3D);
}
@Test
public void fineDetailBreaksUpLevelContoursWithoutTallFaces() {
int changedChannels = 0;
int changedPonds = 0;
for (int x = -64; x <= 64; x++) {
for (int z = -32; z <= 32; z++) {
double detail = BedProfile.detail(1337L, x, z);
int channel = BedProfile.channelDepth(6D, 0.5D, 0D, detail);
int pond = BedProfile.pondDepth(8, 0.5D, 0D, detail);
changedChannels += channel != BedProfile.channelDepth(6D, 0.5D, 0D, 0D) ? 1 : 0;
changedPonds += pond != BedProfile.pondDepth(8, 0.5D, 0D, 0D) ? 1 : 0;
assertTrue(channel == 4 || channel == 5);
assertTrue(pond == 4 || pond == 5);
}
}
assertTrue(changedChannels > 1000);
assertTrue(changedPonds > 1000);
}
@Test
public void depthLimitsAndShallowEdgesSurviveFineDetail() {
for (int maximum : new int[]{1, 2, 6, 8, 16, 64}) {
for (double variation : new double[]{-1D, -0.5D, 0D, 0.5D, 1D}) {
for (double detail : new double[]{-0.35D, 0D, 0.35D}) {
assertEquals(1, BedProfile.channelDepth(maximum, 1D, variation, detail));
assertEquals(1, BedProfile.pondDepth(maximum, 0D, variation, detail));
for (int step = 0; step <= 100; step++) {
double position = step / 100D;
int channel = BedProfile.channelDepth(maximum, position, variation, detail);
int pond = BedProfile.pondDepth(maximum, position, variation, detail);
assertTrue(channel >= 1 && channel <= maximum);
assertTrue(pond >= 1 && pond <= maximum);
assertTrue(Math.abs(channel - BedProfile.channelDepth(maximum, position, variation, 0D)) <= 1);
assertTrue(Math.abs(pond - BedProfile.pondDepth(maximum, position, variation, 0D)) <= 1);
}
}
}
}
}
@Test
public void detailUsesWorldCoordinatesAcrossNegativeChunkBoundaries() {
double[][] expected = new double[66][66];
for (int x = -33; x <= 32; x++) {
for (int z = -33; z <= 32; z++) {
expected[x + 33][z + 33] = BedProfile.detail(1337L, x, z);
}
}
int differentSeeds = 0;
for (int chunkX = 2; chunkX >= -3; chunkX--) {
for (int chunkZ = 2; chunkZ >= -3; chunkZ--) {
for (int localX = 16; localX >= -1; localX--) {
for (int localZ = 16; localZ >= -1; localZ--) {
int x = (chunkX << 4) + localX;
int z = (chunkZ << 4) + localZ;
if (x < -33 || x > 32 || z < -33 || z > 32) {
continue;
}
double detail = BedProfile.detail(1337L, x, z);
assertEquals(expected[x + 33][z + 33], detail, 0D);
differentSeeds += detail != BedProfile.detail(1338L, x, z) ? 1 : 0;
}
}
}
}
assertTrue(differentSeeds > 4000);
}
}
@@ -47,6 +47,41 @@ public class ChunkWaterRasterTest {
private static final float SPACING = 2f;
private static final int GENTLE_STEP = 3;
@Test
public void flatLakesSkipExtraTerrainSamplingForRiffles() {
AtomicInteger samples = new AtomicInteger();
Scene scene = new Scene(settings(new IrisHydrology()), (x, z) -> {
samples.incrementAndGet();
return 70;
}).lake(pond(8L, PondKind.LAKE, 80, circle(8, 8, 100, 1), 8, 8, 0));
assertEquals(80, scene.raster(0, 0).waterHead(8, 8));
assertEquals(320, samples.get());
}
@Test
public void shallowStandingWaterRetainsItsFootprintWhenItsHeadLowersOneBlock() {
int desiredLevel = 75;
Pond water = pond(9L, PondKind.NATURAL, desiredLevel, circle(-16, -16, 12, 1), -16, -16, 0);
HydrologySettings settings = settings(new IrisHydrology());
for (Scene scene : List.of(new Scene(settings, (x, z) -> desiredLevel - 1).pond(water),
new Scene(settings, (x, z) -> desiredLevel - 1).lake(water))) {
int wet = 0;
for (int x = -32; x <= 0; x++) {
for (int z = -32; z <= 0; z++) {
if (!water.mask().contains(x, z)) {
continue;
}
wet++;
assertEquals("head at " + x + "," + z, desiredLevel - 1, scene.head(x, z));
assertTrue("bed at " + x + "," + z, scene.terrain(x, z) < scene.head(x, z));
}
}
assertTrue("wet footprint " + wet, wet > 400);
assertEquals(0, scene.leaks(-32, -32, 0, 0));
}
}
@Test
public void channelBedBelowWaterHead() {
IntBinaryOperator natural = (x, z) -> x >= 40 && x < 50 ? 82 : 90 + (int) Math.round(2D * Math.sin(x / 9D));
@@ -62,15 +97,15 @@ public class ChunkWaterRasterTest {
channel++;
int bed = scene.terrain(x, z);
int ground = natural.applyAsInt(x, z);
assertEquals(87, scene.head(x, z));
assertTrue("bed " + bed + " below the water head at " + x + "," + z, bed < 87);
assertEquals(86, scene.head(x, z));
assertTrue("bed " + bed + " below the water head at " + x + "," + z, bed < 86);
assertTrue("bed never above natural at " + x + "," + z, bed <= ground);
if (ground < 84) {
pooled++;
assertEquals("a depression under the channel keeps its natural floor at " + x + "," + z, ground, bed);
}
}
assertTrue("full depth on the centre line at x=" + x, 87 - scene.terrain(x, 8) >= 3);
assertTrue("the centre line keeps a submerged bed at x=" + x, 86 - scene.terrain(x, 8) >= 2);
}
assertTrue("channel columns " + channel, channel > 900);
assertTrue("pooled columns " + pooled, pooled > 50);
@@ -99,6 +134,69 @@ public class ChunkWaterRasterTest {
assertEquals("far columns sit at the cap", ChunkWaterRaster.MAX_SHORE_DISTANCE, scene.shore(0, -40));
}
@Test
public void channelBedsAndBanksVaryAlongConstantWidthReaches() {
IrisHydrology config = new IrisHydrology();
config.getDeformation().setBankNoise(0D);
HydrologySettings settings = settings(config);
HydrologyTier river = tier("river", 8f, 0.75f, 24, IrisBankStyle.SMOOTH);
Reach reach = line(15L, river, 0L, -112f, 0f, 112f, 0f, (x, z) -> 100);
Scene scene = new Scene(settings, (x, z) -> 120).reach(reach);
Set<Integer> bedHeights = new HashSet<>();
Set<Integer> bankHeights = new HashSet<>();
int previousBed = scene.terrain(-80, 4);
int previousBank = scene.terrain(-80, 20);
for (int x = -80; x <= 80; x++) {
int bed = scene.terrain(x, 4);
int bank = scene.terrain(x, 20);
bedHeights.add(bed);
bankHeights.add(bank);
assertEquals(100, scene.head(x, 4));
assertEquals(Integer.MIN_VALUE, scene.head(x, 20));
assertTrue("depth stays within the tier limit", bed >= 94 && bed < 100);
assertTrue("bank stays between the water and terrain", bank >= 100 && bank <= 120);
assertTrue("bed variation remains coherent across chunk boundaries", Math.abs(bed - previousBed) <= 1);
assertTrue("bank variation remains coherent across chunk boundaries", Math.abs(bank - previousBank) <= 1);
previousBed = bed;
previousBank = bank;
}
assertTrue("constant-width bed has varying contours", bedHeights.size() >= 3);
assertTrue("constant-distance bank has varying contours", bankHeights.size() >= 3);
}
@Test
public void pondShelvesAndTapersVaryWithoutChangingWaterCoverage() {
HydrologySettings settings = settings(new IrisHydrology());
long[] bits = new long[192 * 64 / 64];
Arrays.fill(bits, -1L);
Pond water = pond(16L, PondKind.NATURAL, 100, new PondMask(-96, -32, 1, 192, 64, bits), 0, 0, 0);
Scene scene = new Scene(settings, (x, z) -> 110).pond(water);
Set<Integer> shelfHeights = new HashSet<>();
Set<Integer> deepHeights = new HashSet<>();
Set<Integer> taperHeights = new HashSet<>();
for (int x = -64; x <= 64; x++) {
shelfHeights.add(scene.terrain(x, -28));
deepHeights.add(scene.terrain(x, 0));
taperHeights.add(scene.terrain(x, -40));
for (int z = -48; z <= 47; z++) {
assertEquals("the shoreline remains fixed", water.mask().contains(x, z), scene.water(x, z));
if (scene.water(x, z)) {
assertEquals(100, scene.head(x, z));
assertTrue("pond depth remains bounded", scene.terrain(x, z) >= 92 && scene.terrain(x, z) < 100);
} else {
assertTrue("dry taper stays above water", scene.terrain(x, z) >= 100);
}
}
}
assertTrue("shelf contours vary along a straight shore", shelfHeights.size() >= 3);
assertTrue("deep floor has relief", deepHeights.size() >= 2);
assertTrue("dry taper varies along a straight shore", taperHeights.size() >= 2);
Scene reverse = new Scene(settings, (x, z) -> 110).pond(water);
for (int x = 64; x >= -64; x--) {
assertEquals("generation order leaves the floor unchanged", scene.terrain(x, -28), reverse.terrain(x, -28));
}
}
@Test
public void lakeRimShoreDistanceMatchesNearestLakeWater() {
IntBinaryOperator natural = (x, z) -> 60 + (int) (Math.hypot(x - 24, z - 24) / 2D);
@@ -189,14 +287,14 @@ public class ChunkWaterRasterTest {
wet++;
chunks.add(pack(Math.floorDiv(x, 16), Math.floorDiv(z, 16)));
assertEquals(pond.id(), scene.feature(x, z));
assertEquals("pond level at " + x + "," + z, 75, scene.head(x, z));
assertTrue("pond floor below the level at " + x + "," + z, scene.terrain(x, z) < 75);
assertEquals("pond level at " + x + "," + z, 74, scene.head(x, z));
assertTrue("pond floor below the level at " + x + "," + z, scene.terrain(x, z) < 74);
assertEquals("water", scene.fluid(x, z));
}
}
assertTrue("pond columns " + wet, wet > 400);
assertEquals("the pond spans four chunks", 4, chunks.size());
assertEquals("open water reaches full depth", 75 - 8, scene.terrain(16, 16));
assertTrue("open water stays within the maximum depth", scene.terrain(16, 16) >= 74 - 8 && scene.terrain(16, 16) < 74);
assertEquals(WaterRole.POND_SHORE, scene.role(16, 30));
}
@@ -206,7 +304,7 @@ public class ChunkWaterRasterTest {
float startZ = -20f;
float endX = 80f;
float endZ = 60f;
IntBinaryOperator natural = (x, z) -> 80 + (int) Math.floor(0.35D * perpendicular(x, z, startX, startZ, endX, endZ));
IntBinaryOperator natural = (x, z) -> 79 + (int) Math.floor(0.35D * perpendicular(x, z, startX, startZ, endX, endZ));
HydrologyTier river = tier("river", 3f, 0.5f, 12, IrisBankStyle.SMOOTH);
Scene scene = new Scene(settings(new IrisHydrology()), natural).reach(line(31L, river, 0L, startX, startZ, endX, endZ, (x, z) -> 79));
int interior = 0;
@@ -253,12 +351,12 @@ public class ChunkWaterRasterTest {
.reach(line(44L, stream, 0L, 8f, 8f, 56f, 8f, (x, z) -> Math.clamp(eastSlope.applyAsInt((int) x, (int) z) - 1, 94, 99)));
assertContained("chained ponds", chained, -16, -8, 80, 24, true);
IntBinaryOperator cliff = (x, z) -> (x < 24 ? 100 : 80) - Math.max(0, z - 8);
IntBinaryOperator cliff = (x, z) -> (x < 24 ? 99 : 79) - Math.max(0, z - 8);
Scene waterfall = new Scene(settings, cliff).reach(line(45L, stream, 0L, -24f, 8f, 72f, 8f, (x, z) -> x < 24 ? 99 : 79));
assertContained("waterfall", waterfall, -16, -8, 64, 24, true);
assertEquals(WaterRole.WATERFALL, waterfall.role(24, 8));
assertEquals(99, waterfall.head(22, 8));
assertEquals(79, waterfall.head(24, 8));
assertEquals(98, waterfall.head(22, 8));
assertEquals(78, waterfall.head(24, 8));
}
@Test
@@ -455,7 +553,7 @@ public class ChunkWaterRasterTest {
IrisHydrology config = new IrisHydrology();
config.getDeformation().setBankSlope(0D).setBankSteepFactor(0D).setBankNoise(0D);
HydrologySettings settings = settings(config);
IntBinaryOperator natural = (x, z) -> z >= 8 ? 70 + (z - 8) : 68 - (8 - z) / 4;
IntBinaryOperator natural = (x, z) -> z >= 8 ? 69 + (z - 8) : 67 - (8 - z) / 4;
Map<IrisBankStyle, int[]> cuts = new EnumMap<>(IrisBankStyle.class);
for (IrisBankStyle style : IrisBankStyle.values()) {
Scene scene = new Scene(settings, natural).reach(line(90L + style.ordinal(), tier("river", 2f, 0.5f, 16, style), 0L, -48f, 8f, 64f, 8f,
@@ -466,7 +564,7 @@ public class ChunkWaterRasterTest {
int height = scene.terrain(8, z);
assertFalse(style + " bank is dry at z=" + z, scene.water(8, z));
assertTrue(style + " bank role at z=" + z, scene.role(8, z) == WaterRole.BANK || scene.role(8, z) == WaterRole.SHORE);
assertTrue(style + " bank between water and natural at z=" + z + ": " + height, height >= 69 && height <= natural.applyAsInt(8, z));
assertTrue(style + " bank between water and natural at z=" + z + ": " + height, height >= 68 && height <= natural.applyAsInt(8, z));
assertTrue(style + " bank never falls toward the ground at z=" + z, outward == 1 || height >= cut[outward - 2]);
cut[outward - 1] = height;
int levee = scene.terrain(8, 6 - outward);
@@ -475,7 +573,7 @@ public class ChunkWaterRasterTest {
levee >= ground && levee <= ground + settings.deformation().maxFill());
}
assertEquals(style + " meets natural at the band edge", natural.applyAsInt(8, 26), cut[15]);
assertTrue(style + " levee holds the water", scene.terrain(8, 5) >= 70);
assertTrue(style + " levee holds the water", scene.terrain(8, 5) >= 69);
assertEquals(style + " levee meets natural at the band edge", natural.applyAsInt(8, -10), scene.terrain(8, -10));
assertEquals(style + " leaks", 0, scene.leaks(-16, -16, 32, 32));
cuts.put(style, cut);
@@ -494,16 +592,16 @@ public class ChunkWaterRasterTest {
assertTrue(ordered);
int[] cliff = cuts.get(IrisBankStyle.CLIFF);
for (int index = 0; index < 8; index++) {
assertEquals("cliff bench at outward " + (index + 1), 69, cliff[index]);
assertEquals("cliff bench at outward " + (index + 1), 68, cliff[index]);
}
assertEquals("cliff wall reaches natural", natural.applyAsInt(8, 22), cliff[11]);
int[] terraced = cuts.get(IrisBankStyle.TERRACED);
boolean level = false;
for (int index = 0; index < 15; index++) {
if (terraced[index] < natural.applyAsInt(8, 11 + index)) {
assertEquals("terrace step height at outward " + (index + 1), 0, (terraced[index] - 69) % 3);
assertEquals("terrace step height at outward " + (index + 1), 0, (terraced[index] - 68) % 3);
}
level |= terraced[index] > 69 && terraced[index] == terraced[index + 1];
level |= terraced[index] > 68 && terraced[index] == terraced[index + 1];
}
assertTrue("terraces hold level steps", level);
List<IrisBankStyle> styles = List.of(IrisBankStyle.values());
@@ -521,13 +619,13 @@ public class ChunkWaterRasterTest {
int maxFill = settings.deformation().maxFill();
HydrologyTier stream = tier("stream", 1.5f, 0.5f, 0, IrisBankStyle.SMOOTH);
Reach perchedReach = line(101L, stream, 0L, -32f, 8f, 48f, 8f, (x, z) -> 79);
IntBinaryOperator slope = (x, z) -> 80 - Math.max(0, z - 8);
IntBinaryOperator slope = (x, z) -> 79 - Math.max(0, z - 8);
Scene scene = new Scene(settings, slope).reach(perchedReach);
for (int x = -24; x < 40; x++) {
assertTrue("channel water at x=" + x, scene.water(x, 9));
assertFalse("dry downhill column at x=" + x, scene.water(x, 10));
assertEquals("natural sits below the water at x=" + x, 78, slope.applyAsInt(x, 10));
assertEquals("berm one block above the water at x=" + x, 80, scene.terrain(x, 10));
assertEquals("natural sits below the water at x=" + x, 77, slope.applyAsInt(x, 10));
assertEquals("berm one block above the water at x=" + x, 79, scene.terrain(x, 10));
assertEquals(WaterRole.SHORE, scene.role(x, 10));
assertEquals(perchedReach.id(), scene.feature(x, 10));
assertEquals(stream, scene.tier(x, 10));
@@ -536,11 +634,11 @@ public class ChunkWaterRasterTest {
assertEquals(0, scene.leaks(-24, -8, 40, 24));
Scene steep = new Scene(settings, (x, z) -> z > 9 ? 60 : 80).reach(perchedReach);
assertEquals("past the fill limit the guard berms to the water itself", 79, steep.terrain(0, 10));
assertEquals("past the fill limit the guard berms to the water itself", 78, steep.terrain(0, 10));
assertTrue("the guard rises past the fill limit", steep.terrain(0, 10) > 60 + maxFill);
assertEquals(0, steep.leaks(-24, -8, 40, 24));
Scene below = new Scene(settings, (x, z) -> z > 9 ? 30 : 80).reach(perchedReach);
assertEquals("a column under the sea surface is bermed when the water stands above it", 79, below.terrain(0, 10));
assertEquals("a column under the sea surface is bermed when the water stands above it", 78, below.terrain(0, 10));
}
@Test
@@ -559,7 +657,7 @@ public class ChunkWaterRasterTest {
}
dams++;
assertEquals(dammed.id(), scene.feature(x, z));
assertEquals("dam crest one block above the level at " + x + "," + z, 96, scene.terrain(x, z));
assertEquals("dam crest one block above the level at " + x + "," + z, 95, scene.terrain(x, z));
if (scene.terrain(x, z) - slope.applyAsInt(x, z) > maxFill) {
tall++;
}
@@ -596,7 +694,7 @@ public class ChunkWaterRasterTest {
drawn.catchment());
Scene scene = new Scene(settings(new IrisHydrology()), coast).reach(coastal);
assertEquals("the lip carries the fall", WaterRole.WATERFALL, scene.role(38, 8));
assertEquals(59, scene.head(38, 8));
assertEquals(58, scene.head(38, 8));
int mouths = 0;
for (int x = -20; x <= 56; x++) {
for (int z = 0; z <= 16; z++) {
@@ -0,0 +1,128 @@
package art.arcane.iris.generation.hydrology.raster;
import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
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.Arrays;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class PondChannelSettlementTest {
private static final int SEA_LEVEL = 40;
@Test
public void aLowerChannelDrainsThePondWhereOnlyItsWidthOverlaps() {
for (int offset : new int[]{-512, -19, 0, 513}) {
Pond pond = pond(1L, PondKind.CHAINED, 100, circle(offset, offset, 8));
Reach river = line(2L, offset + 12, offset - 20, offset + 12, offset + 20, 5f, 95, 95);
assertEquals(94, settle(List.of(river), pond));
}
}
@Test
public void cardinalWaterContactDrainsButDiagonalProximityDoesNot() {
Pond pond = pond(1L, PondKind.NATURAL, 100, circle(0, 0, 0));
Reach cardinal = line(2L, 1, 0, 1, 1, 0.25f, 94, 94);
Reach diagonal = line(2L, 1, 1, 1, 2, 0.25f, 94, 94);
assertEquals(94, settle(List.of(cardinal), pond));
assertEquals(100, settle(List.of(diagonal), pond));
}
@Test
public void drySeparationHigherChannelsAndFixedLakesKeepTheirLevels() {
PondMask mask = circle(0, 0, 8);
Pond pond = pond(1L, PondKind.NATURAL, 100, mask);
Pond lake = pond(1L, PondKind.LAKE, 100, mask);
Reach separate = line(2L, 15, -20, 15, 20, 4f, 94, 94);
Reach higher = line(2L, 0, -20, 0, 20, 4f, 104, 104);
Reach lower = line(2L, 0, -20, 0, 20, 4f, 94, 94);
assertEquals(100, settle(List.of(separate), pond));
assertEquals(100, settle(List.of(higher), pond));
assertEquals(100, settle(List.of(lower), lake));
}
@Test
public void equalCanonicalHeadsDoNotTriggerAnotherReflood() {
Pond pond = pond(1L, PondKind.CHAINED, 101, circle(0, 0, 8));
Reach river = line(2L, 0, -20, 0, 20, 4f, 100, 100);
assertEquals(101, settle(List.of(river), pond));
}
@Test
public void nearerChannelsAndIdentityTiesMatchTheRasterWinner() {
Pond pond = pond(1L, PondKind.CHAINED, 100, circle(0, 0, 4));
Reach lower = line(4L, 2, -20, 2, 20, 8f, 94, 94);
Reach nearer = line(3L, 0, -20, 0, 20, 8f, 100, 100);
Pond single = pond(2L, PondKind.NATURAL, 100, circle(0, 0, 0));
Reach tied = line(3L, 2, -20, 2, 20, 8f, 100, 100);
assertEquals(100, settle(List.of(lower, nearer), single));
assertEquals(100, settle(List.of(lower, tied), pond));
assertEquals(100, settle(List.of(tied, lower), pond));
}
@Test
public void projectedStationChoosesOnlyTheTouchingSideOfADrop() {
Reach river = line(2L, -20, 0, 20, 0, 2f, 100, 94);
assertEquals(100, settle(List.of(river), pond(1L, PondKind.NATURAL, 100, circle(-5, 0, 0))));
assertEquals(94, settle(List.of(river), pond(1L, PondKind.NATURAL, 100, circle(5, 0, 0))));
}
@Test
public void adjacentPondsSettleIndependentlyOfReachAndPondOrder() {
Pond first = pond(1L, PondKind.CHAINED, 104, circle(0, 0, 8));
Pond second = pond(2L, PondKind.CHAINED, 102, circle(17, 0, 8));
Reach across = line(3L, -20, 0, 40, 0, 4f, 96, 96);
Reach lower = line(4L, 17, -20, 17, 20, 4f, 94, 94);
assertArrayEquals(new int[]{96, 94}, PondChannelSettlement.levels(List.of(across, lower), List.of(first, second),
new int[]{104, 102}, SEA_LEVEL));
assertArrayEquals(new int[]{94, 96}, PondChannelSettlement.levels(List.of(lower, across), List.of(second, first),
new int[]{102, 104}, SEA_LEVEL));
}
private static int settle(List<Reach> reaches, Pond pond) {
return PondChannelSettlement.levels(reaches, List.of(pond), new int[]{pond.level()}, SEA_LEVEL)[0];
}
private static Pond pond(long id, PondKind kind, int level, PondMask mask) {
return new Pond(id, kind, level, 90, mask.originX(), mask.originZ(), level + 1f, mask, 0L, 0, "water",
mask.originX(), mask.originZ());
}
private static PondMask circle(int x, int z, int radius) {
int size = 2 * radius + 1;
long[] bits = new long[(size * size + 63) >>> 6];
for (int dz = -radius; dz <= radius; dz++) {
for (int dx = -radius; dx <= radius; dx++) {
if (dx * dx + dz * dz <= radius * radius) {
int cell = (dz + radius) * size + dx + radius;
bits[cell >>> 6] |= 1L << cell;
}
}
}
return new PondMask(x - radius, z - radius, 1, size, size, bits);
}
private static Reach line(long id, int x0, int z0, int x1, int z1, float width, int first, int last) {
HydrologyTier tier = new HydrologyTier("river", 0f, width, width, 0f, 0.5f, 2, true, 6, 0, IrisBankStyle.SMOOTH,
false, null, "water", List.of());
float[] widths = new float[]{width, width};
float[] depth = new float[]{2f, 2f};
ReachStyle[] styles = new ReachStyle[2];
Arrays.fill(styles, ReachStyle.RUN);
return new Reach(id, tier, 0L, new float[]{x0, x1}, new float[]{z0, z1}, new int[]{first, last}, widths, depth, styles, 1000f);
}
}
@@ -0,0 +1,135 @@
package art.arcane.iris.generation.hydrology.raster;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.function.IntBinaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class RiffleProfileTest {
private static final int DRY = Integer.MIN_VALUE;
@Test
public void flatPoolsRetainEveryBedDepth() {
IntBinaryOperator heads = (x, z) -> 100;
Surface surface = new Surface(heads);
for (int x = -32; x <= 32; x++) {
for (int z = -16; z <= 16; z++) {
int bed = 64 + Math.floorMod(x + z, 30);
assertEquals(bed, surface.bed(x, z, bed));
}
}
}
@Test
public void twoBlockDropsHaveGradedApproachesAndNoTallSubmergedFace() {
Surface surface = new Surface((x, z) -> x < 0 ? 100 : 98);
for (int z = -24; z <= 24; z++) {
assertEquals(99, surface.bed(-1, z, 92));
assertEquals(97, surface.bed(0, z, 92));
for (int x = -24; x <= 24; x++) {
int bed = surface.bed(x, z, 92);
int next = surface.bed(x + 1, z, 92);
assertTrue(x + "," + z + " bed " + bed, bed < (x < 0 ? 100 : 98));
int headDrop = x == -1 ? 2 : 0;
assertTrue(x + "," + z + " face " + bed + " to " + next, Math.abs(bed - next) <= Math.max(1, headDrop));
}
assertEquals(92, surface.bed(-18, z, 92));
assertEquals(92, surface.bed(17, z, 92));
}
}
@Test
public void deeperBedsFadeWithoutACliffAtTheRadiusBoundary() {
Surface surface = new Surface((x, z) -> x < 0 ? 100 : 98);
for (int z = -8; z <= 8; z++) {
for (int x : new int[]{-18, -17, -16, 14, 15, 16, 17}) {
assertTrue(x + "," + z, Math.abs(surface.bed(x, z, 64) - surface.bed(x + 1, z, 64)) <= 1);
}
assertEquals(64, surface.bed(-18, z, 64));
assertEquals(64, surface.bed(17, z, 64));
assertEquals(99, surface.bed(-1, z, 64));
}
}
@Test
public void lowerPoolsRecoverTheirDepthWithoutAWideFlatShelf() {
Surface surface = new Surface((x, z) -> x < 0 ? 104 : 100);
for (int z = -8; z <= 8; z++) {
assertEquals(103, surface.bed(-1, z, 96));
assertEquals(99, surface.bed(0, z, 96));
assertTrue(surface.bed(3, z, 96) < 99);
assertEquals(96, surface.bed(6, z, 96));
assertTrue(surface.bed(-6, z, 96) > 96);
}
}
@Test
public void nearbyDisconnectedPoolsKeepTheirDepth() {
Surface surface = new Surface((x, z) -> z == 0 ? DRY : z > 0 ? 100 : x < 0 ? 100 : 98);
for (int x = -24; x <= 24; x++) {
assertEquals(92, surface.bed(x, 1, 92));
}
assertEquals(99, surface.bed(-1, -1, 92));
}
@Test
public void aNearerLowerDropNeverCutsOffAHigherRiffle() {
Surface mixed = new Surface((x, z) -> x < 0 ? 104 : z < 0 ? 102 : 100);
Surface high = new Surface((x, z) -> x < 0 ? 104 : 100);
Surface low = new Surface((x, z) -> z < 0 ? 102 : 100);
for (int x = 0; x <= 20; x++) {
for (int z = 0; z <= 20; z++) {
int bed = mixed.bed(x, z, 94);
assertTrue(x + "," + z + " lost the higher influence", bed >= high.bed(x, z, 94));
assertTrue(x + "," + z + " lost the lower influence", bed >= low.bed(x, z, 94));
assertTrue(x + "," + z + " east face", Math.abs(bed - mixed.bed(x + 1, z, 94)) <= 1);
assertTrue(x + "," + z + " south face", Math.abs(bed - mixed.bed(x, z + 1, 94)) <= 1);
}
}
}
@Test
public void diagonalJoinsHaveTheSameProfileAcrossChunkBoundaries() {
IntBinaryOperator heads = (x, z) -> x + z < -7 ? 100 : 98;
for (int chunkX = -2; chunkX <= 1; chunkX++) {
for (int chunkZ = -2; chunkZ <= 1; chunkZ++) {
RiffleProfile first = new RiffleProfile(chunkX, chunkZ, heads);
RiffleProfile east = new RiffleProfile(chunkX + 1, chunkZ, heads);
RiffleProfile south = new RiffleProfile(chunkX, chunkZ + 1, heads);
for (int edge = 0; edge < 16; edge++) {
int x = (chunkX + 1) << 4;
int z = (chunkZ << 4) + edge;
double variation = BedProfile.variation(1337L, x, z);
assertEquals(first.bed(16, edge, 92, heads.applyAsInt(x, z), variation),
east.bed(0, edge, 92, heads.applyAsInt(x, z), variation));
x = (chunkX << 4) + edge;
z = (chunkZ + 1) << 4;
variation = BedProfile.variation(1337L, x, z);
assertEquals(first.bed(edge, 16, 92, heads.applyAsInt(x, z), variation),
south.bed(edge, 0, 92, heads.applyAsInt(x, z), variation));
}
}
}
}
private static final class Surface {
private final IntBinaryOperator heads;
private final Map<Long, RiffleProfile> chunks = new HashMap<>();
private Surface(IntBinaryOperator heads) {
this.heads = heads;
}
private int bed(int x, int z, int original) {
int cx = x >> 4;
int cz = z >> 4;
long key = ((long) cx << 32) | (cz & 0xFFFFFFFFL);
RiffleProfile profile = chunks.computeIfAbsent(key, ignored -> new RiffleProfile(cx, cz, heads));
return profile.bed(x & 15, z & 15, original, heads.applyAsInt(x, z), BedProfile.variation(1337L, x, z));
}
}
}
@@ -35,6 +35,7 @@ import java.util.function.DoubleBinaryOperator;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -177,6 +178,51 @@ public class PondPlannerTest {
assertNull(planner.reflood(sampled.grid(), fine, cups, drained));
}
@Test
public void refloodingBlockMasksKeepsADryFineLatticeMargin() {
HydrologySettings settings = settings(config -> config.getPonds().getShore().setRoundness(0D), MIN_Y, MAX_Y);
int[][] origins = {{0, 0}, {-533, -303}, {529, -301}};
for (int[] origin : origins) {
int x = origin[0];
int z = origin[1];
DoubleBinaryOperator height = (px, pz) -> 100D + 2D * Math.max(Math.abs(px - x - 4), Math.abs(pz - z - 4));
Terrain terrain = new Terrain(height, 1f, ALLOW_ALL, null, MIN_Y, MAX_Y);
HeightLattice fine = new HeightLattice(terrain, 4, false, 4096);
BitSet wet = new BitSet(81);
wet.set(0, 81);
PondMask mask = new PondMask(x, z, 1, 9, 9, wet.toLongArray());
Pond pond = new Pond(1L, PondKind.NATURAL, 109, 100, x + 12, z + 4, 112f, mask, 7L, 0, "water", x + 4, z + 4);
DemGrid raw = new DemGrid((Math.floorDiv(x, 8) - 1) * 8, (Math.floorDiv(z, 8) - 1) * 8, 8, 6, 6);
Pond reflooded = new PondPlanner(settings).reflood(raw, fine, CupSet.EMPTY, pond);
assertNotNull("reflooded pond at " + x + "," + z, reflooded);
assertEquals(pond.level(), reflooded.level());
assertEquals(pond.outletReachId(), reflooded.outletReachId());
assertTrue(reflooded.mask().contains(x + 4, z + 4));
for (int[] node : wetNodes(reflooded.mask())) {
assertTrue("reflood remains within its previous mask", mask.contains(node[0], node[1]));
}
assertContained(reflooded, height);
}
}
@Test
public void refloodingRejectsWetRimsClippedByThePlanningGrid() {
HydrologySettings settings = settings(config -> config.getPonds().getShore().setRoundness(0D), MIN_Y, MAX_Y);
Terrain terrain = new Terrain((x, z) -> x >= 0 && x <= 8 && z >= 0 && z <= 8 ? 100D : 112D,
1f, ALLOW_ALL, null, MIN_Y, MAX_Y);
BitSet wet = new BitSet(81);
wet.set(0, 81);
Pond pond = new Pond(1L, PondKind.NATURAL, 109, 100, 12, 4, 112f, new PondMask(0, 0, 1, 9, 9, wet.toLongArray()),
0L, 0, "water", 4, 4);
Pond reflooded = new PondPlanner(settings).reflood(new DemGrid(0, 0, 8, 2, 2),
new HeightLattice(terrain, 4, false, 4096), CupSet.EMPTY, pond);
assertNull(reflooded);
}
@Test
public void seededCupOnSlopeIsPerchedAndContained() {
HydrologySettings settings = settings(config -> {
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.mantle.MantleHydrologyComponent;
import art.arcane.iris.generation.mantle.MantleWriter;
@@ -84,23 +85,18 @@ public class EffectiveWaterTest {
}
@Test
public void cornerColumnsBelowTwoOneBlockHigherNeighboursBecomeRimsAtTheUpperHead() {
long corner = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(80), wet(79), NONE);
assertEquals(DRY, EffectiveWater.head(corner));
assertEquals(80, EffectiveWater.ground(corner));
public void lowerCornerColumnsRemainWetBetweenHigherNeighbours() {
long corner = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(81), wet(81), wet(79), NONE);
assertEquals(EffectiveWater.column(77, 79), corner);
long mixed = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(80), wet(86), NONE);
assertEquals(DRY, EffectiveWater.head(mixed));
assertEquals(86, EffectiveWater.ground(mixed));
long edge = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(80), wet(79), NONE, NONE);
assertEquals(EffectiveWater.column(77, 79), edge);
long mixed = EffectiveWater.settle(EffectiveWater.column(77, 79), wet(81), wet(81), wet(87), NONE);
assertEquals(EffectiveWater.column(77, 79), mixed);
}
@Test
public void columnsAboveADropOfTwoOrMoreHoldWaterOnlyInTheirTopBlock() {
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(79), wet(99), NONE, NONE));
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), NONE, wet(97), NONE, NONE));
public void wetColumnsKeepTheirCanonicalBedAcrossDrops() {
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(79), wet(99), NONE, NONE));
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), NONE, wet(97), NONE, NONE));
assertEquals(EffectiveWater.column(94, 99), EffectiveWater.settle(EffectiveWater.column(94, 99), wet(98), NONE, NONE, NONE));
assertEquals(EffectiveWater.column(98, 99), EffectiveWater.settle(EffectiveWater.column(98, 99), wet(70), NONE, NONE, NONE));
}
@@ -197,33 +193,27 @@ public class EffectiveWaterTest {
}
@Test(timeout = 180_000L)
public void staircaseStepsTurnExactlyTheirCornerColumnsIntoRims() throws Exception {
public void diagonalStaircaseStepsPreserveEveryWetColumnWithoutSourceConversion() throws Exception {
Scene scene = staircase();
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
Drops drops = new Drops(run.complex(), 0, 7, 0, 7);
int rims = 0;
int wet = 0;
for (int x = 24; x <= 104; x++) {
for (int z = 24; z <= 104; z++) {
int head = rasterHead(run.hydrology(), x, z);
if (head == DRY) {
continue;
}
int above = 0;
int highest = DRY;
wet++;
assertEquals("water head at " + x + "," + z, head, drops.head(x, z));
assertTrue("submerged bed at " + x + "," + z, drops.ground(x, z) < head);
for (int[] side : SIDES) {
int neighbour = rasterHead(run.hydrology(), x + side[0], z + side[1]);
above += neighbour == head + 1 ? 1 : 0;
highest = Math.max(highest, neighbour);
}
boolean rim = above >= 2;
assertEquals("rim at " + x + "," + z, rim, drops.head(x, z) == DRY);
if (rim) {
rims++;
assertEquals("rim ground at " + x + "," + z, highest, drops.ground(x, z));
assertFalse("one-block drop at " + x + "," + z, neighbour != DRY && Math.abs(neighbour - head) == 1);
}
}
}
assertTrue("rims " + rims, rims > 10);
assertTrue("wet columns " + wet, wet > 200);
drops.assertSealed(24, 104, 24, 104);
}
}
@@ -234,7 +224,6 @@ public class EffectiveWaterTest {
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
Drops drops = new Drops(run.complex(), 0, 7, 0, 5);
int lips = 0;
int raised = 0;
int falls = 0;
for (int x = 16; x <= 112; x++) {
for (int z = 30; z <= 50; z++) {
@@ -250,17 +239,22 @@ public class EffectiveWaterTest {
if (lip) {
lips++;
assertEquals("lip ground at " + x + "," + z, head - 1, drops.ground(x, z));
raised += rasterGround(run.hydrology(), x, z) < head - 1 ? 1 : 0;
assertEquals("canonical lip at " + x + "," + z, rasterGround(run.hydrology(), x, z), drops.ground(x, z));
}
if (drops.falling(x, head + 1, z)) {
falls++;
assertTrue(drops.falling(x, head + 3, z));
assertFalse(drops.falling(x, head + 4, z));
int highest = head;
for (int[] side : SIDES) {
highest = Math.max(highest, drops.head(x + side[0], z + side[1]));
}
for (int y = head + 1; y <= highest; y++) {
assertTrue("fall column at " + x + "," + y + "," + z, drops.falling(x, y, z));
}
assertFalse(drops.falling(x, highest + 1, z));
}
}
}
assertTrue("lips " + lips, lips > 10);
assertTrue("lips raised from deeper beds " + raised, raised > 0);
assertTrue("falls " + falls, falls > 10);
drops.assertSealed(16, 112, 30, 50);
}
@@ -268,11 +262,24 @@ public class EffectiveWaterTest {
@Test(timeout = 180_000L)
public void noFallAnywhereMeetsTheSourceConversionCondition() throws Exception {
for (Scene scene : List.of(staircase(), cascade(), outlet())) {
for (Scene scene : List.of(staircase(), cascade(), outlet(), mouth())) {
try (HydrologyFixture fixture = fixture(); SceneRun run = scene.install(fixture)) {
Drops drops = new Drops(run.complex(), 0, 7, 0, 7);
int wet = 0;
for (int x = 8; x <= 120; x++) {
for (int z = 8; z <= 120; z++) {
int head = rasterHead(run.hydrology(), x, z);
if (head != DRY) {
wet++;
assertEquals(scene.name + " water stays connected at " + x + "," + z, head, drops.head(x, z));
assertTrue(scene.name + " bed stays submerged at " + x + "," + z, drops.ground(x, z) < head);
}
}
}
assertTrue(scene.name + " wet columns " + wet, wet > 100);
assertEquals(scene.name + " conversions", 0, drops.conversions(8, 120, 8, 120));
assertEquals(scene.name + " unsupported falls", 0, drops.unsupported(8, 120, 8, 120));
drops.assertSealed(8, 120, 8, 120);
}
}
}
@@ -324,6 +331,21 @@ public class EffectiveWaterTest {
List.of(WaterScene.pond(74L, 191, 48, 48, 10)));
}
private static Scene mouth() {
IntBinaryOperator natural = (x, z) -> x < 64 ? SEA + 12 : SEA - 8;
Reach reach = WaterScene.line(75L, WaterScene.stream(), 8, 40, 120, 40,
(x, z) -> x < 64 ? SEA + 11 : SEA);
ReachStyle[] styles = reach.style().clone();
for (int point = 0; point < styles.length; point++) {
if (reach.xs()[point] >= 64) {
styles[point] = ReachStyle.MOUTH;
}
}
Reach coastal = new Reach(reach.id(), reach.tier(), reach.downstreamId(), reach.xs(), reach.zs(), reach.grade(),
reach.halfWidth(), reach.depth(), styles, reach.catchment());
return new Scene("mouth", natural, List.of(coastal), List.of());
}
private HydrologyFixture fixture() throws Exception {
return new HydrologyFixture(HydrologyFixture.pack(temporaryFolder.newFolder().toPath(), null, null, true));
}
@@ -410,9 +432,11 @@ public class EffectiveWaterTest {
}
private int[] column(int x, int z) {
return columns.computeIfAbsent(((long) x << 32) | (z & 0xFFFFFFFFL), key -> new int[]{
(int) Math.round(complex.resolveHydrologyTerrainHeight(x, z)),
complex.resolveWaterRole(x, z).wet() ? (int) Math.round(complex.resolveWaterHead(x, z)) : DRY});
return columns.computeIfAbsent(((long) x << 32) | (z & 0xFFFFFFFFL), key -> {
int ground = (int) Math.round(complex.resolveHydrologyTerrainHeight(x, z));
int head = (int) Math.round(complex.resolveWaterHead(x, z));
return new int[]{ground, head > ground ? head : DRY};
});
}
private boolean still(int x, int y, int z) {
@@ -206,7 +206,7 @@ public class IrisComplexHeightStreamTest {
assertTrue(fixture.complex.isHydrologyPlanned(8, 8));
bulk.fillChunkDoubles(0, 0, values);
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
assertBits(HydrologyFixture.POND_LEVEL, fixture.complex.getHeightFluidStream().getDouble(8, 8));
assertBits(HydrologyFixture.POND_LEVEL - 1, fixture.complex.getHeightFluidStream().getDouble(8, 8));
fixture.mainThread.set(false);
bulk.fillChunkDoubles(0, 0, values);
assertBits(landed.terrainHeight(8, 8), values[8 << 4 | 8]);
@@ -247,14 +247,14 @@ public class IrisComplexHeightStreamTest {
public void waterStreamsFollowTheRaster() throws Exception {
try (HydrologyFixture fixture = new HydrologyFixture(hydrologyPack())) {
ChunkWaterRaster raster = fixture.hydrology.runtime.raster(0, 0);
assertBits(HydrologyFixture.POND_LEVEL, fixture.complex.getWaterHeadStream().getDouble(8, 8));
assertBits(HydrologyFixture.POND_LEVEL - 1, fixture.complex.getWaterHeadStream().getDouble(8, 8));
assertSame(WaterRole.POND, fixture.complex.getWaterRoleStream().get(8, 8));
assertSame(raster.role(15, 15), fixture.complex.getWaterRoleStream().get(15.5, 15.5));
assertBits(HydrologyFixture.SEA_LEVEL, fixture.complex.getWaterHeadStream().getDouble(640, 640));
assertSame(WaterRole.NONE, fixture.complex.getWaterRoleStream().get(640, 640));
EngineMantle mantle = mock(EngineMantle.class, CALLS_REAL_METHODS);
doReturn(fixture.complex).when(mantle).getComplex();
assertEquals(HydrologyFixture.POND_LEVEL, mantle.getFluidHeight(8, 8));
assertEquals(HydrologyFixture.POND_LEVEL - 1, mantle.getFluidHeight(8, 8));
assertEquals(HydrologyFixture.SEA_LEVEL, mantle.getFluidHeight(640, 640));
}
}
@@ -266,7 +266,7 @@ public class IrisComplexHeightStreamTest {
doReturn(fixture.engine).when(placer).getEngine();
doReturn(fixture.engine.getDimension().getFluidHeight()).when(placer).getFluidHeight();
assertEquals(HydrologyFixture.POND_LEVEL, placer.getFluidHeight(8, 8));
assertEquals(HydrologyFixture.POND_LEVEL - 1, placer.getFluidHeight(8, 8));
assertEquals(HydrologyFixture.SEA_LEVEL, placer.getFluidHeight(640, 640));
}
}
@@ -165,7 +165,7 @@ public class IrisComplexTransitionTest {
if (terrain > 150) {
berms++;
assertEquals(terrain, ground, 0D);
assertTrue(ground >= 156);
assertTrue(ground >= 155);
}
assertContained(complex, x, z, ground);
}
@@ -195,13 +195,13 @@ public class IrisComplexTransitionTest {
double ground = complex.resolveHydrologyTerrainHeight(x, z);
assertTrue(raster.terrainHeight(x, z) < 150);
assertContained(complex, x, z, ground);
if (ground >= 148) {
if (ground >= 147) {
dried++;
assertEquals(127D, complex.resolveWaterHead(x, z), 0D);
assertSame(WaterRole.NONE, complex.resolveWaterRole(x, z));
} else {
wet++;
assertEquals(148D, complex.resolveWaterHead(x, z), 0D);
assertEquals(147D, complex.resolveWaterHead(x, z), 0D);
assertSame(WaterRole.POND, complex.resolveWaterRole(x, z));
}
}
@@ -220,7 +220,7 @@ public class IrisComplexTransitionTest {
FakeHydrology hydrology = seam.hydrology;
IrisComplex complex = seam.natural((x, z) -> 150D).complex();
for (int z = 7; z <= 9; z++) {
assertEquals(148, hydrology.runtime.raster(1, 0).waterHead(0, z));
assertEquals(147, hydrology.runtime.raster(1, 0).waterHead(0, z));
assertSame(WaterRole.NONE, complex.resolveWaterRole(16, z));
assertEquals(150D, complex.resolveHydrologyTerrainHeight(16, z), 0D);
}
@@ -308,9 +308,9 @@ public class IrisComplexTransitionTest {
}
if (stage.raster(1).waterHead(lx, lz) != Integer.MIN_VALUE && oldChunkWeight(x, lz) > 0D && oldChunkWeight(x - 1, lz) == 0D) {
dams++;
assertTrue(x + "," + lz + " dam lost its crest", blended.getRaw(lx, 157, lz) == stage.stone);
assertTrue(x + "," + lz + " dam lost its crest", blended.getRaw(lx, 156, lz) == stage.stone);
}
if (blended.getRaw(lx, 156, lz) != stage.water) {
if (blended.getRaw(lx, 155, lz) != stage.water) {
continue;
}
wet++;
@@ -318,7 +318,7 @@ public class IrisComplexTransitionTest {
int nx = lx + side[0];
int nz = lz + side[1];
if (nx >= 0 && nx < 16 && nz >= 0 && nz < 16) {
assertFalse(x + "," + lz + " leaks toward " + (16 + nx) + "," + nz, stage.open(blended.getRaw(nx, 156, nz)));
assertFalse(x + "," + lz + " leaks toward " + (16 + nx) + "," + nz, stage.open(blended.getRaw(nx, 155, nz)));
}
}
}
@@ -339,13 +339,13 @@ public class IrisComplexTransitionTest {
int dams = 0;
int water = 0;
for (int lz = 0; lz < 16; lz++) {
assertTrue(stage.complex.naturalTrueHeight(16, lz) >= 156);
assertTrue(stage.complex.naturalTrueHeight(16, lz) >= 155);
if (raster.waterHead(1, lz) != Integer.MIN_VALUE) {
dams++;
assertSame(WaterRole.NONE, stage.complex.resolveWaterRole(17, lz));
assertTrue(17 + "," + lz + " dam at " + stage.complex.resolveHydrologyTerrainHeight(17, lz),
stage.complex.resolveHydrologyTerrainHeight(17, lz) >= 157D);
assertSame(stage.stone, blended.getRaw(1, 157, lz));
stage.complex.resolveHydrologyTerrainHeight(17, lz) >= 156D);
assertSame(stage.stone, blended.getRaw(1, 156, lz));
}
for (int lx = 0; lx < 16; lx++) {
for (int y = 0; y < TransitionStage.HEIGHT; y++) {
@@ -373,7 +373,7 @@ public class IrisComplexTransitionTest {
public void seamDamHoldsNewWaterBesideAnOpenOldRiverOrLakeEdge() throws Exception {
try (TransitionStage stage = new TransitionStage(IrisComplexTransitionTest::oldChunkWeight, true, (x, z) -> 150.3D,
TransitionStage.savedColumn(140, 160, -1))) {
assertTrue(stage.complex.resolveWaterHead(16, 4) < 156D);
assertTrue(stage.complex.resolveWaterHead(16, 4) < 155D);
Hunk<NativeBlockState> blended = stage.blend(1, stage.paint(1));
assertTrue(stage.dams(blended) > 0);
stage.assertNoWaterTouchesAir(blended);
@@ -421,9 +421,9 @@ public class IrisComplexTransitionTest {
if (stage.raster(1).waterHead(1, lz) != Integer.MIN_VALUE) {
wet++;
assertSame(WaterRole.POND, stage.complex.resolveWaterRole(17, lz));
assertEquals(156D, stage.complex.resolveWaterHead(17, lz), 0D);
assertSame(stage.water, blended.getRaw(1, 156, lz));
assertSame(stage.stone, blended.getRaw(0, 156, lz));
assertEquals(155D, stage.complex.resolveWaterHead(17, lz), 0D);
assertSame(stage.water, blended.getRaw(1, 155, lz));
assertSame(stage.stone, blended.getRaw(0, 155, lz));
}
}
assertTrue(wet > 0);
@@ -455,7 +455,7 @@ public class IrisComplexTransitionTest {
boolean beside = oldChunkWeight(x + 1, z) == 0D || oldChunkWeight(x - 1, z) == 0D
|| oldChunkWeight(x, z + 1) == 0D || oldChunkWeight(x, z - 1) == 0D;
if (raster.waterHead(index & 15, index >> 4) != Integer.MIN_VALUE && oldChunkWeight(x, z) > 0D && beside
&& stage.complex.resolveHydrologyTerrainHeight(x, z) < 156D + 1D) {
&& stage.complex.resolveHydrologyTerrainHeight(x, z) < 155D + 1D) {
fail(x + "," + z + " stays wet beside a saved column that opens below its head");
}
if (role == WaterRole.NONE && raster.waterHead(index & 15, index >> 4) != Integer.MIN_VALUE && beside
@@ -663,8 +663,8 @@ public class IrisComplexTransitionTest {
for (int lz = 0; lz < 16; lz++) {
if (raster(1).waterHead(1, lz) != Integer.MIN_VALUE && complex.resolveWaterRole(17, lz) == WaterRole.NONE) {
dams++;
assertTrue(17 + "," + lz + " dam below the crest", complex.resolveHydrologyTerrainHeight(17, lz) >= 157D);
assertSame(stone, blended.getRaw(1, 157, lz));
assertTrue(17 + "," + lz + " dam below the crest", complex.resolveHydrologyTerrainHeight(17, lz) >= 156D);
assertSame(stone, blended.getRaw(1, 156, lz));
}
}
return dams;
@@ -821,7 +821,7 @@ public class IrisComplexTransitionTest {
dams++;
assertEquals(127D, head, 0D);
assertSame(WaterRole.NONE, role);
assertTrue(x + "," + z + " dam at " + ground, ground >= 157D);
assertTrue(x + "," + z + " dam at " + ground, ground >= 156D);
}
}
}
@@ -24,6 +24,7 @@ import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Before;
import java.util.ArrayList;
import java.util.List;
@@ -51,6 +52,14 @@ public class IrisTerrainNormalActuatorHydrologyTest {
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
private final NativeBlockState submergedDirt = state("minecraft:dirt");
private final NativeBlockState submergedNetherrack = state("minecraft:netherrack");
@Before
public void bindSubmergedSoils() {
when(platform.registries().block("minecraft:dirt")).thenReturn(submergedDirt);
when(platform.registries().block("minecraft:netherrack")).thenReturn(submergedNetherrack);
}
@Test
public void hydrologyOwnedFluidBypassesBiomeSeaLayers() {
@@ -100,7 +109,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
Hunk<NativeBlockState> output = painter.paint();
assertSame(role.name(), painter.grass, output.get(0, GROUND, 0));
assertSame(role.name(), submergedDirt, output.get(0, GROUND, 0));
for (int y = GROUND + 1; y <= HEAD; y++) {
assertSame(role + " y " + y, painter.channelFluid, output.get(0, y, 0));
}
@@ -117,7 +126,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
Hunk<NativeBlockState> sea = ocean.paint();
assertSame("the sea floor keeps the biome's own top", ocean.grass, sea.get(0, GROUND, 0));
assertSame("the sea floor keeps a submerged soil top", submergedDirt, sea.get(0, GROUND, 0));
assertSame(ocean.dirt, sea.get(0, GROUND - 1, 0));
assertSame("the slot takes the sea layers", ocean.ice, sea.get(0, HEAD, 0));
for (int y = GROUND + 1; y < HEAD; y++) {
@@ -178,7 +187,7 @@ public class IrisTerrainNormalActuatorHydrologyTest {
NativeBlockState expectedTop = switch (role) {
case NONE -> painter.grass;
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL -> painter.bed;
case INLET -> painter.grass;
case INLET -> submergedDirt;
case SHORE, POND_SHORE, DAM -> painter.shore;
case BANK -> painter.bank;
};
@@ -192,11 +201,68 @@ public class IrisTerrainNormalActuatorHydrologyTest {
}
materials.setBank(null).setShore(null).setBed(null);
output = painter.paint();
assertSame(role.name(), painter.grass, output.get(0, GROUND, 0));
assertSame(role.name(), role.wet() ? submergedDirt : painter.grass, output.get(0, GROUND, 0));
assertSame(role.name(), painter.dirt, output.get(0, GROUND - 1, 0));
}
}
@Test
public void submergedSoilLayersAndExplicitPalettesUseStableSubstrates() {
for (String key : List.of("grass_block[snowy=false]", "mycelium", "dirt_path", "farmland[moisture=7]",
"crimson_nylium", "warped_nylium")) {
NativeBlockState livingSoil = state("minecraft:" + key);
NativeBlockState expected = key.endsWith("nylium") ? submergedNetherrack : submergedDirt;
for (WaterRole role : WET) {
Painter painter = new Painter(role, HEAD);
painter.layers(livingSoil, livingSoil);
painter.materials(true).setBed(painter.palette(livingSoil));
Hunk<NativeBlockState> output = painter.paint();
assertSame(role + " " + key, expected, output.get(0, GROUND, 0));
assertSame(role + " submerged side " + key, expected, output.get(0, GROUND - 1, 0));
assertSame(painter.channelFluid, output.get(0, GROUND + 1, 0));
}
for (WaterRole role : List.of(WaterRole.NONE, WaterRole.BANK, WaterRole.SHORE, WaterRole.POND_SHORE)) {
Painter painter = new Painter(role, GROUND);
painter.layers(livingSoil, livingSoil);
assertSame(role + " " + key, livingSoil, painter.paint().get(0, GROUND, 0));
}
}
}
@Test
public void submergedImageMapSoilUsesStableSubstrate() {
Painter painter = new Painter(WaterRole.CHANNEL, HEAD);
when(painter.complex.getImageMapRuntime().sampleSurfaceBlock(11, -4)).thenReturn(painter.grass);
assertSame(submergedDirt, painter.paint().get(0, GROUND, 0));
}
@Test
public void submergedPodzolAndMossKeepTheirAuthoredMaterials() {
for (String key : List.of("podzol[snowy=false]", "moss_block", "mud", "clay")) {
Painter painter = new Painter(WaterRole.POND, HEAD);
NativeBlockState stable = state("minecraft:" + key);
painter.layers(stable, stable);
Hunk<NativeBlockState> output = painter.paint();
assertSame(key, stable, output.get(0, GROUND, 0));
assertSame(key, stable, output.get(0, GROUND - 1, 0));
}
}
@Test
public void dryLowerTerrainSpansKeepTheirLivingSoilsBelowAWetBed() {
Painter painter = new Painter(WaterRole.POND, HEAD);
painter.carved();
Hunk<NativeBlockState> output = painter.paint();
assertSame(submergedDirt, output.get(0, GROUND, 0));
assertSame(painter.grass, output.get(0, 40, 0));
assertSame(painter.dirt, output.get(0, 39, 0));
}
@Test
public void sandSurvivesUnlessTheMaterialsKeepFallingBlocksAwayFromTheWater() {
for (WaterRole role : List.of(WaterRole.CHANNEL, WaterRole.POND, WaterRole.SHORE, WaterRole.BANK)) {
@@ -484,7 +484,7 @@ public final class GenerationSemanticCaptureTest {
ChunkGenerationSemantics pond = GenerationSemanticCapture.capture(fixture.engine, 0, 0, 5L);
for (ChunkGenerationSemantics.RiverFeatureOccurrence occurrence : pond.riverFeatures()) {
assertEquals("water", occurrence.profileKey());
assertEquals(63 - 64, occurrence.position().y());
assertEquals(62 - 64, occurrence.position().y());
assertEquals(WaterRole.POND, hydrology.runtime.raster(0, 0).role(occurrence.position().x() & 15,
occurrence.position().z() & 15));
}