Hydrology fixes

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-10 23:07:46 -04:00
1 parent 577f924133
commit bf12f30d26
168 files changed
+13544 -582

No files matched your search

@@ -0,0 +1,40 @@
package art.arcane.iris.core.pack;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackRiverBank3DValidatorTest {
@Test
public void acceptsVolumetricStylesAndFullLengthSeeds() {
List<String> errors = validate("""
{"enabled":true,"seed":-9223372036854775808,"amplitude":32,
"horizontalScale":4,"verticalScale":256,"maximumOverhang":16,
"densityStyle":{"style":"STRATA","fracture":{"style":"SIMPLEX","zoom":2}}}
""");
assertTrue(errors.toString(), errors.isEmpty());
assertTrue(validate("{\"densityStyle\":\"river/bank-rock\"}").isEmpty());
}
@Test
public void rejectsInvalidBoundsTypesAndStyles() {
for (String fields : List.of("\"seed\":1.5", "\"seed\":9223372036854775808",
"\"enabled\":\"true\"", "\"amplitude\":-1", "\"horizontalScale\":0",
"\"verticalScale\":257", "\"maximumOverhang\":17", "\"maximumOverhang\":1.5",
"\"densityStyle\":null", "\"densityStyle\":{\"style\":\"invalid\"}",
"\"densityStyle\":{\"zoom\":0}")) {
assertFalse(fields, validate("{" + fields + "}").isEmpty());
}
}
private static List<String> validate(String json) {
List<String> errors = new ArrayList<>();
PackRiverBank3DValidator.validate("geometry.banks3D", new JSONObject(json), errors);
return errors;
}
}
@@ -66,6 +66,11 @@ public class IrisRiverSchemaTest {
JSONObject rivers = referencedProperties(definitions, properties.getJSONObject("rivers"));
JSONObject routing = referencedProperties(definitions, rivers.getJSONObject("routing"));
JSONObject geometry = referencedProperties(definitions, rivers.getJSONObject("geometry"));
JSONObject bankVolume = referencedProperties(definitions, geometry.getJSONObject("banks3D"));
assertEquals(32D, bankVolume.getJSONObject("amplitude").getDouble("maximum"), 0D);
assertEquals(4D, bankVolume.getJSONObject("verticalScale").getDouble("minimum"), 0D);
assertEquals(16, bankVolume.getJSONObject("maximumOverhang").getInt("maximum"));
assertSnippetBackedObjectReference(bankVolume.getJSONObject("densityStyle"));
JSONObject drops = referencedProperties(definitions, geometry.getJSONObject("drops"));
JSONObject surface = referencedProperties(definitions, rivers.getJSONObject("surface"));
JSONObject sources = referencedProperties(definitions, surface.getJSONObject("sources"));
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "43b3add0a559f7006e2973344f39db933bbfb1be889f3c5d616a7bf035a9db39";
private static final String EXPECTED_SCHEMA_DIGEST = "6dd947428034d7feb5549a37f9b7061c5552601779611587967451e4e54fb984";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -1,15 +1,29 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.IrisDecorationStep;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRiverMaterialConfig;
import art.arcane.iris.engine.terrain.Terrain3DColumn;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.ChunkedDataCache;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Rule;
@@ -25,6 +39,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
@@ -208,6 +223,79 @@ public class IrisTerrainNormalActuatorHydrologyTest {
verify(fixture.palette, never()).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any());
}
@Test
public void deepBankAndShoreCutsStillReceiveTheirSurfaceMaterial() {
for (HydrologyColumnLayer layer : List.of(shoreLayer(), bankLayer())) {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
IrisData data = mock(IrisData.class);
IrisBiome biome = mock(IrisBiome.class);
IrisRegion region = new IrisRegion();
ChunkContext context = mock(ChunkContext.class);
PlatformBlockState rock = mock(PlatformBlockState.class);
PlatformBlockState grass = mock(PlatformBlockState.class);
PlatformBlockState painted = mock(PlatformBlockState.class);
when(rock.key()).thenReturn("minecraft:stone");
when(grass.key()).thenReturn("minecraft:grass_block");
when(painted.key()).thenReturn("minecraft:dirt");
IrisMaterialPalette palette = mock(IrisMaterialPalette.class);
when(palette.get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), eq(data))).thenReturn(painted);
IrisRiverMaterialConfig material = new IrisRiverMaterialConfig().setEnabled(true).setDepth(2).setPalette(palette);
IrisHydrology hydrology = new IrisHydrology();
hydrology.getRivers().getSurface().getBanks().setShoreMaterial(material).setBankMaterial(material);
IrisDimension dimension = new IrisDimension().setHydrology(hydrology).setBedrock(false).setHideOresForHiddenOre(true);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex);
when(engine.getData()).thenReturn(data);
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
when(complex.getImageMapRuntime()).thenReturn(mock(IrisImageMapRuntime.class));
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 0D));
when(complex.sampleHydrologyColumn(11, -4)).thenReturn(new HydrologyColumnSample(
11, -4, 80, 0, false, "parent", List.of(layer)));
when(context.getRoundedHeight(0, 0)).thenReturn(60);
when(context.getBiome()).thenReturn(new ChunkedDataCache<>(
ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome)), 11, -4, false));
when(context.getRegion()).thenReturn(new ChunkedDataCache<>(
ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> region)), 11, -4, false));
when(context.getRock()).thenReturn(new ChunkedDataCache<>(
ProceduralStream.of((x, z) -> rock, Interpolated.of(value -> 0D, value -> rock)), 11, -4, false));
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>(List.of(grass)));
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 64, 1);
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
assertSame(painted, output.get(0, 60, 0));
assertSame(painted, output.get(0, 59, 0));
assertSame(rock, output.get(0, 58, 0));
material.setEnabled(false);
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
assertSame(rock, output.get(0, 60, 0));
Terrain3DColumn carved = mock(Terrain3DColumn.class);
when(carved.spanCount()).thenReturn(2);
when(carved.floor(1)).thenReturn(60);
when(carved.ceiling(1)).thenReturn(50);
when(carved.floor(0)).thenReturn(40);
when(carved.ceiling(0)).thenReturn(0);
Terrain3DColumn natural = mock(Terrain3DColumn.class);
when(natural.surfaceY(60)).thenReturn(60);
when(natural.surfaceY(40)).thenReturn(44);
when(complex.terrainColumn(eq(11), eq(-4), any(HydrologyColumnSample.class))).thenReturn(carved);
when(complex.naturalTerrainColumn(11, -4)).thenReturn(natural);
when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>());
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
assertSame(grass, output.get(0, 60, 0));
assertSame(rock, output.get(0, 40, 0));
material.setEnabled(true);
new IrisTerrainNormalActuator(engine).terrainSliver(11, -4, 0, output, context);
assertSame(painted, output.get(0, 60, 0));
assertSame(painted, output.get(0, 40, 0));
assertSame(rock, output.get(0, 38, 0));
}
}
private static HydrologyColumnLayer channelLayer() {
return surfaceLayer(true, false);
}
@@ -302,7 +302,7 @@ public class DecoratorCoreTest {
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(1, 2, 1);
int placed = DecoratorCore.placeFloatingStacked(
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data);
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data, null);
assertEquals(1, placed);
assertSame(decorant, output.get(0, 1, 0));
@@ -344,7 +344,7 @@ public class DecoratorCoreTest {
output.set(0, 2, 0, occupied);
DecoratorCore.placeFloatingSimple(
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data);
decorator, 0, 0, 0, 0, 0, 3, output, new RNG(1L), data, null);
assertSame(air, output.get(0, 1, 0));
assertSame(occupied, output.get(0, 2, 0));
@@ -154,6 +154,40 @@ public class IrisShoreLineDecoratorTest {
assertSame(targetAir, output.get(0, FLUID_HEIGHT + 1, 0));
}
@Test
public void acceptedShoreCaneRequiresWaterBesideTheActualSubstrate() {
Fixture fixture = createFixture(false, false);
PlatformBlockState support = sturdyState();
PlatformBlockState air = airState();
PlatformBlockState water = mock(PlatformBlockState.class);
when(water.isWater()).thenReturn(true);
when(fixture.decorant.key()).thenReturn("minecraft:sugar_cane[age=0]");
when(fixture.decorant.canPlaceOnto(support)).thenReturn(true);
Hunk<PlatformBlockState> output = Hunk.newArrayHunk(3, FLUID_HEIGHT + 3, 3);
output.set(1, FLUID_HEIGHT, 1, support);
output.set(1, FLUID_HEIGHT + 1, 1, air);
fixture.shoreline.decorateAcceptedShore(1, 1, 1, 1, output, fixture.biome, FLUID_HEIGHT, output.getHeight());
assertSame(air, output.get(1, FLUID_HEIGHT + 1, 1));
output.set(0, FLUID_HEIGHT, 1, water);
fixture.shoreline.decorateAcceptedShore(1, 1, 1, 1, output, fixture.biome, FLUID_HEIGHT, output.getHeight());
assertSame(fixture.decorant, output.get(1, FLUID_HEIGHT + 1, 1));
}
@Test
public void forcedShoreCaneCannotUseStone() {
Fixture fixture = createFixture(true, false);
PlatformBlockState stone = sturdyState();
PlatformBlockState air = airState();
when(fixture.decorant.key()).thenReturn("minecraft:sugar_cane[age=0]");
when(fixture.decorant.canPlaceOnto(stone)).thenReturn(false);
Hunk<PlatformBlockState> output = output(stone, air);
fixture.shoreline.decorateAcceptedShore(0, 0, 0, 0, output, fixture.biome, FLUID_HEIGHT, output.getHeight());
assertSame(air, output.get(0, FLUID_HEIGHT + 1, 0));
}
@Test
public void unsupportedWaterloggedStackRestoresEveryOriginalBlock() {
Fixture fixture = createFixture(false, false);
@@ -0,0 +1,138 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisSugarCanePlacementTest {
@Test
public void floatingSingleRequiresSuitableSoilAndAdjacentWater() {
verifySupport(Placement.FLOATING_SINGLE);
}
@Test
public void floatingStackRequiresSuitableSoilAndAdjacentWater() {
verifySupport(Placement.FLOATING_STACK);
}
@Test
public void ceilingSingleRequiresSuitableSoilAndAdjacentWater() {
verifySupport(Placement.CEILING_SINGLE);
}
@Test
public void ceilingStackRequiresSuitableSoilAndAdjacentWater() {
verifySupport(Placement.CEILING_STACK);
}
@Test
public void floatingCaneUsesAcceptedWaterAcrossTheChunkBoundary() {
for (Placement placement : List.of(Placement.FLOATING_SINGLE, Placement.FLOATING_STACK)) {
Fixture fixture = new Fixture(1);
fixture.output.set(0, 3, 0, fixture.soil);
IrisComplex complex = mock(IrisComplex.class);
when(fixture.engine.getComplex()).thenReturn(complex);
HydrologyFeatureRef feature = new HydrologyFeatureRef(
11L, HydrologyFeatureType.RIFFLE, 12L, 13L, -17, 3, -16, 1, 0, false);
HydrologyColumnLayer layer = new HydrologyColumnLayer(feature, 1, 3, 3,
true, false, false, true, false, false, true, true, false,
"water", "river", "mouth", "shore", "bank", "cave");
when(complex.sampleHydrologyColumn(-17, -16)).thenReturn(new HydrologyColumnSample(
-17, -16, 6, 0, false, "parent", List.of(layer)));
when(complex.resolveHydrologyFluid("water", -17, -16)).thenReturn(fixture.water);
fixture.place(placement);
assertSame(fixture.cane, fixture.output.get(0, 4, 0));
}
}
private static void verifySupport(Placement placement) {
Fixture fixture = new Fixture(3);
fixture.output.set(1, 3, 1, fixture.stone);
fixture.output.set(0, 3, 1, fixture.water);
fixture.place(placement);
assertSame(fixture.air, fixture.output.get(1, 4, 1));
fixture.output.set(1, 3, 1, fixture.soil);
fixture.output.set(0, 3, 1, fixture.air);
fixture.place(placement);
assertSame(fixture.air, fixture.output.get(1, 4, 1));
fixture.output.set(0, 3, 1, fixture.water);
fixture.place(placement);
assertSame(fixture.cane, fixture.output.get(1, 4, 1));
}
private static PlatformBlockState state(String key) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(key);
return state;
}
private enum Placement {
FLOATING_SINGLE,
FLOATING_STACK,
CEILING_SINGLE,
CEILING_STACK
}
private static final class Fixture {
private final PlatformBlockState air = state("minecraft:air");
private final PlatformBlockState stone = state("minecraft:stone");
private final PlatformBlockState soil = state("minecraft:dirt");
private final PlatformBlockState water = state("minecraft:water");
private final PlatformBlockState cane = state("minecraft:sugar_cane[age=0]");
private final IrisDecorator decorator = mock(IrisDecorator.class);
private final Engine engine = mock(Engine.class);
private final EngineMantle mantle = mock(EngineMantle.class);
private final Hunk<PlatformBlockState> output;
private final int local;
private Fixture(int size) {
local = size / 2;
output = Hunk.newArrayHunk(size, 8, size);
when(air.isAir()).thenReturn(true);
when(water.isWater()).thenReturn(true);
when(cane.canPlaceOnto(soil)).thenReturn(true);
when(cane.canPlaceOnto(cane)).thenReturn(true);
when(mantle.getEngine()).thenReturn(engine);
when(decorator.isForcePlace()).thenReturn(true);
when(decorator.getHeight(any(), anyDouble(), anyDouble(), any())).thenReturn(2);
when(decorator.pickBlockData(any(), any(), anyDouble(), anyDouble())).thenReturn(cane);
when(decorator.pickBlockDataTop(any(), any(), anyDouble(), anyDouble())).thenReturn(cane);
output.fill(air);
}
private void place(Placement placement) {
int world = -16 + local;
switch (placement) {
case FLOATING_SINGLE -> DecoratorCore.placeFloatingSimple(decorator, local, local, world, world,
3, 4, output, new RNG(1L), null, mantle);
case FLOATING_STACK -> DecoratorCore.placeFloatingStacked(decorator, local, local, world, world,
3, 4, output, new RNG(1L), null, mantle);
case CEILING_SINGLE -> DecoratorCore.placeSingleAt(decorator, local, local, world, 4, world,
output, new RNG(1L), null, false, mantle);
case CEILING_STACK -> DecoratorCore.placeStackDown(decorator, local, local, world, world,
4, 0, output, new RNG(1L), null, 2, new DecoratorCore.PlaceOpts(), mantle);
}
}
}
}
@@ -0,0 +1,181 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisSugarCaneTest {
@Test
public void localCaneRequiresSuitableSubstrateAndWaterAtItsBase() {
Fixture fixture = new Fixture(3);
fixture.output.set(1, 3, 1, fixture.soil);
assertFalse(fixture.place(1, 1));
fixture.output.set(0, 4, 1, fixture.water);
assertFalse(fixture.place(1, 1));
fixture.output.set(0, 3, 1, fixture.water);
assertTrue(fixture.place(1, 1));
when(fixture.cane.canPlaceOnto(fixture.soil)).thenReturn(false);
assertFalse(fixture.place(1, 1));
verifyNoInteractions(fixture.engine);
}
@Test
public void lavaAndOrdinaryIceDoNotHydrateCane() {
Fixture fixture = new Fixture(3);
fixture.output.set(1, 3, 1, fixture.soil);
for (String key : new String[]{"minecraft:lava", "minecraft:ice", "minecraft:stone"}) {
fixture.output.set(0, 3, 1, state(key));
assertFalse(key, fixture.place(1, 1));
}
fixture.output.set(0, 3, 1, state("minecraft:frosted_ice[age=0]"));
assertTrue(fixture.place(1, 1));
PlatformBlockState waterlogged = state("minecraft:oak_stairs[waterlogged=true]");
when(waterlogged.isWaterLogged()).thenReturn(true);
fixture.output.set(0, 3, 1, waterlogged);
assertTrue(fixture.place(1, 1));
}
@Test
public void caneStackKeepsItsValidBaseAndCannotReplaceWater() {
Fixture fixture = new Fixture(3);
when(fixture.cane.canPlaceOnto(fixture.cane)).thenReturn(true);
fixture.output.set(1, 3, 1, fixture.cane);
assertFalse(fixture.place(1, 1));
fixture.output.set(1, 2, 1, fixture.soil);
fixture.output.set(0, 2, 1, fixture.water);
assertTrue(fixture.place(1, 1));
fixture.output.set(1, 4, 1, fixture.water);
assertFalse(fixture.place(1, 1));
}
@Test
public void crossChunkWaterUsesAcceptedHydrologyAtTheCorrectElevation() {
Fixture fixture = new Fixture(1);
fixture.plannedTerrain();
fixture.output.set(0, 3, 0, fixture.soil);
HydrologyColumnLayer layer = wetLayer(1, 3);
when(fixture.complex.sampleHydrologyColumn(-17, -16)).thenReturn(new HydrologyColumnSample(
-17, -16, 6, 0, false, "parent", List.of(layer)));
when(fixture.complex.resolveHydrologyFluid("water", -17, -16)).thenReturn(fixture.water);
assertTrue(fixture.place(0, 0));
PlatformBlockState lava = state("minecraft:lava");
when(fixture.complex.resolveHydrologyFluid("water", -17, -16)).thenReturn(lava);
assertFalse(fixture.place(0, 0));
when(fixture.complex.sampleHydrologyColumn(-17, -16)).thenReturn(new HydrologyColumnSample(
-17, -16, 6, 0, false, "parent", List.of(wetLayer(1, 2))));
assertFalse(fixture.place(0, 0));
}
@Test
public void localBlocksOverridePlannedWaterWithoutReadingAnotherChunk() {
Fixture fixture = new Fixture(3);
fixture.output.set(1, 3, 1, fixture.soil);
fixture.output.set(0, 3, 1, state("minecraft:stone"));
assertFalse(fixture.place(1, 1));
verifyNoInteractions(fixture.engine);
}
@Test
public void forcedSurfaceDecorationStillRejectsCaneOnStone() {
Fixture fixture = new Fixture(3);
IrisDecorator decorator = mock(IrisDecorator.class);
PlatformBlockState stone = state("minecraft:stone");
PlatformBlockState air = state("minecraft:air");
when(air.isAir()).thenReturn(true);
when(decorator.isForcePlace()).thenReturn(true);
when(decorator.pickBlockData(any(), any(), anyDouble(), anyDouble())).thenReturn(fixture.cane);
fixture.output.set(1, 3, 1, stone);
fixture.output.set(1, 4, 1, air);
fixture.output.set(0, 3, 1, fixture.water);
DecoratorCore.placeSurfaceSingle(decorator, 1, 1, -15, 3, -15,
fixture.output, new RNG(3L), null, false, false, null);
assertSame(air, fixture.output.get(1, 4, 1));
}
@Test
public void naturalCrossChunkWaterHonorsAuthoredSeaLayers() {
Fixture fixture = new Fixture(1);
fixture.plannedTerrain();
fixture.output.set(0, 3, 0, fixture.soil);
when(fixture.complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 1D));
when(fixture.complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 3D));
IrisBiome biome = mock(IrisBiome.class);
when(fixture.complex.getTrueBiomeStream()).thenReturn(
ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome)));
when(fixture.engine.getSeedManager()).thenReturn(mock(SeedManager.class));
PlatformBlockState ice = state("minecraft:ice");
when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(), anyInt(), any()))
.thenReturn(new KList<>(List.of(ice)));
assertFalse(fixture.place(0, 0));
when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(), anyInt(), any()))
.thenReturn(new KList<>(List.of(fixture.water)));
assertTrue(fixture.place(0, 0));
}
private static HydrologyColumnLayer wetLayer(int bed, int head) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
11L, HydrologyFeatureType.RIFFLE, 12L, 13L, -17, head, -16, 1, 0, false);
return new HydrologyColumnLayer(feature, bed, head, head,
true, false, false, true, false, false, true, true, false,
"water", "river", "mouth", "shore", "bank", "cave");
}
private static PlatformBlockState state(String key) {
PlatformBlockState state = mock(PlatformBlockState.class);
when(state.key()).thenReturn(key);
return state;
}
private static final class Fixture {
private final Engine engine = mock(Engine.class);
private final IrisComplex complex = mock(IrisComplex.class);
private final PlatformBlockState cane = state("minecraft:sugar_cane[age=0]");
private final PlatformBlockState soil = state("minecraft:dirt");
private final PlatformBlockState water = state("minecraft:water");
private final Hunk<PlatformBlockState> output;
private Fixture(int size) {
output = Hunk.newArrayHunk(size, 6, size);
when(cane.canPlaceOnto(soil)).thenReturn(true);
when(water.isWater()).thenReturn(true);
}
private void plannedTerrain() {
when(engine.getComplex()).thenReturn(complex);
when(complex.getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 3D));
when(complex.getRiverWaterSurfaceStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> 0D));
}
private boolean place(int x, int z) {
return IrisSugarCane.canPlace(cane, output, x, 4, z, -16 + x, -16 + z, engine);
}
}
}
@@ -48,7 +48,7 @@ public class HydrologyCaveCourseFilterTest {
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(16, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = course(ridge);
HydrologyColumnLayer layer = layer(ridge, 8, 8);
@@ -105,7 +105,7 @@ public class HydrologyCaveCourseFilterTest {
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(16, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
@@ -148,7 +148,7 @@ public class HydrologyCaveCourseFilterTest {
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(16, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = course(ridge);
List<HydrologyColumnSample> columns = List.copyOf(
@@ -235,7 +235,7 @@ public class HydrologyCaveCourseFilterTest {
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(16, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
@@ -271,7 +271,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(16, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
CavePosition exposedBoundary = new CavePosition(8, 25, 8);
AtomicInteger voxelLoads = new AtomicInteger();
@@ -310,7 +310,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = course(ridge);
RiverFootprint footprint = footprint(terrain, layer(ridge, 0, 8));
@@ -347,7 +347,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 20, 0))
List.of(new HydrologyPoint(0, 20, 0)), HydraulicChannelProfile.uniform(2, 2)
);
HydraulicSegment drop = new HydraulicSegment(
173L,
@@ -363,7 +363,7 @@ public class HydrologyCaveCourseFilterTest {
new HydrologyPoint(0, 20, 0),
new HydrologyPoint(6, 19, 0),
new HydrologyPoint(12, 18, 0)
)
), HydraulicChannelProfile.uniform(2, 2)
);
HydraulicSegment surfacePool = new HydraulicSegment(
174L,
@@ -375,7 +375,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(12, 18, 0))
List.of(new HydrologyPoint(12, 18, 0)), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = new RiverCourse(
171L,
@@ -418,7 +418,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = undergroundCourse(tunnel);
RiverFootprint footprint = footprint(terrain, layer(tunnel, 8, 8));
@@ -467,7 +467,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = new RiverCourse(
pool.courseId(),
@@ -516,7 +516,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(0, 20, 0),
new HydrologyPoint(12, 20, 0)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = course(ridge);
Random random = new Random(0x485944524f4c4f47L);
@@ -576,7 +576,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(8, 76, 8))
List.of(new HydrologyPoint(8, 76, 8)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = course(surface);
RiverFootprint footprint = footprint(terrain, surfaceLayer(surface, 8, 8));
@@ -615,7 +615,7 @@ public class HydrologyCaveCourseFilterTest {
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(16, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
RiverFootprint footprint = footprint(terrain, layer(ridge, 8, 8));
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
@@ -778,7 +778,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 20, 8), new HydrologyPoint(1, 20, 8))
List.of(new HydrologyPoint(0, 20, 8), new HydrologyPoint(1, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
);
HydraulicSegment grotto = grotto(142L, 140L, HydrologyFeatureType.INLAND_GROTTO);
RiverCourse course = new RiverCourse(
@@ -831,7 +831,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8))
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = undergroundCourse(tunnel);
HydrologyColumnLayer oversized = caveLayer(
@@ -890,7 +890,7 @@ public class HydrologyCaveCourseFilterTest {
}
@Test
public void selfCarvedUndergroundCourseDoesNotMaterializeObservedCavesOrBoundaryGuards() {
public void selfCarvedUndergroundCourseChecksTerrainWithoutLoadingExistingCaves() {
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 0D, "parent");
HydraulicSegment tunnel = new HydraulicSegment(
161L,
@@ -902,15 +902,16 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8))
List.of(new HydrologyPoint(8, 20, 8), new HydrologyPoint(9, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = undergroundCourse(tunnel);
RiverFootprint footprint = footprint(terrain, layer(tunnel, 8, 8), layer(tunnel, 9, 8));
AtomicInteger observedLoads = new AtomicInteger();
AtomicInteger terrainChecks = new AtomicInteger();
CaveVoxelView observedView = new CaveVoxelView() {
@Override
public boolean isInWorld(CavePosition position) {
observedLoads.incrementAndGet();
terrainChecks.incrementAndGet();
return true;
}
@@ -928,7 +929,7 @@ public class HydrologyCaveCourseFilterTest {
@Override
public boolean isAboveTerrainSurface(CavePosition position) {
observedLoads.incrementAndGet();
terrainChecks.incrementAndGet();
return false;
}
};
@@ -952,6 +953,7 @@ public class HydrologyCaveCourseFilterTest {
.allMatch((CaveVoxelPrecondition precondition) -> precondition.voxel() == CaveVoxel.UNCONDITIONAL));
assertTrue(diagnostics.isEmpty());
assertEquals(0, observedLoads.get());
assertTrue(terrainChecks.get() > 0);
}
@Test
@@ -970,7 +972,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(0, 20, 8),
new HydrologyPoint(8, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
HydraulicSegment drop = new HydraulicSegment(
103L,
@@ -985,7 +987,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(9, 18, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
HydraulicSegment surface = new HydraulicSegment(
104L,
@@ -997,7 +999,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(9, 18, 8))
List.of(new HydrologyPoint(9, 18, 8)), HydraulicChannelProfile.uniform(2, 2)
);
RiverCourse course = new RiverCourse(
101L,
@@ -1045,7 +1047,7 @@ public class HydrologyCaveCourseFilterTest {
List.of(
new HydrologyPoint(8, 20, 8),
new HydrologyPoint(24, 20, 8)
)
), HydraulicChannelProfile.uniform(2, 2)
);
HydrologyColumnLayer local = layer(ridge, 15, 8);
HydrologyColumnLayer remote = layer(ridge, 16, 8);
@@ -1095,7 +1097,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse seaCave = new RiverCourse(
920L,
@@ -1258,7 +1260,7 @@ public class HydrologyCaveCourseFilterTest {
2,
false,
false,
List.of(new HydrologyPoint(8, 20, 8))
List.of(new HydrologyPoint(8, 20, 8)), HydraulicChannelProfile.uniform(2, 2)
);
}
@@ -0,0 +1,69 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
import org.junit.Test;
import java.util.List;
import java.util.OptionalLong;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyCoastalDropReservoirTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
@Test
public void aDeclaredCoastalGrottoReceivesTheFallThroughUnownedNaturalOceanWater() {
HydrologyTerrainSampler terrain = terrain(54);
RiverCourse course = course(SETTINGS.seaLevel(), 1, true);
HydrologyGeometrySampler geometry = HydrologyGeometrySampler.deterministic(terrain);
HydrologySurfaceDropRaster full = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, geometry, course);
assertTrue(full.connects(0, 0, 92));
assertTrue(full.connects(0, 0, SETTINGS.seaLevel()));
assertFalse(full.connects(1, 0, SETTINGS.seaLevel()));
assertTrue(full.containsRequiredFluid(course, null));
for (HydrologyColumnSample column : full.columns()) {
assertFalse(column.ocean());
}
SurfaceBounds receivingWindow = new SurfaceBounds(1, -2, 2, 2);
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(
SETTINGS, terrain, geometry, course, receivingWindow);
assertTrue(bounded.columns().isEmpty());
assertTrue(bounded.containsRequiredFluid(course, receivingWindow));
}
@Test
public void anOceanBiomeOrUnrelatedGrottoCannotReplaceTheRequiredFluidInterval() {
HydrologyTerrainSampler dryOceanBiome = terrain(SETTINGS.seaLevel());
RiverCourse exact = course(SETTINGS.seaLevel(), 1, true);
assertFalse(HydrologySurfaceDropRaster.compile(SETTINGS, dryOceanBiome,
HydrologyGeometrySampler.deterministic(dryOceanBiome), exact).containsRequiredFluid(exact, null));
HydrologyTerrainSampler ocean = terrain(54);
for (RiverCourse invalid : List.of(course(SETTINGS.seaLevel(), 1, false),
course(SETTINGS.seaLevel(), 2, true), course(SETTINGS.seaLevel() + 1, 1, true))) {
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(
SETTINGS, ocean, HydrologyGeometrySampler.deterministic(ocean), invalid);
assertFalse(raster.containsRequiredFluid(invalid, null));
}
}
private static HydrologyTerrainSampler terrain(int oceanFloor) {
return (x, z) -> x >= 1 ? HydrologyTerrainSample.ocean(oceanFloor, "ocean")
: HydrologyTerrainSample.openLand(92, 0D, "land");
}
private static RiverCourse course(int head, int grottoEnd, boolean grotto) {
HydraulicSegment falling = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
92, head, 4, 3, true, true,
List.of(new HydrologyPoint(0, 92, 0), new HydrologyPoint(1, head, 0)),
HydraulicChannelProfile.uniform(4, 3));
HydraulicSegment chamber = new HydraulicSegment(5L, 1L, HydrologyFeatureType.COASTAL_GROTTO,
head, head, 4, 3, false, false,
List.of(new HydrologyPoint(0, head, 0), new HydrologyPoint(grottoEnd, head, 0)),
HydraulicChannelProfile.uniform(4, 3));
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"default", 1, List.of(), grotto ? List.of(falling, chamber) : List.of(falling));
}
}
@@ -8,6 +8,17 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyCrossTileSurfaceAdmissionTest {
@Test
public void separatedTributariesCanJoinTheSameRegionalOutlet() {
HydrologyCrossTileSurfaceAdmission.Claim first = claim(10L, 100L,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 63, 0)), false, 128);
HydrologyCrossTileSurfaceAdmission.Claim second = claim(20L, 100L,
List.of(new HydrologyPoint(0, 70, 512), new HydrologyPoint(256, 63, 512)), false, 128);
assertTrue(HydrologyCrossTileSurfaceAdmission.admit(List.of(first), List.of(
new HydrologyCrossTileSurfaceAdmission.RankedClaim(new HydrologyTileKey(0, 1), 0, second)))
.rejections().isEmpty());
}
@Test
public void rejectsClusteredIndependentMouths() {
HydrologyCrossTileSurfaceAdmission.Claim current = claim(
@@ -80,7 +80,7 @@ public class HydrologyFootprintCompilerTest {
? 63 : 70;
HydraulicSegment segment = new HydraulicSegment(
9001L, 9000L, type, head, head, 6, 3, false, false,
List.of(new HydrologyPoint(-12, head, -3), new HydrologyPoint(32, head, 5)));
List.of(new HydrologyPoint(-12, head, -3), new HydrologyPoint(32, head, 5)), HydraulicChannelProfile.uniform(6, 3));
RiverCourse course = course(9000L,
type.isSurface() ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND, segment);
SlopeCountingSampler original = new SlopeCountingSampler(false);
@@ -124,7 +124,7 @@ public class HydrologyFootprintCompilerTest {
4,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(10, 4)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(
z < 0 ? 54 : 82,
@@ -158,7 +158,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = new RiverCourse(
10L,
@@ -206,7 +206,7 @@ public class HydrologyFootprintCompilerTest {
5,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0)), HydraulicChannelProfile.uniform(12, 5)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint footprint = new HydrologyFootprintCompiler(
@@ -269,7 +269,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
RiverCourse course = course(14L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(70, 0D, "parent");
@@ -328,7 +328,7 @@ public class HydrologyFootprintCompilerTest {
8,
false,
false,
List.of(new HydrologyPoint(0, -20, 0))
List.of(new HydrologyPoint(0, -20, 0)), HydraulicChannelProfile.uniform(40, 8)
);
RiverCourse course = new RiverCourse(
14L,
@@ -382,7 +382,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(16, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(16, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = course(110L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 1D, "parent");
@@ -425,7 +425,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment undergroundSegment = new HydraulicSegment(
31L,
@@ -437,7 +437,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(20, 50, 0))
List.of(new HydrologyPoint(20, 50, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse surface = course(20L, RiverCourseType.SURFACE, surfaceSegment);
RiverCourse underground = course(30L, RiverCourseType.UNDERGROUND, undergroundSegment);
@@ -479,7 +479,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = course(40L, RiverCourseType.SURFACE, segment);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
@@ -531,7 +531,7 @@ public class HydrologyFootprintCompilerTest {
10,
false,
false,
List.of(new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(12, 10)
);
RiverCourse course = course(42L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(80, 1D, "parent");
@@ -584,7 +584,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(96, 70, 0)), HydraulicChannelProfile.uniform(12, 3)
);
RiverCourse course = course(46L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
@@ -613,7 +613,7 @@ public class HydrologyFootprintCompilerTest {
2,
true,
true,
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 68, 0))
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 68, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = course(50L, RiverCourseType.SURFACE, segment);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
@@ -692,7 +692,7 @@ public class HydrologyFootprintCompilerTest {
new HydrologyPoint(0, 48, 0),
new HydrologyPoint(12, 48, 5),
new HydrologyPoint(24, 48, 0)
)
), HydraulicChannelProfile.uniform(12, 2)
);
RiverCourse course = course(60L, RiverCourseType.UNDERGROUND, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
@@ -749,7 +749,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
)
);
RiverCourse underground = course(
@@ -765,7 +765,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(96, 40, 0))
List.of(new HydrologyPoint(96, 40, 0)), HydraulicChannelProfile.uniform(4, 2)
)
);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
@@ -823,7 +823,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment waterfall = new HydraulicSegment(
221L,
@@ -835,7 +835,7 @@ public class HydrologyFootprintCompilerTest {
2,
true,
true,
List.of(new HydrologyPoint(0, 74, 0))
List.of(new HydrologyPoint(0, 74, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 1D, "parent");
RiverFootprint footprint = new HydrologyFootprintCompiler(
@@ -865,7 +865,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(-2, 70, 0), new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(-2, 70, 0), new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment lower = new HydraulicSegment(
232L,
@@ -877,7 +877,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
true,
List.of(new HydrologyPoint(0, 68, 0), new HydrologyPoint(2, 68, 0))
List.of(new HydrologyPoint(0, 68, 0), new HydrologyPoint(2, 68, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 1D, "parent");
RiverFootprint footprint = new HydrologyFootprintCompiler(
@@ -920,7 +920,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0))
List.of(new HydrologyPoint(0, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = course(60L, RiverCourseType.SURFACE, segment);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
@@ -974,7 +974,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 63, 0))
List.of(new HydrologyPoint(0, 63, 0)), HydraulicChannelProfile.uniform(6, 2)
);
RiverCourse course = course(70L, RiverCourseType.SURFACE, segment);
@@ -1000,7 +1000,7 @@ public class HydrologyFootprintCompilerTest {
}
@Test
public void naturallySubmergedLandAtAMouthPublishesOnlyANonOwningApron() {
public void mouthCutsDrySeaLevelLandIntoAnOwnedChannel() {
HydraulicSegment mouth = new HydraulicSegment(
81L,
80L,
@@ -1011,16 +1011,16 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 63, 0))
List.of(new HydrologyPoint(0, 63, 0)), HydraulicChannelProfile.uniform(6, 2)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(63, 0D, "shore");
RiverFootprint footprint = compiler(terrain).compile(List.of(course(80L, RiverCourseType.SURFACE, mouth)));
HydrologyColumnSample sample = footprint.sample(0, 0).orElseThrow();
assertEquals(63, sample.terrainHeight());
assertTrue(sample.layers().stream().allMatch(HydrologyColumnLayer::oceanApron));
assertTrue(sample.layers().stream().noneMatch(HydrologyColumnLayer::terrainOwned));
assertTrue(sample.layers().stream().noneMatch(HydrologyColumnLayer::fluidOwned));
assertEquals(61, sample.terrainHeight());
assertTrue(sample.layers().stream().noneMatch(HydrologyColumnLayer::oceanApron));
assertTrue(sample.layers().stream().allMatch(HydrologyColumnLayer::terrainOwned));
assertTrue(sample.layers().stream().allMatch(HydrologyColumnLayer::fluidOwned));
}
@Test
@@ -1035,7 +1035,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment cave = new HydraulicSegment(
303L,
@@ -1047,7 +1047,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(8, 66, 0), new HydrologyPoint(12, 66, 0))
List.of(new HydrologyPoint(8, 66, 0), new HydrologyPoint(12, 66, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = new RiverCourse(
301L,
@@ -1112,7 +1112,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(6, 3)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(
x < 4 ? 90 : 77,
@@ -1159,7 +1159,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(-8, 70, 0), new HydrologyPoint(8, 70, 0))
List.of(new HydrologyPoint(-8, 70, 0), new HydrologyPoint(8, 70, 0)), HydraulicChannelProfile.uniform(6, 2)
),
new HydraulicSegment(
403L,
@@ -1171,7 +1171,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(8, 60, 0), new HydrologyPoint(14, 60, 0))
List.of(new HydrologyPoint(8, 60, 0), new HydrologyPoint(14, 60, 0)), HydraulicChannelProfile.uniform(4, 2)
),
new HydraulicSegment(
404L,
@@ -1183,7 +1183,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(14, 63, 0), new HydrologyPoint(20, 63, 0))
List.of(new HydrologyPoint(14, 63, 0), new HydrologyPoint(20, 63, 0)), HydraulicChannelProfile.uniform(6, 2)
)
)
);
@@ -1200,7 +1200,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 74, -4), new HydrologyPoint(12, 74, -4))
List.of(new HydrologyPoint(0, 74, -4), new HydrologyPoint(12, 74, -4)), HydraulicChannelProfile.uniform(4, 2)
)
);
RiverCourse underground = new RiverCourse(
@@ -1222,7 +1222,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(4, 45, 2), new HydrologyPoint(16, 45, 2))
List.of(new HydrologyPoint(4, 45, 2), new HydrologyPoint(16, 45, 2)), HydraulicChannelProfile.uniform(4, 2)
),
new HydraulicSegment(
423L,
@@ -1234,7 +1234,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(16, 63, 2), new HydrologyPoint(20, 63, 2))
List.of(new HydrologyPoint(16, 63, 2), new HydrologyPoint(20, 63, 2)), HydraulicChannelProfile.uniform(4, 2)
)
)
);
@@ -1256,9 +1256,9 @@ public class HydrologyFootprintCompilerTest {
).compile(courses);
for (HydrologyColumnSample sample : full.columns().values()) {
assertEquals(sample.primarySurfaceLayer().isPresent(),
assertEquals(sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent(),
validation.plannedSurface().ownsTerrain(sample.x(), sample.z()));
assertEquals(sample.primarySurfaceLayer().isPresent(),
assertEquals(sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent(),
validation.withMaterializedSurface(full).plannedSurface().ownsTerrain(sample.x(), sample.z()));
assertEquals(
"planned surface at " + sample.x() + "," + sample.z(),
@@ -1319,7 +1319,7 @@ public class HydrologyFootprintCompilerTest {
new HydrologyPoint(0, 70, 0),
new HydrologyPoint(32, 70, 0),
new HydrologyPoint(64, 70, 0)
)
), HydraulicChannelProfile.uniform(8, 3)
);
HydraulicSegment first = new HydraulicSegment(
502L,
@@ -1331,7 +1331,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydraulicSegment second = new HydraulicSegment(
503L,
@@ -1343,7 +1343,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(64, 70, 0))
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(64, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint continuousFootprint = compiler(terrain).compile(List.of(new RiverCourse(
@@ -1398,7 +1398,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydraulicSegment second = new HydraulicSegment(
512L,
@@ -1410,7 +1410,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(32, 70, 32))
List.of(new HydrologyPoint(32, 70, 0), new HydrologyPoint(32, 70, 32)), HydraulicChannelProfile.uniform(8, 3)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint footprint = compiler(terrain).compile(List.of(new RiverCourse(
@@ -1442,10 +1442,11 @@ public class HydrologyFootprintCompilerTest {
70,
70,
8,
3,
4,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(64, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0), new HydrologyPoint(64, 70, 0)),
new HydraulicChannelProfile(new double[]{8D, 8D, 8D}, new double[]{4D, 3D, 3D})
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverCourse course = course(520L, RiverCourseType.SURFACE, segment);
@@ -1481,7 +1482,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 74, 0), new HydrologyPoint(24, 70, 0))
List.of(new HydrologyPoint(0, 74, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint footprint = compiler(terrain).compile(List.of(course(527L, RiverCourseType.SURFACE, cascade)));
@@ -1507,7 +1508,7 @@ public class HydrologyFootprintCompilerTest {
}
@Test
public void aOneBlockLowBankIsLippedAndADeeperOneIsReportedWithoutRaising() {
public void lowBanksKeepTheirNaturalHeightAndReportUncontainedWater() {
HydraulicSegment channel = new HydraulicSegment(
529L,
528L,
@@ -1518,7 +1519,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
RiverCourse course = course(528L, RiverCourseType.SURFACE, channel);
HydrologyTerrainSampler highTerrain = (int x, int z) ->
@@ -1547,12 +1548,12 @@ public class HydrologyFootprintCompilerTest {
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
// One block short of the water: a one-block lip holds it and nothing is reported.
// A low bank remains below the water and is reported without raising terrain.
assertEquals(69, lowBank.naturalHeight());
assertEquals(70, lowBank.terrainHeight());
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
assertEquals(69, lowBank.terrainHeight());
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
assertTrue(sample.terrainHeight() <= sample.naturalHeight() + 1);
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
}
// Two blocks short: the bank keeps its natural height and the edge is reported.
assertEquals(68, deepBank.naturalHeight());
@@ -1572,7 +1573,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
RiverCourse course = course(528L, RiverCourseType.SURFACE, channel);
HydrologyFootprintCompiler compiler = compiler((int x, int z) ->
@@ -1605,7 +1606,7 @@ public class HydrologyFootprintCompilerTest {
new HydrologyPoint(2, 72, 0),
new HydrologyPoint(3, 71, 0),
new HydrologyPoint(4, 70, 0)
)
), HydraulicChannelProfile.uniform(8, 3)
);
RiverCourse course = course(529L, RiverCourseType.SURFACE, cascade);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.ocean(82, "ocean");
@@ -1626,7 +1627,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(32, 63, 0))
List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(32, 63, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydraulicSegment mouth = new HydraulicSegment(
532L,
@@ -1638,7 +1639,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(32, 63, 0), new HydrologyPoint(64, 63, 0))
List.of(new HydrologyPoint(32, 63, 0), new HydrologyPoint(64, 63, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydrologyTerrainSampler terrain = (int x, int z) -> x >= 64
? HydrologyTerrainSample.ocean(48, "ocean")
@@ -1696,7 +1697,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(48, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(48, 70, 0)), HydraulicChannelProfile.uniform(8, 3)
);
HydraulicSegment sinkhole = new HydraulicSegment(
542L,
@@ -1708,7 +1709,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
true,
List.of(new HydrologyPoint(48, 70, 0), new HydrologyPoint(52, 60, 0))
List.of(new HydrologyPoint(48, 70, 0), new HydrologyPoint(52, 60, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment grotto = new HydraulicSegment(
543L,
@@ -1720,7 +1721,7 @@ public class HydrologyFootprintCompilerTest {
4,
false,
true,
List.of(new HydrologyPoint(52, 60, 0))
List.of(new HydrologyPoint(52, 60, 0)), HydraulicChannelProfile.uniform(8, 4)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint footprint = compiler(terrain).compile(List.of(new RiverCourse(
@@ -1761,7 +1762,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(64, 70, 0))
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(64, 70, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment receiver = new HydraulicSegment(
552L,
@@ -1773,7 +1774,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
true,
List.of(new HydrologyPoint(64, 70, 0), new HydrologyPoint(80, 69, 0))
List.of(new HydrologyPoint(64, 70, 0), new HydrologyPoint(80, 69, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment outflow = new HydraulicSegment(
553L,
@@ -1785,7 +1786,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(80, 69, 0), new HydrologyPoint(160, 69, 0))
List.of(new HydrologyPoint(80, 69, 0), new HydrologyPoint(160, 69, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(82, 0D, "parent");
RiverFootprint footprint = compiler(terrain).compile(List.of(new RiverCourse(
@@ -1876,7 +1877,7 @@ public class HydrologyFootprintCompilerTest {
3,
false,
false,
List.of(new HydrologyPoint(96, 63, 0), new HydrologyPoint(112, 63, 0))
List.of(new HydrologyPoint(96, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(8, 3)
);
RiverFootprint mouthFootprint = new HydrologyFootprintCompiler(
seaCaveSettings(8, 12),
@@ -1966,7 +1967,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(4, 2)
);
return new RiverCourse(
id,
@@ -1992,10 +1993,10 @@ public class HydrologyFootprintCompilerTest {
12,
true,
false,
List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(32, 70, 0))
List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(32, 70, 0)), HydraulicChannelProfile.uniform(16, 12)
);
RiverCourse course = course(120L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(96, 1D, "parent");
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(70, 1D, "parent");
RiverFootprint defaults = compile(HydrologyPlannerSettings.defaults(), terrain, course);
RiverFootprint explicit = compile(withFlow(new HydrologyPlannerSettings.Flow(0.65D, 2, 2D, 1)), terrain, course);
@@ -2071,7 +2072,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 48, 0))
List.of(new HydrologyPoint(0, 48, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = course(140L, RiverCourseType.UNDERGROUND, chamber);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(84, 1D, "parent");
@@ -2137,7 +2138,7 @@ public class HydrologyFootprintCompilerTest {
new HydrologyPoint(0, 48, 0),
new HydrologyPoint(bend / 2, 48, 5),
new HydrologyPoint(bend, 48, 0)
)
), HydraulicChannelProfile.uniform(12, 2)
);
return new RiverCourse(
id,
@@ -67,7 +67,7 @@ public class HydrologyModelTest {
2,
false,
true,
centerline
centerline, HydraulicChannelProfile.uniform(4, 2)
);
assertEquals(6, graded.drop());
assertFalse(graded.fallingFluid());
@@ -81,7 +81,7 @@ public class HydrologyModelTest {
2,
false,
false,
centerline
centerline, HydraulicChannelProfile.uniform(4, 2)
);
assertFalse(intermediate.receivingPool());
HydraulicSegment accepted = new HydraulicSegment(
@@ -94,7 +94,7 @@ public class HydrologyModelTest {
2,
true,
true,
centerline
centerline, HydraulicChannelProfile.uniform(4, 2)
);
assertEquals(6, accepted.drop());
}
@@ -111,7 +111,7 @@ public class HydrologyModelTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 80, 0))
List.of(new HydrologyPoint(0, 80, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment sinkhole = new HydraulicSegment(
2L,
@@ -123,7 +123,7 @@ public class HydrologyModelTest {
2,
true,
true,
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 74, 0))
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(1, 74, 0)), HydraulicChannelProfile.uniform(4, 2)
);
HydraulicSegment grotto = new HydraulicSegment(
3L,
@@ -135,7 +135,7 @@ public class HydrologyModelTest {
2,
false,
false,
List.of(new HydrologyPoint(1, 74, 0))
List.of(new HydrologyPoint(1, 74, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse accepted = new RiverCourse(
@@ -160,7 +160,7 @@ public class HydrologyModelTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 80, 0))
List.of(new HydrologyPoint(0, 80, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse containedApproach = new RiverCourse(
9L,
@@ -197,7 +197,7 @@ public class HydrologyModelTest {
2,
false,
false,
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0)), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse seaCave = new RiverCourse(
@@ -288,7 +288,7 @@ public class HydrologyModelTest {
assertThrows(IllegalArgumentException.class, () -> new HydrologyColumnSample(
4,
9,
63,
62,
63,
false,
"shore_parent",
@@ -298,7 +298,7 @@ public class HydrologyModelTest {
HydrologyColumnSample raised = new HydrologyColumnSample(
4,
9,
64,
63,
63,
false,
"shore_parent",
@@ -394,6 +394,23 @@ public class HydrologyModelTest {
assertEquals(72, sample.terrainHeight());
}
@Test
public void uncutShoreRetainsItsBiomeWithoutOverridingAnOwnedBank() {
HydrologyColumnLayer shore = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 30L, 75),
75, 75, false, true, true, false, false);
HydrologyColumnLayer bank = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 31L, 72),
72, 72, false, false, true, true, false);
HydrologyColumnSample uncut = new HydrologyColumnSample(0, 0, 75, 63, false, "parent", List.of(shore));
HydrologyColumnSample cut = new HydrologyColumnSample(0, 0, 75, 63, false, "parent", List.of(shore, bank));
assertEquals(shore, uncut.primarySurfaceLayer().orElseThrow());
assertEquals("shore", uncut.primarySurfaceLayer().orElseThrow().biomeKey());
assertEquals(75, uncut.terrainHeight());
assertTrue(uncut.primarySurfaceFluidLayer().isEmpty());
assertEquals(bank, cut.primarySurfaceLayer().orElseThrow());
assertEquals(72, cut.terrainHeight());
}
@Test
public void acceptedSurfaceArbitrationPrefersChannelThenHydraulicType() {
HydrologyColumnLayer grading = surfaceLayer(
@@ -620,7 +637,7 @@ public class HydrologyModelTest {
new HydrologyPoint(0, 20, 0),
new HydrologyPoint(500, 20, 0),
new HydrologyPoint(1000, 20, 0)
)
), HydraulicChannelProfile.uniform(4, 2)
);
RiverCourse course = new RiverCourse(
201L,
@@ -0,0 +1,79 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
import org.junit.Test;
import java.util.List;
import java.util.OptionalLong;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class HydrologyMouthDropReservoirTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
private static final HydrologyTerrainSample FLOODED = HydrologyTerrainSample.openLand(54, 0D, "coast");
@Test
public void aMouthReceivesTheFallThroughProvenConnectedWaterWithoutOwningTheFloodedCoast() {
HydrologyTerrainSampler source = coast();
RiverCourse course = course(SETTINGS.seaLevel(), 1);
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forCourse(SETTINGS, source, course);
HydrologyGeometrySampler geometry = HydrologyGeometrySampler.deterministic(source);
HydrologySurfaceDropRaster full = HydrologySurfaceDropRaster.compile(SETTINGS, scoped, geometry, course);
assertTrue(full.connects(0, 0, 92));
assertFalse(full.connects(1, 0, SETTINGS.seaLevel()));
assertTrue(full.containsRequiredFluid(course, null));
assertSame(FLOODED, scoped.sample(1, 0));
assertFalse(scoped.sample(1, 0).ocean());
for (HydrologyColumnSample column : full.columns()) {
assertTrue(column.naturalHeight() >= SETTINGS.seaLevel());
}
SurfaceBounds receivingWindow = new SurfaceBounds(1, -1, 2, 1);
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(
SETTINGS, scoped, geometry, course, receivingWindow);
assertTrue(bounded.columns().isEmpty());
assertTrue(bounded.containsRequiredFluid(course, receivingWindow));
}
@Test
public void aRaisedMouthOrBrokenConnectionCannotReplaceTheRequiredReceiverInterval() {
HydrologyTerrainSampler source = coast();
for (RiverCourse course : List.of(course(SETTINGS.seaLevel() + 1, 1), course(SETTINGS.seaLevel(), 2))) {
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forCourse(SETTINGS, source, course);
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(
SETTINGS, scoped, HydrologyGeometrySampler.deterministic(source), course);
assertFalse(raster.containsRequiredFluid(course, null));
}
RiverCourse course = course(SETTINGS.seaLevel(), 1);
for (HydrologyTerrainSampler terrain : List.<HydrologyTerrainSampler>of(
(x, z) -> x == 4 ? HydrologyTerrainSample.openLand(70, 0D, "sill") : source.sample(x, z),
(x, z) -> x >= 8 ? FLOODED : source.sample(x, z))) {
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forCourse(SETTINGS, terrain, course);
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(
SETTINGS, scoped, HydrologyGeometrySampler.deterministic(terrain), course);
assertFalse(raster.containsRequiredFluid(course, null));
}
assertFalse(HydrologySurfaceDropRaster.compile(SETTINGS, source,
HydrologyGeometrySampler.deterministic(source), course).containsRequiredFluid(course, null));
}
private static HydrologyTerrainSampler coast() {
return (x, z) -> x <= 0 ? HydrologyTerrainSample.openLand(92, 0D, "land")
: x >= 8 ? HydrologyTerrainSample.ocean(54, "ocean") : FLOODED;
}
private static RiverCourse course(int mouthHead, int mouthStart) {
HydraulicSegment falling = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
92, SETTINGS.seaLevel(), 4, 3, true, true,
List.of(new HydrologyPoint(0, 92, 0), new HydrologyPoint(1, SETTINGS.seaLevel(), 0)),
HydraulicChannelProfile.uniform(4, 3));
HydraulicSegment mouth = new HydraulicSegment(5L, 1L, HydrologyFeatureType.MOUTH,
mouthHead, mouthHead, 4, 3, false, false,
List.of(new HydrologyPoint(mouthStart, mouthHead, 0), new HydrologyPoint(8, mouthHead, 0)),
HydraulicChannelProfile.uniform(4, 3));
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"default", 1, List.of(), List.of(falling, mouth));
}
}
@@ -28,7 +28,8 @@ public class HydrologyOceanBoundaryRefinerTest {
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
crossing,
terrainSampler,
routingSampler
routingSampler,
63
);
assertEquals(List.of(point(1, -7), point(2, -7), point(3, -7)), probes);
@@ -50,7 +51,8 @@ public class HydrologyOceanBoundaryRefinerTest {
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
crossing(-2, 1, 9),
terrainSampler,
routingSampler(new ArrayList<>(), -1, HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN)
routingSampler(new ArrayList<>(), -1, HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN),
63
);
assertEquals(List.of(point(-2, 9)), detailSamples);
@@ -69,7 +71,8 @@ public class HydrologyOceanBoundaryRefinerTest {
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
crossing(0, 3, 0),
terrainSampler,
routingSampler(new ArrayList<>(), 2, HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE)
routingSampler(new ArrayList<>(), 2, HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE),
63
);
assertNull(result);
@@ -87,13 +90,33 @@ public class HydrologyOceanBoundaryRefinerTest {
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
crossing(-3, 0, 2),
terrainSampler,
routingSampler(new ArrayList<>(), Integer.MAX_VALUE, HydrologyRoutingTerrainSampler.NaturalClassification.LAND)
routingSampler(new ArrayList<>(), Integer.MAX_VALUE, HydrologyRoutingTerrainSampler.NaturalClassification.LAND),
63
);
assertNull(result);
assertEquals(List.of(), detailSamples);
}
@Test
public void floodedCoastRetreatsToPhysicalShoreAndKeepsKnownOceanEndpoint() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(x < 2 ? 306 : 282, 0D, "coast");
HydrologyOceanBoundaryRefiner.Result result = HydrologyOceanBoundaryRefiner.refine(
crossing(0, 6, 0), terrain,
routingSampler(new ArrayList<>(), 5, HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN), 306);
assertEquals(1, result.landwardPoint().x());
assertEquals(306, result.landwardTerrain().naturalHeight());
assertEquals(5, result.oceanPoint().x());
}
@Test
public void aCompletelySubmergedCoastEdgeHasNoLandwardEndpoint() {
assertNull(HydrologyOceanBoundaryRefiner.refine(crossing(0, 5, 0),
(x, z) -> HydrologyTerrainSample.openLand(282, 0D, "flooded"),
routingSampler(new ArrayList<>(), 5, HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN), 306));
}
private HydrologyRoutingTerrainSampler routingSampler(
List<HydrologyPoint> probes,
int classifiedX,
@@ -0,0 +1,152 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.List;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class HydrologyOceanReceiverTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
private static final HydrologyTerrainSample LAND = HydrologyTerrainSample.openLand(70, 0D, "coast");
private static final HydrologyTerrainSample FLOODED = HydrologyTerrainSample.openLand(60, 0D, "coast");
private static final HydrologyTerrainSample OCEAN = HydrologyTerrainSample.ocean(60, "ocean");
@Test
public void connectedFloodedCoastIsAReceiverWithoutChangingRawTerrain() {
HydrologyTerrainSampler source = coast();
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forOutlet(SETTINGS, source, outlet());
for (int x = 1; x < 8; x++) {
assertTrue(scoped.receivingWater(x, 0, 63));
assertSame(FLOODED, scoped.sample(x, 0));
assertFalse(scoped.sample(x, 0).ocean());
assertFalse(source.receivingWater(x, 0, 63));
}
assertTrue(scoped.receivingWater(3, 1, 63));
assertFalse(scoped.receivingWater(0, 0, 63));
assertFalse(scoped.receivingWater(3, 0, 64));
}
@Test
public void isolatedFloodedBasinIsNotPromotedInsideTheSameHalo() {
HydrologyTerrainSampler source = (x, z) -> x == 3 && z == 5 ? FLOODED : coast().sample(x, z);
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forOutlet(SETTINGS, source, outlet());
assertTrue(scoped.receivingWater(3, 1, 63));
assertFalse(scoped.receivingWater(3, 5, 63));
assertSame(FLOODED, scoped.sample(3, 5));
}
@Test
public void aDrySillBreaksTheClaimedNaturalConnection() {
HydrologyTerrainSampler source = (x, z) -> x == 4 ? LAND : coast().sample(x, z);
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forOutlet(SETTINGS, source, outlet());
assertFalse(scoped.receivingWater(3, 0, 63));
assertFalse(scoped.receivingWater(5, 0, 63));
assertTrue(scoped.receivingWater(8, 0, 63));
}
@Test
public void aFloodedEndpointWithoutKnownSeaDoesNotCertifyTheRun() {
HydrologyTerrainSampler source = (x, z) -> x == 0 ? LAND : FLOODED;
HydrologyTerrainSampler scoped = HydrologyOceanReceiver.forOutlet(SETTINGS, source, outlet());
assertSame(source, scoped);
assertFalse(scoped.receivingWater(8, 0, 63));
}
@Test
public void diagonalWetSamplesRequireACardinalWaterBridge() {
RiverCourse course = course(List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(3, 63, 3)), 8);
HydrologyTerrainSampler diagonal = (x, z) -> x == 3 && z == 3 ? OCEAN
: x == z && x > 0 ? FLOODED : LAND;
assertFalse(HydrologyOceanReceiver.forCourse(SETTINGS, diagonal, course).receivingWater(1, 1, 63));
HydrologyTerrainSampler bridged = (x, z) -> x == 1 && z == 2 || x == 2 && z == 3
? FLOODED : diagonal.sample(x, z);
assertTrue(HydrologyOceanReceiver.forCourse(SETTINGS, bridged, course).receivingWater(1, 1, 63));
}
@Test
public void overlappingCourseViewsKeepIdenticalTerrainMetadata() {
HydrologyTerrainSampler source = coast();
HydrologyTerrainSampler first = HydrologyOceanReceiver.forCourse(SETTINGS, source,
course(List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(8, 63, 0)), 8));
HydrologyTerrainSampler second = HydrologyOceanReceiver.forCourse(SETTINGS, source,
course(List.of(new HydrologyPoint(0, 63, 1), new HydrologyPoint(8, 63, 1)), 4));
HydrologyColumnSample firstColumn = new FootprintMutableColumn(3, 0, first.sample(3, 0), 63).build();
HydrologyColumnSample secondColumn = new FootprintMutableColumn(3, 0, second.sample(3, 0), 63).build();
HydrologyFootprintCompiler.validateMatchingTerrainMetadata(firstColumn, secondColumn);
assertEquals(firstColumn, secondColumn);
assertFalse(firstColumn.ocean());
for (int x = 7; x > 0; x--) {
assertEquals(first.receivingWater(x, 0, 63), second.receivingWater(x, 0, 63));
}
}
@Test
public void receiverSamplingHasAFixedBudgetEvenForAWideMouth() {
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler source = (x, z) -> {
samples.incrementAndGet();
return x == 8 && z == 0 ? OCEAN : x <= 0 ? LAND : FLOODED;
};
HydrologyOceanReceiver.forCourse(SETTINGS, source,
course(List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(8, 63, 0)), 1000));
assertTrue(samples.get() <= 65_538);
assertTrue(samples.get() >= 65_536);
}
@Test
public void aMouthCannotBorrowAnotherMouthsOceanWitness() {
HydrologyTerrainSampler source = coast();
HydraulicSegment proven = course(List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(8, 63, 0)), 8)
.segments().getFirst();
HydraulicSegment unproven = course(List.of(new HydrologyPoint(0, 63, 1), new HydrologyPoint(7, 63, 1)), 8)
.segments().getFirst();
HydrologyTerrainSampler first = HydrologyOceanReceiver.forMouth(SETTINGS, source, proven);
HydrologyTerrainSampler second = HydrologyOceanReceiver.forMouth(SETTINGS, source, unproven);
assertTrue(first.receivingWater(7, 1, 63));
assertFalse(second.receivingWater(7, 1, 63));
assertSame(source, second);
assertSame(first.sample(7, 1), second.sample(7, 1));
}
@Test
public void anIncomingCoastalMouthReversesOnlyItsProofDirection() {
HydraulicSegment incoming = course(List.of(new HydrologyPoint(8, 63, 0), new HydrologyPoint(0, 63, 0)), 8)
.segments().getFirst();
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forMouth(SETTINGS, coast(), incoming);
assertTrue(receiver.receivingWater(1, 0, 63));
assertSame(FLOODED, receiver.sample(1, 0));
assertEquals(new HydrologyPoint(8, 63, 0), incoming.start());
assertEquals(new HydrologyPoint(0, 63, 0), incoming.end());
}
private static HydrologyTerrainSampler coast() {
return (x, z) -> Math.abs(z) > 1 || x <= 0 ? LAND : x >= 8 ? OCEAN : FLOODED;
}
private static RiverOutlet outlet() {
return new RiverOutlet(1L, HydrologyFeatureType.MOUTH, 2L,
new HydrologyPoint(0, 63, 0), new HydrologyPoint(8, 63, 0), 63, true);
}
private static RiverCourse course(List<HydrologyPoint> points, int width) {
HydraulicSegment mouth = new HydraulicSegment(3L, 4L, HydrologyFeatureType.MOUTH,
63, 63, width, 3, false, false, points, HydraulicChannelProfile.uniform(width, 3));
return new RiverCourse(4L, RiverCourseType.SURFACE, OptionalLong.of(2L), OptionalLong.of(1L),
"water", 1, List.of(), List.of(mouth));
}
}
@@ -0,0 +1,71 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class HydrologyOutletBoundaryPolicyTest {
@Test
public void aCoarseOceanMatchCannotHideAMismatchedExactShoreOrReceivingCell() {
for (int mismatchedX : new int[] {127, 128}) {
HydrologyTerrainSampler terrain = (x, z) -> sample(x >= 128, x == mismatchedX ? "other" : "area");
HydrologyPlanner planner = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologySampledGrid grid = grid(terrain);
assertTrue(grid.node(0).terrain().drainsInto(grid.node(1).terrain()));
assertTrue(planner.outletPlanner.oceanOutletCandidates(grid, true).isEmpty());
}
}
@Test
public void aMatchingExactBoundaryKeepsItsReceivingOceanCell() {
HydrologyTerrainSampler terrain = (x, z) -> sample(x >= 128, "area");
HydrologyPlanner planner = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<OutletCandidate> outlets = planner.outletPlanner.oceanOutletCandidates(grid(terrain), true);
assertFalse(outlets.isEmpty());
for (OutletCandidate candidate : outlets) {
assertEquals(127, candidate.outlet().landwardPoint().x());
assertEquals(128, candidate.outlet().connectionPoint().x());
assertTrue(terrain.sample(candidate.outlet().connectionPoint().x(), candidate.outlet().connectionPoint().z()).ocean());
}
}
@Test
public void aCoastalChannelRequiresTheOriginOceanToEnterTheLandArea() {
HydrologyTerrainSampler terrain = (x, z) -> sample(x >= 1, x >= 1 ? "sea" : null);
HydrologyPlanner planner = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(true, 1), terrain);
RiverOutlet outlet = new RiverOutlet(1L, HydrologyFeatureType.MOUTH, 2L,
new HydrologyPoint(0, 63, 0), new HydrologyPoint(1, 63, 0), 63, true);
assertTrue(terrain.sample(0, 0).drainsInto(terrain.sample(1, 0)));
assertFalse(planner.regional.bidirectionalMouth(outlet));
HydrologyTerrainSampler matching = (x, z) -> sample(x >= 1, "sea");
HydrologyPlanner accepted = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(true, 1), matching);
assertTrue(accepted.regional.bidirectionalMouth(outlet));
}
private static HydrologySampledGrid grid(HydrologyTerrainSampler terrain) {
ArrayList<HydrologyGridNode> nodes = new ArrayList<>();
for (int index = 0; index < 4; index++) {
int x = index % 2 * 256;
int z = index / 2 * 256;
nodes.add(new HydrologyGridNode(index, index % 2, index / 2, x, z, index + 1L, terrain.sample(x, z)));
}
return new HydrologySampledGrid(0, 0, 0, 0, 512, 2, 256, nodes);
}
private static HydrologyTerrainSample sample(boolean ocean, String confines) {
int height = ocean ? 60 : 66;
return new HydrologyTerrainSample(height, 0D, ocean, false, height - 32, height - 30,
!ocean, !ocean, !ocean, false, false, false,
0D, ocean ? 0D : 1D, 0D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of("default"), List.of(),
Double.NaN, confines, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
}
}
@@ -17,7 +17,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyPlanHashTest {
private static final String EXPECTED_PLAN_DIGEST = "4ff5293a3d4110f54ffa82630ac95efa98cd7d51cf57911475f466c66f1433c8";
private static final String EXPECTED_PLAN_DIGEST = "20e96d6f310786d09e7182ae6f73e41e5b7d07591b5571a62b1299fe1ab58b9a";
private static final List<HydrologyTileKey> TILES = List.of(
new HydrologyTileKey(0, 0),
@@ -198,7 +198,7 @@ public class HydrologyPlanHashTest {
HydrologyPlannerSettings.Geometry.defaults().drops());
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
@@ -114,7 +114,7 @@ public class HydrologyPlannerConfinementTest {
HydrologyPlannerSettings.ChannelShape stableChannel = HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0),
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 12, 2, 4, 12, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(false, undergroundSources, 68, 82, 4, 14, 2, 5, 6, 12, false, 0),
@@ -301,7 +301,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0),
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
@@ -671,7 +671,7 @@ public class HydrologyPlannerGlobalInvariantTest {
0.5D,
0.1D,
1D,
0
0, HydrologyPlannerSettings.Regional.disabled()
),
new HydrologyPlannerSettings.Surface(
surfaceEnabled,
@@ -1254,6 +1254,11 @@ public class HydrologyPlannerGlobalInvariantTest {
addBoolean(segment.fallingFluid());
addBoolean(segment.receivingPool());
addPoints(segment.centerline());
addInt(segment.channelProfile().size());
for (int station = 0; station < segment.channelProfile().size(); station++) {
addLong(Double.doubleToLongBits(segment.channelProfile().widthAt(station)));
addLong(Double.doubleToLongBits(segment.channelProfile().depthAt(station)));
}
}
private void add(HydrologyCavePlan plan) {
@@ -310,7 +310,7 @@ public class HydrologyPlannerSeaCaveTest {
true, surfaceSources, 4, 18, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults());
return new HydrologyPlannerSettings(
SEA,
new HydrologyPlannerSettings.Routing(tileSize, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(tileSize, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
surface,
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(
@@ -86,7 +86,7 @@ public class HydrologyPlannerSettingsTest {
0D,
0D,
1D,
0
0, HydrologyPlannerSettings.Regional.disabled()
);
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
@@ -504,7 +504,7 @@ public class HydrologyPlannerSettingsTest {
HydrologyPlannerSettings.Erosion erosion = HydrologyPlannerSettings.Erosion.defaults();
assertEquals(new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D), erosion);
IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()), erosion);
assertEquals(erosion, HydrologyPlannerSettings.Erosion.of(true, 12, 0.45D, 1D, 0.5D));
assertEquals(IrisRiverBlendStyle.SMOOTH, erosion.style());
assertEquals(4, erosion.terraceSteps());
@@ -512,23 +512,23 @@ public class HydrologyPlannerSettingsTest {
assertEquals(IrisRiverBedProfile.BOWL, erosion.bedProfile());
assertEquals(0D, erosion.shoreRise(), 0D);
assertEquals(0D, erosion.blendBaseWidth(), 0D);
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.TERRACED, 2, 0D, IrisRiverBedProfile.V, 0D, 0D);
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.CLIFF, 16, 1D, IrisRiverBedProfile.U, 4D, 32D);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, null, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 1, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, -0.01D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 1.01D, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, Double.NaN, IrisRiverBedProfile.BOWL, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, null, 0D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, -0.1D, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, Double.POSITIVE_INFINITY, 0D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, Double.NaN));
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.TERRACED, 2, 0D, IrisRiverBedProfile.V, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults());
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.CLIFF, 16, 1D, IrisRiverBedProfile.U, 4D, 32D, HydrologyPlannerSettings.Excavation.defaults());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, null, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 1, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, -0.01D, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 1.01D, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, Double.NaN, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, null, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, -0.1D, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, Double.POSITIVE_INFINITY, 0D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, -0.1D, HydrologyPlannerSettings.Excavation.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, Double.NaN, HydrologyPlannerSettings.Excavation.defaults()));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings linear = withBanks(base, banksWith(banks,
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.LINEAR, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D),
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.LINEAR, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, HydrologyPlannerSettings.Excavation.defaults()),
banks.channel(), banks.flow()));
assertTrue(base.fingerprint() != linear.fingerprint());
assertEquals(base.publicationRadius(), linear.publicationRadius());
@@ -684,7 +684,7 @@ public class HydrologyPlannerSettingsTest {
HydrologyPlannerSettings.Geometry geometry = base.geometry();
HydrologyPlannerSettings reshaped = rebuild(base,
surfaceWith(base.surface(), banksWith(banks,
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.TERRACED, 8, 0.25D, IrisRiverBedProfile.U, 1D, 4D),
new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.TERRACED, 8, 0.25D, IrisRiverBedProfile.U, 1D, 4D, HydrologyPlannerSettings.Excavation.defaults()),
new HydrologyPlannerSettings.Channel(8, 0.5D, 2D, 2D),
new HydrologyPlannerSettings.Flow(0.5D, 3, 3D, 2))
.withInlet(new HydrologyPlannerSettings.Inlet(64, 3, 32, 0.25D, 2D))),
@@ -102,7 +102,7 @@ public class HydrologyPlannerSurfacePoolTest {
private static HydrologyPlannerSettings settings(double density, int spacing, int minimumRadius, int maximumRadius, int depth) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0);
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0, HydrologyPlannerSettings.Regional.disabled());
return new HydrologyPlannerSettings(
base.seaLevel(),
routing,
@@ -415,7 +415,7 @@ public class HydrologyPlannerTest {
new HydrologyPlannerSettings.Routing(tileSize, spacing, Math.max(routing.maximumRouteNodes(), latticeWidth * latticeWidth),
routing.maximumRouteLength(), routing.minimumSurfaceCourseLength(), routing.minimumUndergroundCourseLength(),
routing.valleyPreference(), routing.uphillPenalty(), routing.slopePenalty(),
routing.confluenceAttraction(), routing.lengthPreference(), routing.tributaries()),
routing.confluenceAttraction(), routing.lengthPreference(), routing.tributaries(), HydrologyPlannerSettings.Regional.disabled()),
settings.surface(), settings.hydraulics(), settings.underground(), settings.outlets(), settings.geometry(),
settings.deepFluids(), settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(),
HydrologyPlannerSettings.SurfacePolicyBounds.NONE
@@ -755,7 +755,7 @@ public class HydrologyPlannerTest {
0,
24
));
HydrologyTerrainSampler connected = (int x, int z) -> terrain(
HydrologyTerrainSampler connected = (int x, int z) -> x >= 112 ? oceanTerrain() : terrain(
110 - Math.floorDiv(x, 16),
1D,
x >= 112,
@@ -1196,7 +1196,7 @@ public class HydrologyPlannerTest {
routing.valleyPreference(),
routing.uphillPenalty(),
routing.slopePenalty(),
routing.confluenceAttraction(), 1D, 0
routing.confluenceAttraction(), 1D, 0, HydrologyPlannerSettings.Regional.disabled()
),
base.surface(),
base.hydraulics(),
@@ -2575,7 +2575,7 @@ public class HydrologyPlannerTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(
surfaceDensity > 0D || surfaceSources.maximumPerTile() > 0,
surfaceSources,
@@ -2648,7 +2648,7 @@ public class HydrologyPlannerTest {
2D,
0.2D,
1D,
0
0, HydrologyPlannerSettings.Regional.disabled()
),
new HydrologyPlannerSettings.Surface(
true,
@@ -6,10 +6,12 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class HydrologyPlannerTributaryTest {
@@ -108,7 +110,7 @@ public class HydrologyPlannerTributaryTest {
centerline.add(new HydrologyPoint(x, 120, 0));
}
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline), HydraulicChannelProfile.uniform(4, 2))));
planner.tributaries.levelApproach(segments, 100);
@@ -146,7 +148,7 @@ public class HydrologyPlannerTributaryTest {
centerline.add(new HydrologyPoint(x, x >= 4 && x <= 6 ? 122 : 120, 0));
}
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline), HydraulicChannelProfile.uniform(4, 2))));
planner.tributaries.levelApproach(segments, 112);
@@ -163,6 +165,82 @@ public class HydrologyPlannerTributaryTest {
assertTrue(segments.getFirst().upstreamHeadY() >= segments.getFirst().downstreamHeadY());
}
@Test
public void loweringAnApproachCanFlattenAContainedFallIntoAnExposedPool() {
HydrologyTerrainSampler terrain = (x, z) -> land(100);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
HydrologyPlanner planner = new HydrologyPlanner(23L, settings, terrain);
HydraulicChannelProfile profile = new HydraulicChannelProfile(new double[]{8D, 6D}, new double[]{3D, 2D});
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
1L, 7L, HydrologyFeatureType.WATERFALL, 120, 110, 8, 3, true, true,
List.of(new HydrologyPoint(0, 120, 0), new HydrologyPoint(1, 110, 0)), profile)));
planner.tributaries.levelApproach(segments, 100);
HydraulicSegment level = segments.getFirst();
assertEquals(HydrologyFeatureType.SURFACE_POOL, level.type());
assertEquals(100, level.upstreamHeadY());
assertEquals(100, level.downstreamHeadY());
assertFalse(level.fallingFluid());
assertFalse(level.receivingPool());
assertSame(profile, level.channelProfile());
assertEquals(List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(1, 100, 0)), level.centerline());
RiverCourse course = new RiverCourse(7L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"default", 1, List.of(), segments);
assertSame(HydrologySurfaceDropRaster.empty(), HydrologySurfaceDropRaster.compile(
settings, terrain, HydrologyGeometrySampler.deterministic(terrain), course));
}
@Test
public void aPartlyLoweredApproachRetainsItsPositiveFallingIntervalAndDimensions() {
HydrologyTerrainSampler terrain = (x, z) -> land(x == 0 ? 118 : 100);
HydrologyPlanner planner = new HydrologyPlanner(23L, HydrologyPlannerSettings.defaults(), terrain);
HydraulicChannelProfile profile = new HydraulicChannelProfile(new double[]{8D, 6D}, new double[]{3D, 2D});
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
1L, 7L, HydrologyFeatureType.WATERFALL, 120, 110, 8, 3, true, true,
List.of(new HydrologyPoint(0, 120, 0), new HydrologyPoint(1, 110, 0)), profile)));
planner.tributaries.levelApproach(segments, 100);
HydraulicSegment falling = segments.getFirst();
assertEquals(HydrologyFeatureType.WATERFALL, falling.type());
assertTrue(falling.upstreamHeadY() > falling.downstreamHeadY());
assertEquals(100, falling.downstreamHeadY());
assertTrue(falling.fallingFluid());
assertTrue(falling.receivingPool());
assertSame(profile, falling.channelProfile());
assertEquals(falling.upstreamHeadY(), falling.start().y());
assertEquals(falling.downstreamHeadY(), falling.end().y());
}
@Test
public void backwaterClearsOnlyTheFallsThatItCompletelySubmerges() {
HydraulicChannelProfile profile = new HydraulicChannelProfile(new double[]{8D, 6D}, new double[]{3D, 2D});
HydraulicSegment original = new HydraulicSegment(
1L, 7L, HydrologyFeatureType.WATERFALL, 120, 110, 8, 3, true, true,
List.of(new HydrologyPoint(0, 120, 0), new HydrologyPoint(1, 110, 0)), profile);
ArrayList<HydraulicSegment> partlySubmerged = new ArrayList<>(List.of(original));
ArrayList<HydraulicSegment> submerged = new ArrayList<>(List.of(original));
HydrologyTributaryPlanner.backwater(partlySubmerged, 115);
HydrologyTributaryPlanner.backwater(submerged, 120);
HydraulicSegment falling = partlySubmerged.getFirst();
assertEquals(HydrologyFeatureType.WATERFALL, falling.type());
assertEquals(5, falling.drop());
assertTrue(falling.fallingFluid());
assertTrue(falling.receivingPool());
assertSame(profile, falling.channelProfile());
HydraulicSegment level = submerged.getFirst();
assertEquals(HydrologyFeatureType.SURFACE_POOL, level.type());
assertEquals(120, level.upstreamHeadY());
assertEquals(0, level.drop());
assertFalse(level.fallingFluid());
assertFalse(level.receivingPool());
assertSame(profile, level.channelProfile());
assertEquals(List.of(new HydrologyPoint(0, 120, 0), new HydrologyPoint(1, 120, 0)), level.centerline());
}
private static HydrologyTerrainSample land(int height) {
return new HydrologyTerrainSample(
height,
@@ -275,7 +353,7 @@ public class HydrologyPlannerTributaryTest {
private static HydrologyPlannerSettings settings(int tributaries, double density, int maximumPerTile) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, tributaries);
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, tributaries, HydrologyPlannerSettings.Regional.disabled());
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, density, 0, 0, maximumPerTile, 128);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
true, sources, 4, 8, 2, 4, 40, 1.5D, HydrologyPlannerSettings.Banks.defaults());
@@ -433,7 +511,7 @@ public class HydrologyPlannerTributaryTest {
HydrologyPlannerSettings.ChannelShape stableChannel = HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(false, none, 4, 18, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(true, sources, 68, 82, 4, 12, 2, 4, 5, 9, true, tributaries),
@@ -577,7 +577,7 @@ public class HydrologyPlannerUndergroundKnobsTest {
);
return new HydrologyPlannerSettings(
SEA_LEVEL,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
surface,
new HydrologyPlannerSettings.Hydraulics(4),
HydrologyPlannerSettings.Underground.of(
@@ -638,7 +638,7 @@ public class HydrologyPlannerUndergroundKnobsTest {
) {
return new HydrologyPlannerSettings(
SEA_LEVEL,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(
surfaceEnabled,
surfaceSources,
@@ -0,0 +1,101 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalAdaptiveBendsTest {
@Test
public void confinedValleysRetainSmallerBendsAndLegalEndpoints() {
List<HydrologyPoint> guide = guide();
HydrologyPlannerSettings settings = HydrologyRegionalMorphologyTest.settings(8192, 256);
HydrologyTerrainSampler open = (x, z) -> HydrologyTerrainSample.openLand(76, 0D, "land");
HydrologyTerrainSampler confined = (x, z) -> HydrologyTerrainSample.openLand(Math.abs(z - 768) < 16 ? 76 : 108, 0D, "land");
HydrologyRegionalRoute.Refinement broad = refine(settings, open, guide);
HydrologyRegionalRoute.Refinement tight = refine(settings, confined, guide);
assertNull(broad.toString(), broad.rejection());
assertNull(tight.toString(), tight.rejection());
assertTrue(maximumDeviation(broad.points()) >= 32);
assertTrue(maximumDeviation(tight.points()) < maximumDeviation(broad.points()) / 2D);
assertTrue(maximumDeviation(tight.points()) > 0);
assertEquals(guide.getFirst(), tight.points().getFirst());
assertEquals(guide.getLast(), tight.points().getLast());
assertEquals(tight, refine(settings, confined, guide));
assertTrue(tight.points().stream().allMatch(point -> point.y() == 76));
}
@Test
public void reducedFreedomBlendsTheFixedDetailFieldWithoutMovingEndpoints() throws Exception {
HydrologyPlannerSettings settings = HydrologyRegionalMorphologyTest.settings(8192, 256);
List<HydrologyPoint> guide = guide();
HydrologyTerrainSampler open = (x, z) -> HydrologyTerrainSample.openLand(76, 0D, "land");
HydrologyTerrainSampler narrow = (x, z) -> HydrologyTerrainSample.openLand(Math.abs(z - 768) < 12 ? 76 : 108, 0D, "land");
List<HydrologyPoint> broad = interpolate(settings, open, guide);
List<HydrologyPoint> tight = interpolate(settings, narrow, guide);
assertTrue("broad=" + crossings(broad) + " tight=" + crossings(tight), crossings(tight) > crossings(broad));
assertTrue(maximumDeviation(tight) < maximumDeviation(broad) / 2D);
assertEquals(guide.getFirst(), tight.getFirst());
assertEquals(guide.getLast(), tight.getLast());
for (HydrologyPoint point : tight) {
assertTrue(Math.abs(point.z() - 768) <= 256D * settings.geometry().meanders().maximumOffsetRatio());
}
}
private static HydrologyRegionalRoute.Refinement refine(HydrologyPlannerSettings settings, HydrologyTerrainSampler terrain,
List<HydrologyPoint> guide) {
HydrologyRegionalRoute route = new HydrologyRegionalRoute(new HydrologyPlanner(1337L, settings, terrain));
return route.select(guide, "default", false, terrain,
candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
}
@SuppressWarnings("unchecked")
private static List<HydrologyPoint> interpolate(HydrologyPlannerSettings settings, HydrologyTerrainSampler terrain,
List<HydrologyPoint> guide) throws Exception {
HydrologyRegionalRoute route = new HydrologyRegionalRoute(new HydrologyPlanner(1337L, settings, terrain));
Method method = HydrologyRegionalRoute.class.getDeclaredMethod("interpolate", List.class, double.class, HydrologyRegionalMorphology.class);
method.setAccessible(true);
return (List<HydrologyPoint>) method.invoke(route, guide, 1D, HydrologyRegionalMorphology.sample(guide, terrain, settings));
}
private static int maximumDeviation(List<HydrologyPoint> points) {
int maximum = 0;
for (HydrologyPoint point : points) {
maximum = Math.max(maximum, Math.abs(point.z() - 768));
}
return maximum;
}
private static int crossings(List<HydrologyPoint> points) {
int previous = 0;
int crossings = 0;
for (HydrologyPoint point : points) {
if (point.x() < 512 || point.x() > 5632) {
continue;
}
int side = point.z() > 769 ? 1 : point.z() < 767 ? -1 : 0;
if (side != 0) {
if (previous != 0 && side != previous) {
crossings++;
}
previous = side;
}
}
return crossings;
}
private static List<HydrologyPoint> guide() {
ArrayList<HydrologyPoint> guide = new ArrayList<>();
for (int x = 0; x <= 6144; x += 256) {
guide.add(new HydrologyPoint(x, 76, 768));
}
return guide;
}
}
@@ -0,0 +1,157 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicInteger;
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 HydrologyRegionalBankSupportTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
private static final HydrologyGeometrySampler GEOMETRY = request -> switch (request.field()) {
case SURFACE_WIDTH -> 8;
case SURFACE_DEPTH -> 2;
default -> request.minimum();
};
@Test
public void containedCenterDepressionKeepsTheDirectRouteAndFinalWaterWithoutRaisingLand() throws Exception {
HydrologyTerrainSampler terrain = depression(false);
List<HydrologyPoint> path = path();
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path, "default", false, terrain);
assertEquals(path, refined);
assertNull(validate(planner, path, terrain).rejection());
SurfaceCourseResult built = build(path, terrain);
assertTrue(built.accepted());
RiverCourse course = new RiverCourse(91L, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(2L),
"default", 1, List.of(), built.segments());
SurfaceFootprint footprint = new SurfaceFootprintCompiler(SETTINGS, terrain, GEOMETRY).compile(course);
assertTrue(footprint.accepted());
assertEquals(0, footprint.uncontainedWetCells());
assertTrue(course.hydraulicallyNonRising());
for (SurfaceLayerColumn column : footprint.columns()) {
assertTrue(column.layer().bedY() <= column.terrain().naturalHeight());
}
SurfaceLayerColumn center = footprint.columns().stream()
.filter(column -> column.x() == 132 && column.z() == 0).findFirst().orElseThrow();
assertEquals(65, center.terrain().naturalHeight());
assertEquals(65, center.layer().bedY());
assertEquals(80, center.layer().fluidHeadY());
}
@Test
public void depressionAcrossTheBanksStillFailsTheUnchangedIncisionLimit() throws Exception {
HydrologyTerrainSampler terrain = depression(true);
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
validate(planner, path(), terrain).rejection());
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path(), "default", false, terrain);
if (!refined.isEmpty()) {
assertFalse(build(refined, terrain).accepted());
}
SurfaceCourseResult built = build(path(), terrain);
assertFalse(built.accepted());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
assertEquals(17, built.rejectionDetail());
}
@Test
public void levelCentersNeedNoBankSamplesAndSupportedDipsCannotRaiseIncomingHead() {
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
assertTrue(Math.abs(x - 132) <= 5 && Math.abs(z) <= 5);
samples.incrementAndGet();
return depression(false).sample(x, z);
};
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
assertEquals(79, hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
132, 0, 1D, 0D, HydrologyTerrainSample.openLand(80, 0D, "land"), 79), terrain));
assertEquals(0, samples.get());
assertEquals(78, hydraulics.supportedHead(station(78), terrain));
assertTrue(samples.get() > 0 && samples.get() < 128);
}
@Test
public void unavailableCrossSectionOrPerimeterCannotCertifyAContainedDip() {
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
HydrologyTerrainSampler crossMissing = (x, z) -> x == 132 && z == 2 ? null : depression(false).sample(x, z);
HydrologyTerrainSampler perimeterMissing = (x, z) -> x == 133 ? null : depression(false).sample(x, z);
assertEquals(65, hydraulics.supportedHead(station(80), crossMissing));
assertEquals(65, hydraulics.supportedHead(station(80), perimeterMissing));
}
@Test
public void bankSupportCannotSamplePastTheExistingSearchBudget() throws Exception {
AtomicInteger requests = new AtomicInteger();
HydrologyTerrainSample ground = HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyTerrainSampler terrain = (x, z) -> {
requests.incrementAndGet();
return ground;
};
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(new HydrologyPlanner(71L, SETTINGS, terrain));
Map<Long, HydrologyTerrainSample> sampled = new HashMap<>();
for (int index = 0; index < 65536; index++) {
sampled.put(RiverFootprint.pack(index, 100000), ground);
}
Method sample = HydrologyRegionalTerrainRefiner.class.getDeclaredMethod("sample", int.class, int.class, Map.class);
sample.setAccessible(true);
HydrologyTerrainSampler limited = (x, z) -> {
try {
return (HydrologyTerrainSample) sample.invoke(refiner, x, z, sampled);
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
}
};
assertEquals(65, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(80), limited));
assertEquals(0, requests.get());
assertEquals(65536, sampled.size());
}
private static HydrologyRegionalHydraulics.HeadStation station(int incomingHead) {
return new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D,
HydrologyTerrainSample.openLand(65, 0D, "land"), incomingHead);
}
private static HydrologyTerrainSampler depression(boolean acrossBanks) {
return (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && (acrossBanks || z == 0) ? 65 : 80, 0D, "land");
}
private static List<HydrologyPoint> path() {
return List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(512, 80, 0));
}
private static SurfaceCourseResult build(List<HydrologyPoint> path, HydrologyTerrainSampler terrain) {
return new SurfaceCourseBuilder(SETTINGS.surface(), terrain, GEOMETRY, SETTINGS.seaLevel())
.build(71L, 91L, "default", path, SurfaceTerminal.SINKHOLE, 40, 256);
}
private static HydrologyRegionalRoute.Refinement validate(HydrologyPlanner planner, List<HydrologyPoint> path,
HydrologyTerrainSampler terrain) throws Exception {
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateHydraulics",
HydrologyRegionalRoute.Refinement.class, String.class, boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
return (HydrologyRegionalRoute.Refinement) validate.invoke(new HydrologyRegionalRoute(planner),
new HydrologyRegionalRoute.Refinement(path, null, null, 0, null), "default", false, terrain);
}
}
@@ -0,0 +1,85 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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;
public class HydrologyRegionalCacheTest {
@Test
public void clearingTilesRefreshesChangedRegionalTerrain() throws Exception {
AtomicInteger height = new AtomicInteger(70);
AtomicInteger reads = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
reads.incrementAndGet();
return HydrologyTerrainSample.openLand(height.get(), 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
assertEquals(70, refiner.sample(0, 0).naturalHeight());
height.set(85);
cache.clear();
assertEquals(85, refiner.sample(0, 0).naturalHeight());
assertEquals(2, reads.get());
}
}
@Test
public void clearingTilesRetriesPreviouslyUnavailableRegionalTerrain() throws Exception {
AtomicBoolean available = new AtomicBoolean();
AtomicInteger reads = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
reads.incrementAndGet();
return available.get() ? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
assertNull(refiner.sample(0, 0));
available.set(true);
cache.clear();
assertNotNull(refiner.sample(0, 0));
assertEquals(2, reads.get());
}
}
@Test
public void clearingTilesReplansRegionalReachesAgainstCurrentTerrain() throws Exception {
AtomicBoolean flooded = new AtomicBoolean();
HydrologyTerrainSampler terrain = (x, z) -> flooded.get() && x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
assertEquals(guide, refiner.refine(guide, "default", false, terrain));
flooded.set(true);
cache.clear();
List<HydrologyPoint> actual = refiner.refine(guide, "default", false, terrain);
List<HydrologyPoint> fresh = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
assertFalse(fresh.isEmpty());
assertTrue(fresh.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
assertEquals(fresh, actual);
}
}
private static HydrologyRegionalTerrainRefiner refiner(HydrologyPlanner planner) throws Exception {
Field routes = HydrologyRegionalPlanner.class.getDeclaredField("routes");
routes.setAccessible(true);
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrainRefiner");
refiner.setAccessible(true);
return (HydrologyRegionalTerrainRefiner) refiner.get(routes.get(planner.regional));
}
}
@@ -0,0 +1,202 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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 HydrologyRegionalCandidateSelectionTest {
@Test
public void lowerMeanderCandidatePassesTheFullPlannerAfterTheFirstBankCutFails() throws Exception {
HydrologyTerrainSampler terrain = coast(true);
HydrologyPlanner planner = planner(terrain, 16, 2);
List<HydrologyPoint> guide = guide(terrain);
Object context = context(guide, terrain);
ArrayList<HydrologyRegionalRoute.Attempt> attempted = new ArrayList<>();
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> {
HydrologyRegionalRoute.Attempt result = attempt(planner, context, candidate);
attempted.add(result);
return result;
});
assertTrue(selected.toString(), selected.accepted());
assertEquals(2, attempted.size());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, attempted.getFirst().rejection().rejection());
assertEquals(18, attempted.getFirst().rejection().detail());
assertEquals(1, selected.network().courses().size());
assertEquals(selected, attempted.getLast());
RiverCourse course = selected.network().courses().getFirst();
assertTrue(course.hydraulicallyNonRising());
assertEquals(selected.network().outlets().getFirst().id(), course.outletId().orElseThrow());
assertFalse(selected.network().nodes().isEmpty());
assertFalse(selected.network().edges().isEmpty());
assertNull(selected.rejection());
}
@Test
public void successfulFullAdmissionStopsAllLaterCurveEvaluation() throws Exception {
AtomicBoolean accepted = new AtomicBoolean();
HydrologyTerrainSampler source = coast(false);
HydrologyTerrainSampler terrain = (x, z) -> {
assertFalse("No later curve may sample terrain after final admission", accepted.get());
return source.sample(x, z);
};
HydrologyPlanner planner = planner(terrain, 16, 2);
List<HydrologyPoint> guide = guide(terrain);
Object context = context(guide, terrain);
AtomicInteger attempts = new AtomicInteger();
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> {
attempts.incrementAndGet();
HydrologyRegionalRoute.Attempt result = attempt(planner, context, candidate);
accepted.set(result.accepted());
return result;
});
assertTrue(selected.toString(), selected.accepted());
assertEquals(1, attempts.get());
}
@Test
public void exhaustedCurveSelectionDeduplicatesGeometryAndKeepsItsFinalFailure() {
HydrologyTerrainSampler terrain = coast(false);
HydrologyPlanner planner = planner(terrain, 16, 2);
Set<HydrologyRegionalRoute.Refinement> attempted = new HashSet<>();
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide(terrain), "default", false, terrain,
candidate -> {
assertTrue("An identical candidate must not repeat final validation", attempted.add(candidate));
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY,
new HydrologyDiagnosticCandidate(99L, HydrologyCandidateKind.REGIONAL_SOURCE,
HydrologyFeatureType.SURFACE_POOL, candidate.points().getFirst(),
HydrologyCandidateRejection.SURFACE_BANK_BUDGET, 7));
});
assertFalse(selected.accepted());
assertTrue(attempted.size() > 1 && attempted.size() <= 18);
assertEquals(HydrologyCandidateRejection.SURFACE_BANK_BUDGET, selected.rejection().rejection());
assertEquals(7, selected.rejection().detail());
}
@Test
public void failedSourcePublishesOneFinalDiagnosticAfterSeveralCandidateAttempts() throws Exception {
HydrologyTerrainSampler terrain = coast(false);
AtomicInteger sourceDepthSamples = new AtomicInteger();
HydrologyPlannerSettings settings = settings(2);
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 8;
case SURFACE_DEPTH -> {
if (request.x() == 0 && request.z() == 0) {
sourceDepthSamples.incrementAndGet();
}
yield 3;
}
default -> request.minimum();
};
HydrologyPlanner planner = new HydrologyPlanner(71L, settings, terrain, geometry, -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, 63, -4096, 4096));
ArrayList<HydrologyGridNode> nodes = new ArrayList<>();
for (int z = 0; z < 9; z++) {
for (int x = 0; x < 9; x++) {
int index = z * 9 + x;
nodes.add(new HydrologyGridNode(index, x, z, x * 128, z * 128, index + 100L, terrain.sample(x * 128, z * 128)));
}
}
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 1152, 9, 128, nodes);
HydrologyRegionalGraph.Label[] selected = new HydrologyRegionalGraph.Label[nodes.size()];
HydrologyRegionalGraph.Label downstream = null;
for (int index = 8; index >= 0; index--) {
downstream = new HydrologyRegionalGraph.Label(new HydrologyRegionalGraph.LabelState(
index, index, 63, (8 - index) * 128D, (8 - index) * 128D, 0), downstream);
selected[index] = downstream;
}
int[] contributions = new int[9];
Arrays.fill(contributions, 1);
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, contributions,
List.of(new OutletCandidate(8, 8, outlet())));
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
Method build = HydrologyRegionalPlanner.class.getDeclaredMethod("build", HydrologySampledGrid.class,
HydrologyRegionalGraph.Tree.class, int.class, OutletCandidate.class, long.class, List.class);
build.setAccessible(true);
HydrologyRegionalNetwork network = (HydrologyRegionalNetwork) build.invoke(planner.regional, grid, tree, 0, null, 123L, diagnostics);
assertTrue(network.courses().isEmpty());
assertEquals(1, diagnostics.size());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, diagnostics.getFirst().rejection());
assertTrue(sourceDepthSamples.get() > 1);
}
private static HydrologyRegionalRoute.Attempt attempt(HydrologyPlanner planner, Object context,
HydrologyRegionalRoute.Refinement candidate) {
try {
Method attempt = HydrologyRegionalPlanner.class.getDeclaredMethod("attempt", context.getClass(), HydrologyRegionalRoute.Refinement.class);
attempt.setAccessible(true);
return (HydrologyRegionalRoute.Attempt) attempt.invoke(planner.regional, context, candidate);
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
}
}
private static Object context(List<HydrologyPoint> guide, HydrologyTerrainSampler terrain) throws Exception {
Class<?> type = Class.forName(HydrologyRegionalPlanner.class.getName() + "$BuildContext");
Constructor<?> constructor = type.getDeclaredConstructors()[0];
constructor.setAccessible(true);
int[] contributions = new int[guide.size()];
Arrays.fill(contributions, 1);
return constructor.newInstance(new HydrologyGridNode(0, 0, 0, 0, 0, 100L, terrain.sample(0, 0)),
null, 123L, outlet(), "default", new HydrologyRegionalFlow(guide, contributions),
HydrologyRegionalMorphology.sample(guide, terrain, settings(16)));
}
private static RiverOutlet outlet() {
return new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 8L, new HydrologyPoint(1024, 63, 0),
new HydrologyPoint(1025, 63, 0), 63, true);
}
private static List<HydrologyPoint> guide(HydrologyTerrainSampler terrain) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (int x = 0; x <= 1024; x += 128) {
points.add(new HydrologyPoint(x, terrain.sample(x, 0).naturalHeight(), 0));
}
return List.copyOf(points);
}
private static HydrologyTerrainSampler coast(boolean lowBank) {
return (x, z) -> x >= 1025 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(lowBank && x == 288 && z == 19 ? 64
: 80 - Math.max(0, x - 752) / 16, 0D, "land");
}
private static HydrologyPlanner planner(HydrologyTerrainSampler terrain, int incision, int depth) {
HydrologyPlannerSettings settings = settings(incision);
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 8;
case SURFACE_DEPTH -> depth;
default -> request.minimum();
};
return new HydrologyPlanner(71L, settings, terrain, geometry, -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, 63, -4096, 4096));
}
private static HydrologyPlannerSettings settings(int incision) {
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Surface surface = base.surface();
return new HydrologyPlannerSettings(base.seaLevel(), base.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
surface.minimumDepth(), surface.maximumDepth(), incision, surface.shoreWidth(), surface.banks()),
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
}
}
@@ -0,0 +1,138 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.List;
import java.util.OptionalLong;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class HydrologyRegionalConnectivityTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
@Test
public void stackedWaterRequiresTheActualFallingIntervalToConnect() {
HydrologyRegionalConnectivity water = new HydrologyRegionalConnectivity();
water.addFluidColumn(0, 0, 67, 70);
water.addFluidColumn(1, 0, 67, 70);
water.addFluidColumn(1, 0, 60, 63);
water.addFluidColumn(2, 0, 60, 63);
assertFalse(water.connected(course(), ocean(), SETTINGS));
water.addFluidColumn(1, 0, 62, 68);
assertTrue(water.connected(course(), ocean(), SETTINGS));
}
@Test
public void anOceanBiomeAtSeaLevelIsNotAReceivingWaterColumn() {
HydrologyRegionalConnectivity water = new HydrologyRegionalConnectivity();
for (int x = 0; x < 3; x++) {
water.addFluidColumn(x, 0, 60, 63);
}
assertFalse(water.connected(course(), (x, z) -> HydrologyTerrainSample.ocean(63, "ocean"), SETTINGS));
assertTrue(water.connected(course(), ocean(), SETTINGS));
}
@Test
public void adjacentColumnsWithNoSharedWetHeightCannotConnect() {
HydrologyRegionalConnectivity water = new HydrologyRegionalConnectivity();
water.addFluidColumn(0, 0, 63, 66);
water.addFluidColumn(1, 0, 60, 63);
water.addFluidColumn(2, 0, 60, 63);
assertFalse(water.connected(course(), ocean(), SETTINGS));
}
@Test
public void aFloodedCoastalTailConnectsOwnedWaterToItsKnownOceanEndpoint() {
HydrologyRegionalConnectivity water = flatChannel();
HydrologyTerrainSampler sampler = floodedCoasts();
assertTrue(water.connected(extendedCourse(false), sampler, SETTINGS));
assertFalse(sampler.receivingWater(3, 0, 63));
}
@Test
public void aDrySillInTheFloodedTailBreaksItsOceanConnection() {
HydrologyTerrainSampler sampler = (x, z) -> x == 6
? HydrologyTerrainSample.openLand(63, 0D, "coast") : floodedCoasts().sample(x, z);
assertFalse(flatChannel().connected(extendedCourse(false), sampler, SETTINGS));
}
@Test
public void seaToSeaChannelsMustReachBothDeclaredCoasts() {
HydrologyTerrainSampler sampler = floodedCoasts();
HydrologyTerrainSampler blocked = (x, z) -> x == -4
? HydrologyTerrainSample.openLand(63, 0D, "coast") : sampler.sample(x, z);
assertTrue(flatChannel().connected(extendedCourse(true), sampler, SETTINGS));
assertFalse(flatChannel().connected(extendedCourse(true), blocked, SETTINGS));
}
@Test
public void anEarlierOceanPocketCannotStandInForTheDeclaredReceivingOcean() {
HydrologyTerrainSampler sampler = (x, z) -> x >= 3 && x != 6
? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(72, 0D, "land");
assertFalse(flatChannel().connected(extendedCourse(false), sampler, SETTINGS));
}
@Test
public void diagonalOceanStationsNeedCardinalWetBridges() {
HydraulicSegment channel = course().segments().getFirst();
HydraulicSegment mouth = mouth(2L, new HydrologyPoint(2, 63, 0), new HydrologyPoint(5, 63, 3));
RiverCourse course = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(channel, mouth));
HydrologyTerrainSampler sampler = (x, z) -> x >= 3 && x <= 5 && z == x - 2
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(72, 0D, "land");
HydrologyTerrainSampler bridged = (x, z) -> x >= 3 && x <= 5 && z == x - 3
? HydrologyTerrainSample.ocean(60, "ocean") : sampler.sample(x, z);
assertFalse(flatChannel().connected(course, sampler, SETTINGS));
assertTrue(flatChannel().connected(course, bridged, SETTINGS));
}
private static HydrologyRegionalConnectivity flatChannel() {
HydrologyRegionalConnectivity water = new HydrologyRegionalConnectivity();
for (int x = 0; x < 3; x++) {
water.addFluidColumn(x, 0, 60, 63);
}
return water;
}
private static HydrologyTerrainSampler floodedCoasts() {
return (x, z) -> x <= -8 || x >= 8 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x < 0 || x > 2 ? 60 : 72, 0D, "coast");
}
private static RiverCourse extendedCourse(boolean twoCoasts) {
HydraulicSegment channel = course().segments().getFirst();
HydraulicSegment outflow = mouth(2L, new HydrologyPoint(2, 63, 0), new HydrologyPoint(8, 63, 0));
List<HydraulicSegment> segments = twoCoasts ? List.of(
mouth(6L, new HydrologyPoint(-8, 63, 0), new HydrologyPoint(0, 63, 0)), channel, outflow)
: List.of(channel, outflow);
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), segments);
}
private static HydraulicSegment mouth(long id, HydrologyPoint start, HydrologyPoint end) {
return new HydraulicSegment(id, 1L, HydrologyFeatureType.MOUTH, 63, 63,
4, 3, false, false, List.of(start, end), HydraulicChannelProfile.uniform(4, 3));
}
private static HydrologyTerrainSampler ocean() {
return (x, z) -> x >= 3 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(72, 0D, "land");
}
private static RiverCourse course() {
HydraulicSegment channel = new HydraulicSegment(5L, 1L, HydrologyFeatureType.SURFACE_POOL, 63, 63,
4, 3, false, false, List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(2, 63, 0)),
HydraulicChannelProfile.uniform(4, 3));
HydraulicSegment segment = new HydraulicSegment(2L, 1L, HydrologyFeatureType.MOUTH, 63, 63,
4, 3, false, false, List.of(new HydrologyPoint(2, 63, 0), new HydrologyPoint(3, 63, 0)),
HydraulicChannelProfile.uniform(4, 3));
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(channel, segment));
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalCourseGraphTest {
@Test
public void truncatedCoursePublishesOnlyItsRetainedPathAndDischarge() {
List<HydrologyPoint> guide = List.of(point(0, 0), point(1000, 0), point(2000, 0));
HydrologyRegionalFlow flow = new HydrologyRegionalFlow(guide, new int[]{2, 50, 100});
List<HydrologyPoint> path = List.of(point(0, 0), point(0, 200), point(300, 200), point(500, 100));
HydrologyRegionalCourseGraph graph = HydrologyRegionalCourseGraph.create(terrain(),
new HydrologyRegionalCourseGraph.Options(7L, 9L, 128, path, flow));
assertEquals(0, graph.nodes().getFirst().x());
assertEquals(500, graph.nodes().getLast().x());
assertEquals(100, graph.nodes().getLast().z());
assertEquals(0D, graph.nodes().getLast().potential(), 0D);
assertEquals(26, graph.discharge());
double length = 500D + StrictMath.hypot(200D, 100D);
assertEquals(length, graph.nodes().getFirst().potential(), 1e-9D);
for (int index = 0; index < graph.edges().size(); index++) {
DrainageEdge edge = graph.edges().get(index);
assertEquals(graph.nodes().get(index).id(), edge.upstreamNodeId());
assertEquals(graph.nodes().get(index + 1).id(), edge.downstreamNodeId());
assertEquals(9L, edge.outletId());
assertTrue(edge.cost() > 0D);
assertTrue(edge.contributingSurfaceSources() <= graph.discharge());
assertTrue(edge.centerline().getLast().x() <= 500);
}
}
@Test
public void denseHydraulicPathsRetainOnlyBoundedCoarseGraphNodes() {
ArrayList<HydrologyPoint> path = new ArrayList<>();
for (int x = 0; x <= 8192; x++) {
path.add(point(x, 0));
}
HydrologyRegionalFlow flow = new HydrologyRegionalFlow(List.of(path.getFirst(), path.getLast()),
new int[]{1, 64});
HydrologyRegionalCourseGraph graph = HydrologyRegionalCourseGraph.create(terrain(),
new HydrologyRegionalCourseGraph.Options(7L, 9L, 256, path, flow));
assertEquals(33, graph.nodes().size());
assertEquals(32, graph.edges().size());
assertEquals(8192, graph.nodes().getLast().x());
assertEquals(64, graph.discharge());
for (DrainageEdge edge : graph.edges()) {
assertEquals(2, edge.centerline().size());
}
}
private static HydrologyPoint point(int x, int z) {
return new HydrologyPoint(x, 70, z);
}
private static HydrologyTerrainSampler terrain() {
return (x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land");
}
}
@@ -0,0 +1,128 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicInteger;
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 final class HydrologyRegionalFallsTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
private static final HydrologyTerrainSampler TERRAIN = (x, z) -> HydrologyTerrainSample.openLand(89, 0D, "parent");
@Test
public void fullContainmentProofIsCachedAndOnlyFinalTileActionsAreClipped() {
AtomicInteger validations = new AtomicInteger();
HydrologyPlanner planner = new HydrologyPlanner(1L, SETTINGS, TERRAIN, 0, surface -> {
validations.incrementAndGet();
return solid(null);
});
RiverCourse course = course();
HydrologyRegionalNetwork metadata = metadata(course, List.of());
List<HydrologyCavePlan> plans = new HydrologyRegionalFalls(planner, course).validate(metadata, new ArrayList<>());
assertEquals(1, plans.size());
HydrologyCavePlan full = plans.getFirst();
assertTrue(full.actions().containsKey(new CavePosition(12, 80, 0)));
HydrologyRegionalNetwork regional = metadata(course, plans);
assertTrue(regional.stations() > full.baselinePreconditions().size());
SurfaceBounds bounds = new SurfaceBounds(-2, -2, 1, 2);
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, planner.geometrySampler);
compiler.seedRegionalSurface(regional, bounds);
HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter.Result(metadata.nodes(), metadata.edges(),
metadata.outlets(), metadata.courses(), plans);
HydrologyCrossTileResolver.MaterializedHydrology materialized = new HydrologyCrossTileResolver(planner)
.materializeFinalHydrology(result, new ArrayList<>(), compiler, true);
assertEquals("A clipped raster cannot replace the full regional containment proof", 1, validations.get());
HydrologyCaveCourseFilter.Result clipped = HydrologyCrossTileResolver.clipRegionalPlans(materialized.result(), regional, bounds);
HydrologyCavePlan partial = clipped.cavePlans().getFirst();
assertFalse(partial.actions().isEmpty());
assertTrue(partial.actions().size() < full.actions().size());
assertTrue(partial.baselinePreconditions().keySet().stream().allMatch(position -> bounds.contains(position.x(), position.z())));
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : partial.actions().entrySet()) {
assertEquals(full.actions().get(entry.getKey()), entry.getValue());
}
HydrologyTile tile = new HydrologyTile(new HydrologyTileKey(0, 0), 1L, SETTINGS.fingerprint(), 512,
clipped.nodes(), clipped.edges(), clipped.outlets(), clipped.courses(), clipped.cavePlans(), List.of(), materialized.footprint());
assertEquals(1, tile.courses().size());
assertEquals(full, regional.cavePlans().getFirst());
}
@Test
public void exposedRegionalReceiverRejectsTheEntireCourse() {
HydrologyPlanner planner = new HydrologyPlanner(1L, SETTINGS, TERRAIN, solid(new CavePosition(12, 80, 0)));
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
assertNull(new HydrologyRegionalFalls(planner, course()).validate(metadata(course(), List.of()), diagnostics));
assertFalse(diagnostics.isEmpty());
assertEquals(HydrologyCandidateKind.REGIONAL_SOURCE, diagnostics.getFirst().kind());
}
@Test
public void localTerrainCutsCannotRemoveARegionalSealGuard() {
HydrologyPlanner planner = new HydrologyPlanner(1L, SETTINGS, TERRAIN, solid(null));
HydrologyCavePlan plan = new HydrologyRegionalFalls(planner, course())
.validate(metadata(course(), List.of()), new ArrayList<>()).getFirst();
CavePosition guard = plan.actions().entrySet().stream()
.filter(entry -> entry.getValue() == HydrologyCaveAction.SEAL_GUARD).findFirst().orElseThrow().getKey();
HydrologyFeatureRef feature = new HydrologyFeatureRef(11L, HydrologyFeatureType.SURFACE_POOL, 12L, 13L,
guard.x(), guard.y(), guard.z(), 1, 0, false);
HydrologyColumnLayer layer = new HydrologyColumnLayer(feature, guard.y() - 1, guard.y() - 1, guard.y() - 1,
false, false, true, false, false, false, true, false, false,
"water", "surface", "mouth", "shore", "bank", "cave");
HydrologyColumnSample column = new HydrologyColumnSample(guard.x(), guard.z(), 89, 63, false, "parent", List.of(layer));
RiverFootprint cut = new RiverFootprint(Map.of(RiverFootprint.pack(guard.x(), guard.z()), column));
assertTrue(HydrologyRegionalProtection.changesWitness("water", cut, "water", plan));
assertFalse(HydrologyRegionalProtection.changesWitness("water", RiverFootprint.empty(), "water", plan));
}
private static RiverCourse course() {
HydraulicSegment segment = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
90, 80, 4, 3, true, true, List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(12, 80, 0)),
HydraulicChannelProfile.uniform(4, 3));
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(segment));
}
private static HydrologyRegionalNetwork metadata(RiverCourse course, List<HydrologyCavePlan> plans) {
DrainageNode node = new DrainageNode(3L, 0, 0, TERRAIN.sample(0, 0), 1D, 4L);
HydrologyPoint receiver = new HydrologyPoint(12, 80, 0);
RiverOutlet outlet = new RiverOutlet(4L, HydrologyFeatureType.INLAND_GROTTO, 3L, receiver, receiver, 63, false);
return new HydrologyRegionalNetwork(List.of(node), List.of(), List.of(outlet), List.of(course), List.of(), plans);
}
private static CaveVoxelView solid(CavePosition exposed) {
return new CaveVoxelView() {
@Override
public boolean isInWorld(CavePosition position) {
return position.y() >= 0 && position.y() < 128;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
return CaveVoxel.SOLID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
return false;
}
@Override
public boolean isAboveTerrainSurface(CavePosition position) {
return position.equals(exposed);
}
};
}
}
@@ -0,0 +1,124 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class HydrologyRegionalFloodedMouthTest {
@Test
public void aLongDryPrefixEndsBeforeItsConnectedFloodedLandTail() throws Exception {
HydrologyTerrainSampler terrain = coast(2201, 2206);
HydrologyRegionalRoute.Refinement result = validate(terrain,
List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(8192, 65, 0)));
assertNull(result.toString(), result.rejection());
assertEquals(new HydrologyPoint(2200, 63, 0), result.oceanEntry().landward());
assertEquals(new HydrologyPoint(2206, 63, 0), result.oceanEntry().receiving());
assertEquals(new HydrologyPoint(2200, 65, 0), result.points().getLast());
}
@Test
public void floodedTailDoesNotCountTowardTheRegionalMinimumLength() throws Exception {
assertNotNull(validate(coast(2048, 2200), straight()).rejection());
assertNull(validate(coast(2049, 2200), straight()).rejection());
}
@Test
public void aDrySillBeforeTheReceivingOceanRejectsTheTail() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> x == 2204 ? HydrologyTerrainSample.openLand(63, 0D, "sill")
: coast(2201, 2206).sample(x, z);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
validate(terrain, straight()).rejection());
}
@Test
public void unavailableTailTerrainCannotProveTheSeaConnection() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> x == 2204 ? null : coast(2201, 2206).sample(x, z);
assertNotNull(validate(terrain, straight()).rejection());
}
@Test
public void aFloodedTailWithoutKnownSeaCannotBecomeAMouth() throws Exception {
assertEquals(HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED,
validate(coast(2201, 4000), straight()).rejection());
}
@Test
public void aDifferentFluidProfileInterruptsTheReceivingPath() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> x == 2204 ? terrain(false, 60, "lava", null)
: coast(2201, 2206).sample(x, z);
assertEquals(HydrologyCandidateRejection.POLICY_EXCLUDED, validate(terrain, straight()).rejection());
}
@Test
public void aConfinedReceivingTailCannotCrossItsBoundary() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> x >= 2204 ? terrain(x >= 2206, 60, "default", null)
: terrain(false, x >= 2201 ? 60 : 65, "default", "coast");
assertEquals(HydrologyCandidateRejection.CONFINED_NO_OUTLET, validate(terrain, straight()).rejection());
}
@Test
public void diagonalWetSamplesRequireAnActualCardinalWaterConnection() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> x >= 2201 && z == x - 2200
? terrain(x >= 2203, 60, "default", null) : HydrologyTerrainSample.openLand(65, 0D, "land");
assertEquals(HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, validate(terrain,
List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(2200, 65, 0),
new HydrologyPoint(2203, 60, 3))).rejection());
}
@Test
public void aCurvedFloodedTailCannotCertifyADryPersistedMouthChord() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> {
boolean wet = z == 0 && x >= 2201 && x <= 2204 || x == 2204 && z >= 0 && z <= 4
|| z == 4 && x >= 2204;
return wet ? terrain(x >= 2208, 60, "default", null) : HydrologyTerrainSample.openLand(65, 0D, "land");
};
assertEquals(HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, validate(terrain,
List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(2204, 60, 0),
new HydrologyPoint(2204, 60, 4), new HydrologyPoint(2208, 60, 4))).rejection());
}
private static List<HydrologyPoint> straight() {
return List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(3000, 65, 0));
}
private static HydrologyTerrainSampler coast(int firstWet, int firstOcean) {
return (x, z) -> x >= firstOcean ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= firstWet ? 60 : 65, 0D, "land");
}
private static HydrologyRegionalRoute.Refinement validate(HydrologyTerrainSampler terrain,
List<HydrologyPoint> points) throws Exception {
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Routing routing = base.routing();
HydrologyPlannerSettings.Regional regional = routing.regional();
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(),
new HydrologyPlannerSettings.Routing(routing.tileSize(), routing.sampleSpacing(), routing.maximumRouteNodes(),
4096, routing.minimumSurfaceCourseLength(), routing.minimumUndergroundCourseLength(),
routing.valleyPreference(), routing.uphillPenalty(), routing.slopePenalty(), routing.confluenceAttraction(),
routing.lengthPreference(), routing.tributaries(), new HydrologyPlannerSettings.Regional(true,
regional.sampleSpacing(), 2048, regional.maximumTrunks(), regional.maximumCachedBasins(),
regional.maximumCachedStations(), false, 0D, regional.maximumCoastalIncision())),
base.surface(), base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(),
base.surfacePools(), base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
HydrologyRegionalRoute route = new HydrologyRegionalRoute(new HydrologyPlanner(1L, settings, terrain));
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class,
boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
return (HydrologyRegionalRoute.Refinement) validate.invoke(route, points, "default", false, terrain);
}
private static HydrologyTerrainSample terrain(boolean ocean, int height, String profile, String confines) {
return new HydrologyTerrainSample(height, 0D, ocean, false, height - 32, height - 30,
!ocean, true, !ocean, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
Double.NaN, confines, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
}
}
@@ -0,0 +1,201 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import org.junit.Test;
import java.util.ArrayList;
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.assertTrue;
public class HydrologyRegionalGraphTest {
@Test
public void naturallyFloodedLandDoesNotJoinTheDryDrainageTree() {
Fixture fixture = fixture(false, true);
ArrayList<HydrologyGridNode> nodes = new ArrayList<>(fixture.grid().nodes());
HydrologyGridNode original = nodes.get(12);
nodes.set(12, new HydrologyGridNode(original.index(), original.gridX(), original.gridZ(),
original.x(), original.z(), original.id(), HydrologyTerrainSample.openLand(60, 0D, "land")));
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 1280, 5, 256, nodes);
HydrologyRegionalGraph.Tree tree = fixture.graph().route(grid, fixture.outlets(), false);
assertEquals(-1, tree.root(12));
assertEquals(-1, tree.root(14));
}
@Test
public void individuallyLegalRisesCannotAccumulateIntoAnImpossibleDownstreamRidge() {
Fixture fixture = fixture(false, false);
HydrologyRegionalGraph.Tree tree = fixture.graph().route(fixture.grid(), fixture.outlets(), false);
assertEquals(-1, tree.root(14));
assertTrue(tree.root(12) >= 0);
}
@Test
public void aHigherCostLowValleyPreservesTheGloballyFeasibleDrainageRoute() {
Fixture fixture = fixture(true, false);
HydrologyRegionalGraph.Tree tree = fixture.graph().route(fixture.grid(), fixture.outlets(), false);
List<Integer> path = fixture.graph().path(tree, 14);
assertTrue(tree.root(14) >= 0);
assertTrue(path.stream().anyMatch(index -> fixture.grid().node(index).gridZ() == 4));
assertFalse(path.contains(11));
assertEquals(path, fixture.graph().path(fixture.graph().route(fixture.grid(), fixture.outlets(), false), 14));
}
@Test
public void downstreamCliffsRemainLegal() {
Fixture fixture = fixture(false, true);
HydrologyRegionalGraph.Tree tree = fixture.graph().route(fixture.grid(), fixture.outlets(), false);
assertEquals(List.of(14, 13, 12, 11, 10), fixture.graph().path(tree, 14));
}
@Test
public void minimumHeadUsesDepthAndTheStricterPolicyIncisionMultiplier() {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(settings);
HydrologyTerrainSample terrain = new HydrologyTerrainSample(80, 0D, false, false, 40, 42,
true, true, true, false, false, false, 0D, 1D, 1D, 1D, 2D, 0.5D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of("default"), List.of(),
Double.NaN, null, Double.NaN, true,
new SurfaceRiverPolicy("limited", null, null, null, null, null, null, 10));
assertEquals(79, hydraulics.minimumHead(terrain));
assertEquals(80, hydraulics.maximumHead(terrain));
}
@Test
public void addingALowerHeadOutletPreservesTheShorterFeasibleUpstreamRoute() {
Fixture fixture = competingOutlets(false);
HydrologyRegionalGraph.Tree both = fixture.graph().route(fixture.grid(), fixture.outlets(), false);
HydrologyRegionalGraph.Tree shortOnly = fixture.graph().route(fixture.grid(), List.of(fixture.outlets().getLast()), false);
assertEquals(List.of(9, 14, 19, 24), fixture.graph().path(shortOnly, 9));
assertEquals(fixture.graph().path(shortOnly, 9), fixture.graph().path(both, 9));
assertEquals(768D, both.length(9), 0D);
assertEquals(1104D, both.cost(9), 0D);
assertEquals(2L, both.outlets().get(both.root(9)).outlet().id());
assertEquals(List.of(14, 13, 12, 11, 10), fixture.graph().path(both, 14));
assertEquals(1024D, both.length(14), 0D);
}
@Test
public void aCheaperLongerRouteCannotHideAnEqualHeadShorterRoute() {
Fixture fixture = competingOutlets(true);
HydrologyRegionalGraph.Tree tree = fixture.graph().route(fixture.grid(), fixture.outlets(), false);
assertEquals(List.of(14, 13, 12, 11, 10), fixture.graph().path(tree, 14));
assertEquals(List.of(9, 14, 19, 24), fixture.graph().path(tree, 9));
assertEquals(768D, tree.length(9), 0D);
assertTrue(tree.cost(9) > tree.cost(14));
}
@Test
public void sharedCoordinatesAccumulateEachSourceOnceAlongItsExactSelectedRoute() {
Fixture fixture = competingOutlets(false);
HydrologyRegionalGraph.Tree tree = fixture.graph().route(fixture.grid(), fixture.outlets(), false);
assertArrayEquals(new int[]{1, 1, 2, 3}, tree.contributions(9));
assertArrayEquals(new int[]{1, 2, 3, 4, 5}, tree.contributions(14));
assertEquals(8, tree.contributions(10)[0] + tree.contributions(24)[0]);
}
@Test
public void distinctShorePositionsInOneGridCellKeepTheirOwnRouteLengths() {
Fixture fixture = fixture(false, true);
OutletCandidate close = new OutletCandidate(10, 5, new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 10,
new HydrologyPoint(200, 63, 512), new HydrologyPoint(200, 63, 511), 63, true));
HydrologyRegionalGraph.Tree tree = fixture.graph().route(fixture.grid(), List.of(fixture.outlets().getFirst(), close), false);
assertEquals(List.of(11, 10), fixture.graph().path(tree, 11));
assertEquals(56D, tree.length(11), 0D);
assertEquals(2L, tree.outlets().get(tree.root(11)).outlet().id());
}
private static Fixture competingOutlets(boolean equalHead) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Routing original = base.routing();
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
original.tileSize(), original.sampleSpacing(), original.maximumRouteNodes(), 1024,
original.minimumSurfaceCourseLength(), original.minimumUndergroundCourseLength(),
original.valleyPreference(), original.uphillPenalty(), original.slopePenalty(),
original.confluenceAttraction(), original.lengthPreference(), original.tributaries(), original.regional());
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(true, base.surface().sources(),
4, 8, 2, 4, 16, base.surface().shoreWidth(), base.surface().banks());
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(), routing, surface,
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
HydrologyTerrainSampler sampler = (x, z) -> {
int gridX = Math.floorDiv(x, 256);
int gridZ = Math.floorDiv(z, 256);
if (gridZ == 2 && gridX >= 0 && gridX <= 4) {
return HydrologyTerrainSample.openLand(gridX == 4 ? 86 : 70, 0D, "land");
}
if (gridX == 4 && gridZ == 1) {
return HydrologyTerrainSample.openLand(110, 0D, "land");
}
if (gridX == 4 && gridZ == 3) {
return HydrologyTerrainSample.openLand(equalHead ? 86 : 100, equalHead ? 1000D : 0D, "land");
}
if (gridX == 4 && gridZ == 4) {
return HydrologyTerrainSample.openLand(63, 0D, "land");
}
return HydrologyTerrainSample.ocean(60, "ocean");
};
ArrayList<HydrologyGridNode> nodes = new ArrayList<>();
for (int z = 0; z < 5; z++) {
for (int x = 0; x < 5; x++) {
int index = z * 5 + x;
nodes.add(new HydrologyGridNode(index, x, z, x * 256, z * 256, index, sampler.sample(x * 256, z * 256)));
}
}
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 1280, 5, 256, nodes);
OutletCandidate low = new OutletCandidate(10, 5, new RiverOutlet(1L, HydrologyFeatureType.MOUTH, 10,
new HydrologyPoint(0, 63, 512), new HydrologyPoint(0, 63, 511), 63, true));
OutletCandidate high = new OutletCandidate(24, 23, new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 24,
new HydrologyPoint(1024, 63, 1024), new HydrologyPoint(1023, 63, 1024), 63, true));
return new Fixture(new HydrologyRegionalGraph(new HydrologyPlanner(18L, settings, sampler)), grid, List.of(low, high));
}
private static Fixture fixture(boolean valley, boolean downhill) {
HydrologyTerrainSampler sampler = (x, z) -> {
int gridX = x / 256;
int gridZ = z / 256;
if (gridZ == 2) {
int height = switch (gridX) {
case 0 -> 66;
case 1 -> downhill ? 66 : 100;
case 2 -> downhill ? 80 : 90;
case 3 -> downhill ? 95 : 80;
default -> downhill ? 110 : 70;
};
return HydrologyTerrainSample.openLand(height, 0D, "land");
}
if (valley && (gridZ == 4 || gridZ == 3 && (gridX == 0 || gridX == 4))) {
return HydrologyTerrainSample.openLand(70, 1000D, "valley");
}
return HydrologyTerrainSample.ocean(60, "ocean");
};
ArrayList<HydrologyGridNode> nodes = new ArrayList<>();
for (int z = 0; z < 5; z++) {
for (int x = 0; x < 5; x++) {
int index = z * 5 + x;
nodes.add(new HydrologyGridNode(index, x, z, x * 256, z * 256, index, sampler.sample(x * 256, z * 256)));
}
}
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 1280, 5, 256, nodes);
HydrologyPoint landward = new HydrologyPoint(0, 63, 512);
List<OutletCandidate> outlets = List.of(new OutletCandidate(10, 5,
new RiverOutlet(1L, HydrologyFeatureType.MOUTH, 10, landward, new HydrologyPoint(0, 63, 511), 63, true)));
HydrologyPlanner planner = new HydrologyPlanner(18L, HydrologyRegionalPlannerTest.settings(false, 1), sampler);
return new Fixture(new HydrologyRegionalGraph(planner), grid, outlets);
}
private record Fixture(HydrologyRegionalGraph graph, HydrologySampledGrid grid, List<OutletCandidate> outlets) {
}
}
@@ -0,0 +1,40 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class HydrologyRegionalHydraulicsTest {
@Test
public void inletAllowanceLeavesTheOrdinaryChannelLimitUnchanged() {
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(
HydrologyRegionalPlannerTest.settings(false, 1));
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(100, 0D, "land");
assertEquals(86, hydraulics.minimumHead(terrain));
assertEquals(70, hydraulics.inletMinimumHead(terrain));
assertEquals(86, hydraulics.minimumHead(terrain));
assertEquals(63, hydraulics.inletMinimumHead(HydrologyTerrainSample.openLand(65, 0D, "land")));
}
@Test
public void inletAllowanceRetainsDepthAndIncisionMultipliers() {
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(
HydrologyRegionalPlannerTest.settings(false, 1));
HydrologyTerrainSample terrain = terrain(10, 2D, 0.5D);
assertEquals(119, hydraulics.minimumHead(terrain));
assertEquals(108, hydraulics.inletMinimumHead(terrain));
}
private static HydrologyTerrainSample terrain(int incision, double depthMultiplier, double incisionMultiplier) {
return new HydrologyTerrainSample(120, 0D, false, false, 80, 82,
true, true, true, false, false, false, 0D, 1D, 1D, 1D, depthMultiplier, incisionMultiplier, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of("default"), List.of(),
Double.NaN, null, Double.NaN, true,
new SurfaceRiverPolicy("limited", null, null, null, null, null, null, incision));
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
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 HydrologyRegionalMeanderScaleTest {
@Test
public void longOpenValleysRetainRepeatedBendsAtTheAuthoredScale() {
HydrologyPlannerSettings settings = settings(0.34D, 0.42D);
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(76, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(1337L, settings, terrain);
List<HydrologyPoint> guide = guide(76);
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
HydrologyRegionalRoute.Refinement result = route.select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertValid(result, guide, planner);
int minimumZ = Integer.MAX_VALUE;
int maximumZ = Integer.MIN_VALUE;
int crossings = 0;
int previousSide = 0;
for (HydrologyPoint point : result.points()) {
if (point.x() < 1024 || point.x() > 5120) {
continue;
}
minimumZ = Math.min(minimumZ, point.z());
maximumZ = Math.max(maximumZ, point.z());
int side = point.z() > 780 ? 1 : point.z() < 756 ? -1 : 0;
if (side != 0) {
if (previousSide != 0 && previousSide != side) {
crossings++;
}
previousSide = side;
}
}
assertTrue("minimum Z=" + minimumZ, minimumZ <= 736);
assertTrue("maximum Z=" + maximumZ, maximumZ >= 800);
assertTrue("bend crossings=" + crossings, crossings >= 3);
assertEquals(result, route.select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement());
assertEquals(result, new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement());
}
@Test
public void elevationSelectsADifferentThreeDimensionalBendSlice() {
HydrologyPlannerSettings settings = settings(0.34D, 0.42D);
List<HydrologyPoint> lower = refine(settings, 76);
List<HydrologyPoint> upper = refine(settings, 108);
int changed = 0;
int compared = Math.min(lower.size(), upper.size());
for (int index = 0; index < compared; index++) {
if (lower.get(index).distanceSquared2D(upper.get(index)) > 64L) {
changed++;
}
}
assertTrue("different bend stations=" + changed, changed > compared / 2);
}
@Test
public void zeroBendStrengthKeepsAnOpenStraightGuide() {
List<HydrologyPoint> points = refine(settings(0D, 0D), 76);
assertTrue(points.stream().allMatch(point -> point.z() == 768));
}
private static List<HydrologyPoint> refine(HydrologyPlannerSettings settings, int height) {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(height, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(1337L, settings, terrain);
List<HydrologyPoint> guide = guide(height);
HydrologyRegionalRoute.Refinement result = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertValid(result, guide, planner);
return result.points();
}
private static List<HydrologyPoint> guide(int height) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (int x = 0; x <= 6144; x += 256) {
points.add(new HydrologyPoint(x, height, 768));
}
return List.copyOf(points);
}
private static void assertValid(HydrologyRegionalRoute.Refinement result, List<HydrologyPoint> guide,
HydrologyPlanner planner) {
assertNull(result.toString(), result.rejection());
assertFalse(result.points().isEmpty());
assertEquals(guide.getFirst(), result.points().getFirst());
assertEquals(guide.getLast(), result.points().getLast());
SurfaceCenterline centerline = SurfaceCenterline.densify(result.points());
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int availableHead = Integer.MAX_VALUE;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample terrain = planner.sampler.sample(centerline.x()[station], centerline.z()[station]);
availableHead = Math.min(availableHead, hydraulics.maximumHead(terrain));
assertTrue(hydraulics.minimumHead(terrain) <= availableHead);
}
double length = 0D;
List<HydrologyPoint> points = result.points();
for (int index = 1; index < points.size(); index++) {
length += StrictMath.sqrt(points.get(index - 1).distanceSquared2D(points.get(index)));
if (index + 1 == points.size()) {
continue;
}
HydrologyPoint previous = points.get(Math.max(0, index - 4));
HydrologyPoint point = points.get(index);
HydrologyPoint next = points.get(Math.min(points.size() - 1, index + 4));
double firstX = point.x() - previous.x();
double firstZ = point.z() - previous.z();
double nextX = next.x() - point.x();
double nextZ = next.z() - point.z();
double turn = StrictMath.toDegrees(StrictMath.abs(StrictMath.atan2(firstX * nextZ - firstZ * nextX,
firstX * nextX + firstZ * nextZ)));
assertTrue("turn=" + turn, turn <= planner.settings.geometry().meanders().maximumTurnDegrees());
}
assertTrue(length >= planner.settings.routing().regional().minimumLength());
assertTrue(length <= planner.settings.routing().maximumRouteLength());
}
private static HydrologyPlannerSettings settings(double primaryStrength, double detailStrength) {
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Routing routing = base.routing();
HydrologyPlannerSettings.Geometry geometry = base.geometry();
HydrologyPlannerSettings.Regional regional = new HydrologyPlannerSettings.Regional(true, 256, 2048,
1, 1, 32768, false, 1D, 8);
return new HydrologyPlannerSettings(base.seaLevel(), new HydrologyPlannerSettings.Routing(routing.tileSize(),
routing.sampleSpacing(), routing.maximumRouteNodes(), 8192, routing.minimumSurfaceCourseLength(),
routing.minimumUndergroundCourseLength(), routing.valleyPreference(), routing.uphillPenalty(),
routing.slopePenalty(), routing.confluenceAttraction(), routing.lengthPreference(), routing.tributaries(), regional),
base.surface(), base.hydraulics(), base.underground(), base.outlets(),
new HydrologyPlannerSettings.Geometry(new HydrologyPlannerSettings.Meanders(64, 12,
primaryStrength, detailStrength, 0.48D, 1, 20D), geometry.surface(), geometry.underground(),
geometry.grottos(), geometry.drops()), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
}
}
@@ -0,0 +1,125 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalMorphologyTest {
@Test
public void flatOpenTerrainRetainsFullFreedomWithoutSamplingDuringQueries() {
AtomicInteger calls = new AtomicInteger();
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide(4096, 256), (x, z) -> {
calls.incrementAndGet();
return HydrologyTerrainSample.openLand(76, 0D, "land");
}, settings(8192, 256));
int samples = calls.get();
for (int x = -128; x <= 4224; x++) {
assertEquals(1D, morphology.freedomAt(x, 7), 0D);
}
assertEquals(samples, calls.get());
assertEquals(85, samples);
}
@Test
public void steepGradesAndNarrowValleysReduceFreedomIndependently() {
List<HydrologyPoint> guide = guide(4096, 256);
HydrologyPlannerSettings settings = settings(8192, 256);
HydrologyRegionalMorphology gentle = HydrologyRegionalMorphology.sample(guide,
(x, z) -> HydrologyTerrainSample.openLand(1200 - Math.floorDiv(x, 128), 0D, "land"), settings);
HydrologyRegionalMorphology steep = HydrologyRegionalMorphology.sample(guide,
(x, z) -> HydrologyTerrainSample.openLand(1200 - Math.floorDiv(x, 4), 0D, "land"), settings);
HydrologyRegionalMorphology confined = HydrologyRegionalMorphology.sample(guide,
(x, z) -> HydrologyTerrainSample.openLand(Math.abs(z) < 12 ? 76 : 108, 0D, "land"), settings);
for (int x = 0; x <= 4096; x += 16) {
assertEquals(1D, gentle.freedomAt(x, 0), 0D);
assertEquals(0D, steep.freedomAt(x, 0), 0D);
assertEquals(0D, confined.freedomAt(x, 0), 0D);
}
}
@Test
public void terrainTransitionsInterpolateAcrossGuideAndTileBoundaries() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 1024 && Math.abs(z) >= 12 ? 108 : 76, 0D, "land");
HydrologyPlannerSettings settings = settings(8192, 256);
HydrologyRegionalMorphology coarse = HydrologyRegionalMorphology.sample(guide(4096, 256), terrain, settings);
HydrologyRegionalMorphology subdivided = HydrologyRegionalMorphology.sample(guide(4096, 64), terrain, settings);
assertEquals(1D, coarse.freedomAt(0, 0), 0D);
assertEquals(0D, coarse.freedomAt(2048, 0), 0D);
for (int x = 0; x < 4096; x++) {
assertEquals(coarse.freedomAt(x, 0), subdivided.freedomAt(x, 0), 0D);
assertTrue(StrictMath.abs(coarse.freedomAt(x + 1, 0) - coarse.freedomAt(x, 0)) < 0.01D);
}
assertTrue(StrictMath.abs(coarse.freedomAt(1024D - 1e-4D, 0) - coarse.freedomAt(1024D + 1e-4D, 0)) < 1e-8D);
}
@Test
public void longGuidesHaveAtMost645DistinctTerrainSamples() {
AtomicInteger calls = new AtomicInteger();
Set<Long> locations = new HashSet<>();
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide(32768, 4), (x, z) -> {
calls.incrementAndGet();
assertTrue(locations.add(RiverFootprint.pack(x, z)));
assertTrue(Math.abs(z) <= 64);
return HydrologyTerrainSample.openLand(76, 0D, "land");
}, settings(32768, 128));
assertEquals(645, calls.get());
assertEquals(1D, morphology.freedomAt(16000, 0), 0D);
}
@Test
public void missingOrFloodedBanksReduceFreedomWithoutExpandingSampling() {
for (boolean missing : List.of(false, true)) {
AtomicInteger calls = new AtomicInteger();
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide(4096, 256), (x, z) -> {
calls.incrementAndGet();
return z == 0 ? HydrologyTerrainSample.openLand(76, 0D, "land")
: missing ? null : HydrologyTerrainSample.ocean(60, "ocean");
}, settings(8192, 256));
assertEquals(0D, morphology.freedomAt(2048, 0), 0D);
assertTrue(calls.get() <= 85);
}
}
@Test
public void degenerateAndOverlengthGuidesDoNotSampleTerrain() {
HydrologyTerrainSampler terrain = (x, z) -> {
throw new AssertionError("Invalid guides must not sample terrain");
};
for (List<HydrologyPoint> guide : List.of(List.<HydrologyPoint>of(), List.of(new HydrologyPoint(0, 76, 0)), guide(16384, 256))) {
assertEquals(0D, HydrologyRegionalMorphology.sample(guide, terrain, settings(8192, 256)).freedomAt(0, 0), 0D);
}
}
static HydrologyPlannerSettings settings(int maximumLength, int spacing) {
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Routing routing = base.routing();
HydrologyPlannerSettings.Geometry geometry = base.geometry();
HydrologyPlannerSettings.Regional regional = new HydrologyPlannerSettings.Regional(true, spacing, 2048,
1, 1, 32768, false, 1D, 8);
return new HydrologyPlannerSettings(base.seaLevel(), new HydrologyPlannerSettings.Routing(routing.tileSize(),
routing.sampleSpacing(), routing.maximumRouteNodes(), maximumLength, routing.minimumSurfaceCourseLength(),
routing.minimumUndergroundCourseLength(), routing.valleyPreference(), routing.uphillPenalty(),
routing.slopePenalty(), routing.confluenceAttraction(), routing.lengthPreference(), routing.tributaries(), regional),
base.surface(), base.hydraulics(), base.underground(), base.outlets(),
new HydrologyPlannerSettings.Geometry(new HydrologyPlannerSettings.Meanders(64, 12,
0.34D, 0.42D, 0.48D, 1, 20D), geometry.surface(), geometry.underground(),
geometry.grottos(), geometry.drops()), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
}
private static List<HydrologyPoint> guide(int length, int spacing) {
ArrayList<HydrologyPoint> guide = new ArrayList<>();
for (int x = 0; x <= length; x += spacing) {
guide.add(new HydrologyPoint(x, 76, 0));
}
return guide;
}
}
@@ -0,0 +1,234 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
import org.junit.Test;
import java.util.List;
import java.util.ArrayList;
import java.lang.reflect.Method;
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;
public class HydrologyRegionalMouthRefinementTest {
@Test
public void certifiedFloodedLandRetainsTheInletBudgetAtTheActualDryShore() throws Exception {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyTerrainSampler terrain = (x, z) -> x >= 880 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 800 ? 60 : x >= 768 ? 80 : 65, 0D, "land");
HydrologyPoint landward = new HydrologyPoint(799, 63, 0);
RiverOutlet outlet = new RiverOutlet(1L, HydrologyFeatureType.MOUTH, 1L, landward,
new HydrologyPoint(880, 63, 0), 63, true);
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forOutlet(settings, terrain, outlet);
HydrologyPlanner planner = new HydrologyPlanner(1L, settings, terrain);
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
new HydrologyPoint(768, 80, 0), landward);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class,
boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
HydrologyRegionalRoute.Refinement accepted = (HydrologyRegionalRoute.Refinement) validate.invoke(
route, path, "default", false, receiver);
HydrologyRegionalRoute.Refinement unproved = (HydrologyRegionalRoute.Refinement) validate.invoke(
route, path, "default", false, terrain);
assertNull(accepted.toString(), accepted.rejection());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, unproved.rejection());
assertTrue(new HydrologyRegionalTerrainRefiner(planner).receivingTerminal(landward, "default", false, receiver));
assertFalse(terrain.sample(800, 0).ocean());
assertTrue(receiver.receivingWater(800, 0, 63));
}
@Test
public void downstreamOceanEntryReplacesTheLaterCoastalSample() {
HydrologyTerrainSample ocean = terrain(true, 60, "default", null);
HydrologyRegionalRoute.Refinement result = refine(new Scenario(768, 1024, 512), ocean, null, false);
assertNull(result.toString(), result.rejection());
assertNotNull(result.oceanEntry());
assertEquals(new HydrologyPoint(767, 63, 0), result.oceanEntry().landward());
assertEquals(new HydrologyPoint(768, 63, 0), result.oceanEntry().receiving());
assertEquals(767, result.points().getLast().x());
assertTrue(result.points().stream().allMatch(point -> point.x() < 768));
assertEquals(result, refine(new Scenario(768, 1024, 512), ocean, null, false));
}
@Test
public void firstReceivingSeaCanBeFarFromTheOriginalCoarseOutlet() {
HydrologyTerrainSample ocean = terrain(true, 60, "default", null);
HydrologyRegionalRoute.Refinement accepted = refine(new Scenario(2560, 4096, 2048), ocean, null, false);
HydrologyRegionalRoute.Refinement tooEarly = refine(new Scenario(1792, 4096, 2048), ocean, null, false);
HydrologyRegionalRoute.Refinement oneBlockShort = refine(new Scenario(2048, 4096, 2048), ocean, null, false);
assertNull(accepted.toString(), accepted.rejection());
assertEquals(new HydrologyPoint(2560, 63, 0), accepted.oceanEntry().receiving());
assertTrue(4096 - accepted.oceanEntry().receiving().x() > 512);
assertNotNull(tooEarly.rejection());
assertNull(tooEarly.oceanEntry());
assertNotNull(oneBlockShort.rejection());
assertNull(oneBlockShort.oceanEntry());
}
@Test
public void anEarlierOceanBarrierCannotTruncateARegionalCourse() {
HydrologyRegionalRoute.Refinement result = refine(new Scenario(400, 1024, 512), terrain(true, 60, "default", null), null, false);
assertNotNull(result.rejection());
assertNull(result.oceanEntry());
}
@Test
public void dryOceanColumnsCannotServeAsReceivingWater() {
for (int height : new int[]{63, 64}) {
HydrologyRegionalRoute.Refinement result = refine(new Scenario(768, 1024, 512), terrain(true, height, "default", null), null, false);
assertNotNull(result.rejection());
assertNull(result.oceanEntry());
}
}
@Test
public void receivingWaterMustMatchTheFluidAndConfinedArea() {
HydrologyRegionalRoute.Refinement mismatch = refine(new Scenario(768, 1024, 512), terrain(true, 60, "lava", null), null, false);
HydrologyRegionalRoute.Refinement confined = refine(new Scenario(768, 1024, 512), terrain(true, 60, "default", "ocean"), "land", false);
assertNotNull(mismatch.rejection());
assertNull(mismatch.oceanEntry());
assertNotNull(confined.rejection());
assertNull(confined.oceanEntry());
}
@Test
public void coastalEntryMustAlsoPermitDrainageBackIntoTheChannel() {
HydrologyTerrainSample ocean = terrain(true, 60, "default", "ocean");
assertNull(refine(new Scenario(768, 1024, 512), ocean, null, false).rejection());
HydrologyRegionalRoute.Refinement coastal = refine(new Scenario(768, 1024, 512), ocean, null, true);
assertNotNull(coastal.rejection());
assertNull(coastal.oceanEntry());
}
@Test
public void bendsAfterTheActualMouthDoNotRejectTheExposedPrefix() throws Exception {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlanner planner = new HydrologyPlanner(15L, settings,
(x, z) -> x >= 768 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(65, 0D, "land"));
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (int x = 0; x <= 800; x += 4) {
points.add(new HydrologyPoint(x, 65, 0));
}
for (int z = 4; z <= 128; z += 4) {
points.add(new HydrologyPoint(800, 65, z));
}
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
HydrologyRegionalRoute.Refinement result = (HydrologyRegionalRoute.Refinement) validate.invoke(route, points, "default", false, planner.sampler);
assertNull(result.toString(), result.rejection());
assertEquals(new HydrologyPoint(768, 63, 0), result.oceanEntry().receiving());
Method round = HydrologyRegionalRoute.class.getDeclaredMethod("roundBends", List.class, double.class);
Method resample = HydrologyRegionalRoute.class.getDeclaredMethod("resample", List.class);
Method rounded = HydrologyRegionalRoute.class.getDeclaredMethod("validateRounded", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
round.setAccessible(true);
resample.setAccessible(true);
rounded.setAccessible(true);
HydrologyRegionalRoute.Refinement continuous = (HydrologyRegionalRoute.Refinement) rounded.invoke(route,
resample.invoke(route, round.invoke(route, points, 8D)), "default", false, planner.sampler);
assertNull(continuous.toString(), continuous.rejection());
assertEquals(new HydrologyPoint(768, 63, 0), continuous.oceanEntry().receiving());
}
@Test
public void anUnusedSegmentTailCannotExceedTheAcceptedMouthLengthBudget() throws Exception {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlanner planner = new HydrologyPlanner(15L, settings,
(x, z) -> x >= 1536 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(65, 0D, "land"));
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
HydrologyRegionalRoute.Refinement result = (HydrologyRegionalRoute.Refinement) validate.invoke(route,
List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(4096, 65, 0)), "default", false, planner.sampler);
assertNull(result.toString(), result.rejection());
assertEquals(new HydrologyPoint(1536, 63, 0), result.oceanEntry().receiving());
HydrologyPlanner inland = new HydrologyPlanner(15L, settings,
(x, z) -> HydrologyTerrainSample.openLand(65, 0D, "land"));
HydrologyRegionalRoute.Refinement overlong = (HydrologyRegionalRoute.Refinement) validate.invoke(new HydrologyRegionalRoute(inland),
List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(4096, 65, 0)), "default", false, inland.sampler);
assertEquals(HydrologyCandidateRejection.ROUTE_LIMIT, overlong.rejection());
}
@Test
public void theVerifiedMouthRetainsItsAuthoredInletIncisionBudget() throws Exception {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 768 ? 80 : 65, 0D, "land");
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
new HydrologyPoint(768, 80, 0), new HydrologyPoint(799, 80, 0), new HydrologyPoint(800, 63, 0));
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 6;
case SURFACE_DEPTH -> 2;
default -> request.minimum();
};
SurfaceCourseResult course = new SurfaceCourseBuilder(settings.surface(), terrain, geometry, 63).build(
1L, 2L, "default", path, SurfaceTerminal.OCEAN_MOUTH, 63, 512);
HydrologyRegionalRoute route = new HydrologyRegionalRoute(new HydrologyPlanner(1L, settings, terrain));
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
HydrologyRegionalRoute.Refinement result = (HydrologyRegionalRoute.Refinement) validate.invoke(route, path, "default", false, terrain);
assertNull(course.rejection());
assertNull(result.toString(), result.rejection());
assertEquals(new HydrologyPoint(800, 63, 0), result.oceanEntry().receiving());
HydrologyTerrainSampler inlandRidge = (x, z) -> x >= 800 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 600 && x <= 631 ? 80 : 65, 0D, "land");
HydrologyRegionalRoute.Refinement rejected = (HydrologyRegionalRoute.Refinement) validate.invoke(
new HydrologyRegionalRoute(new HydrologyPlanner(1L, settings, inlandRidge)), path, "default", false, inlandRidge);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, rejected.rejection());
}
private static HydrologyRegionalRoute.Refinement refine(Scenario scenario, HydrologyTerrainSample ocean,
String confines, boolean coastal) {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
HydrologyPlannerSettings.Routing routing = settings.routing();
HydrologyPlannerSettings.Regional regional = routing.regional();
HydrologyPlannerSettings.Routing route = new HydrologyPlannerSettings.Routing(routing.tileSize(), routing.sampleSpacing(),
routing.maximumRouteNodes(), Math.max(routing.maximumRouteLength(), scenario.destination()),
routing.minimumSurfaceCourseLength(), routing.minimumUndergroundCourseLength(), routing.valleyPreference(),
routing.uphillPenalty(), routing.slopePenalty(), routing.confluenceAttraction(), routing.lengthPreference(),
routing.tributaries(), new HydrologyPlannerSettings.Regional(regional.enabled(), regional.sampleSpacing(),
scenario.minimumLength(), regional.maximumTrunks(), regional.maximumCachedBasins(), regional.maximumCachedStations(),
regional.coastalChannels(), regional.coastalChannelChance(), regional.maximumCoastalIncision()));
HydrologyPlannerSettings precise = new HydrologyPlannerSettings(settings.seaLevel(), route,
settings.surface(), settings.hydraulics(), settings.underground(), settings.outlets(),
new HydrologyPlannerSettings.Geometry(new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 20D),
geometry.surface(), geometry.underground(), geometry.grottos(), geometry.drops()),
settings.deepFluids(), settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(),
settings.surfacePolicyBounds());
HydrologyTerrainSample land = terrain(false, 65, "default", confines);
HydrologyPlanner planner = new HydrologyPlanner(15L, precise,
(x, z) -> x >= scenario.oceanStart() && x <= scenario.oceanStart() + 128 ? ocean : land);
return new HydrologyRegionalRoute(planner).select(
List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(scenario.destination(), 65, 0)), "default", coastal, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
}
private record Scenario(int oceanStart, int destination, int minimumLength) {
}
private static HydrologyTerrainSample terrain(boolean ocean, int height, String profile, String confines) {
return new HydrologyTerrainSample(height, 0D, ocean, false, height - 32, height - 30,
!ocean, true, !ocean, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
Double.NaN, confines, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
}
}
@@ -0,0 +1,176 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalPlannerTest {
@Test(timeout = 120000)
public void oneRegionalCourseCrossesSeveralGenerationTilesWithStableHydraulics() {
HydrologyPlanner planner = new HydrologyPlanner(71L, settings(false, 8), coast());
HydrologyRegionalNetwork network = planner.regional.draft(new HydrologyTileKey(0, 0));
assertFalse("The regional coast must accept a complete trunk", network.courses().isEmpty());
RiverCourse course = network.courses().getFirst();
assertGraphEndpoints(network, course);
Set<HydrologyTileKey> keys = crossedTiles(course, 512);
assertTrue("A regional trunk must continue across generation tiles", keys.size() >= 3);
assertTrue(course.hydraulicallyNonRising());
for (HydrologyTileKey key : keys) {
RiverFootprint footprint = planner.regional.materialize(network, planner.regional.tileBounds(key));
for (HydrologyColumnSample column : footprint.columns().values()) {
assertTrue(planner.regional.tileBounds(key).contains(column.x(), column.z()));
assertTrue(column.layers().stream().allMatch(layer -> layer.feature().courseId() == course.id()));
}
}
SurfaceBounds firstBounds = new SurfaceBounds(400, -128, 650, 2176);
SurfaceBounds secondBounds = new SurfaceBounds(500, -128, 750, 2176);
RiverFootprint first = planner.regional.materialize(network, firstBounds);
RiverFootprint second = planner.regional.materialize(network, secondBounds);
int overlap = 0;
for (HydrologyColumnSample column : first.columns().values()) {
if (secondBounds.contains(column.x(), column.z())) {
assertEquals(column, second.sample(column.x(), column.z()).orElseThrow());
overlap++;
}
}
assertTrue("Tile windows must share identical accepted bank and water columns", overlap > 0);
}
@Test(timeout = 120000)
public void coastalChannelHasTwoReceivingSeasAndOneWaterLevel() {
HydrologyTerrainSampler terrain = (x, z) -> x < 128 || x >= 1664
? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(65, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(19L, settings(true, 8), terrain);
HydrologyRegionalNetwork network = planner.regional.draft(new HydrologyTileKey(0, 0));
assertFalse("The lowland strip must accept a coastal channel", network.courses().isEmpty());
RiverCourse course = network.courses().getFirst();
assertGraphEndpoints(network, course);
assertEquals(HydrologyFeatureType.MOUTH, course.segments().getFirst().type());
assertEquals(HydrologyFeatureType.MOUTH, course.segments().getLast().type());
assertEquals(2, network.outlets().size());
for (RiverOutlet outlet : network.outlets()) {
HydrologyTerrainSample ocean = terrain.sample(outlet.connectionPoint().x(), outlet.connectionPoint().z());
assertTrue(ocean.ocean());
assertTrue(ocean.naturalHeight() < 63);
}
for (HydraulicSegment segment : course.segments()) {
assertEquals(63, segment.upstreamHeadY());
assertEquals(63, segment.downstreamHeadY());
}
assertTrue(crossedTiles(course, 512).size() >= 3);
}
@Test(timeout = 120000)
public void rejectedBasinKeepsBoundedDeterministicDiagnostics() {
HydrologyPlanner planner = new HydrologyPlanner(71L, settings(false, 8),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
HydrologyTileKey key = new HydrologyTileKey(0, 0);
HydrologyRegionalNetwork first = planner.regional.draft(key);
assertTrue(first.courses().isEmpty());
assertFalse(first.diagnostics().isEmpty());
assertTrue(first.diagnostics().size() <= 64);
assertEquals(HydrologyCandidateKind.REGIONAL_SOURCE, first.diagnostics().getFirst().kind());
assertEquals(HydrologyCandidateRejection.NO_LEGAL_OUTLET, first.diagnostics().getFirst().rejection());
planner.regional.clear();
assertEquals(first.diagnostics(), planner.regional.draft(key).diagnostics());
HydrologyRegionalNetwork window = planner.regional.coursesIn(new SurfaceBounds(0, 0, 511, 511));
assertEquals(first.diagnostics(), window.diagnostics());
}
@Test(timeout = 120000)
public void regionalCacheEvictionAndConcurrentRequestsKeepTheSameCourse() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(71L, settings(false, 1), coast());
HydrologyTileKey key = new HydrologyTileKey(0, 0);
HydrologyRegionalNetwork first = planner.regional.draft(key);
assertFalse(first.courses().isEmpty());
planner.regional.draft(new HydrologyTileKey(0, 1));
assertEquals(first, planner.regional.draft(key));
planner.regional.clear();
ExecutorService workers = Executors.newFixedThreadPool(3);
try {
ArrayList<CompletableFuture<HydrologyRegionalNetwork>> futures = new ArrayList<>();
for (int index = 0; index < 3; index++) {
futures.add(CompletableFuture.supplyAsync(() -> planner.regional.draft(key), workers));
}
for (CompletableFuture<HydrologyRegionalNetwork> future : futures) {
assertEquals(first, future.get(90, TimeUnit.SECONDS));
}
} finally {
workers.shutdownNow();
}
}
@Test(timeout = 120000)
public void aHighIsthmusCannotBecomeACoastalChannel() {
HydrologyTerrainSampler terrain = (x, z) -> x < 128 || x >= 1664
? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 768 && x <= 1024 ? 90 : 65, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(19L, settings(true, 8), terrain);
HydrologyRegionalNetwork network = planner.regional.draft(new HydrologyTileKey(0, 0));
for (RiverCourse course : network.courses()) {
assertFalse("The coastal incision budget must exclude the ridge",
course.segments().getFirst().type() == HydrologyFeatureType.MOUTH);
}
}
private static void assertGraphEndpoints(HydrologyRegionalNetwork network, RiverCourse course) {
DrainageNode source = network.nodes().stream()
.filter(node -> node.id() == course.sourceNodeId().orElseThrow()).findFirst().orElseThrow();
HydrologyPoint start = course.segments().getFirst().type() == HydrologyFeatureType.MOUTH
? course.segments().getFirst().end() : course.segments().getFirst().start();
assertEquals(start.x(), source.x());
assertEquals(start.z(), source.z());
for (RiverOutlet outlet : network.outlets()) {
DrainageNode terminal = network.nodes().stream()
.filter(node -> node.id() == outlet.drainageNodeId()).findFirst().orElseThrow();
assertEquals(outlet.landwardPoint().x(), terminal.x());
assertEquals(outlet.landwardPoint().z(), terminal.z());
}
}
private static Set<HydrologyTileKey> crossedTiles(RiverCourse course, int tileSize) {
Set<HydrologyTileKey> keys = new HashSet<>();
for (HydraulicSegment segment : course.segments()) {
for (HydrologyPoint point : segment.centerline()) {
keys.add(HydrologyTileKey.fromBlock(point.x(), point.z(), tileSize));
}
}
return keys;
}
private static HydrologyTerrainSampler coast() {
return (x, z) -> x < 128 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(66 + Math.max(0, x - 128) / 256, 0D, "land");
}
static HydrologyPlannerSettings settings(boolean coastalChannels, int cachedBasins) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Source source = new HydrologyPlannerSettings.Source(true, 0D, 64, 0, 0, 128);
HydrologyPlannerSettings.Source undergroundSource = new HydrologyPlannerSettings.Source(false, 0D, 0, 0, 0, 128);
HydrologyPlannerSettings.Regional regional = new HydrologyPlannerSettings.Regional(true, 128, 512, 1,
cachedBasins, 32768, coastalChannels, 1D, 8);
return new HydrologyPlannerSettings(63,
new HydrologyPlannerSettings.Routing(512, 64, 1024, 2048, 128, 0,
1.5D, 24D, 2D, 0.2D, 1D, 0, regional),
new HydrologyPlannerSettings.Surface(true, source, 4, 8, 2, 3, 16, 1.5D, base.surface().banks()),
base.hydraulics(),
HydrologyPlannerSettings.Underground.of(false, undergroundSource, 0, 50, 3, 8, 1, 3, 6, 14, false, 0),
HydrologyPlannerSettings.Outlets.of(true, new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096), false, 12, 48, 8, 1, 2),
base.geometry(), List.of(), List.of(), 0D, HydrologyPlannerSettings.SeaCaves.disabled(),
HydrologyPlannerSettings.SurfacePolicyBounds.NONE);
}
}
@@ -0,0 +1,169 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalSkippedWaypointTest {
@Test
public void aSkippableHillDoesNotBlockAnEarlierValleyAcceptedByTheFullPlanner() throws Exception {
Set<Integer> hills = Set.of(4);
HydrologyTerrainSampler terrain = terrain(hills, true);
HydrologyPlanner planner = planner(terrain);
List<HydrologyPoint> guide = guide(terrain, hills);
assertCoarseFeasible(planner, terrain, guide);
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
assertFalse(refined.isEmpty());
assertFalse(refined.contains(guide.get(4)));
Object context = context(guide, terrain);
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> attempt(planner, context, candidate));
assertTrue(selected.toString(), selected.accepted());
assertEquals(1, selected.network().courses().size());
assertTrue(selected.network().courses().getFirst().hydraulicallyNonRising());
assertEquals(16, planner.settings.surface().maximumIncision());
}
@Test
public void theTerminalHeadRemainsMandatory() {
HydrologyTerrainSampler highTerminal = terrain(Set.of(8), false);
HydrologyPlanner planner = planner(highTerminal);
List<HydrologyPoint> guide = guide(highTerminal, Set.of(8));
assertCoarseFeasible(planner, highTerminal, guide);
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, highTerminal).isEmpty());
HydrologyTerrainSampler lowTerminal = terrain(Set.of(), false);
assertFalse(new HydrologyRegionalTerrainRefiner(planner(lowTerminal))
.refine(guide(lowTerminal, Set.of()), "default", false, lowTerminal).isEmpty());
}
@Test
public void threeConsecutiveHighWaypointsCannotBeSkipped() {
Set<Integer> hills = Set.of(4, 5, 6);
HydrologyTerrainSampler terrain = terrain(hills, true);
HydrologyPlanner planner = planner(terrain);
List<HydrologyPoint> guide = guide(terrain, hills);
assertCoarseFeasible(planner, terrain, guide);
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain).isEmpty());
Set<Integer> twoHills = Set.of(4, 5);
HydrologyTerrainSampler skippable = terrain(twoHills, true);
assertFalse(new HydrologyRegionalTerrainRefiner(planner(skippable))
.refine(guide(skippable, twoHills), "default", false, skippable).isEmpty());
}
@Test
public void aSampledRidgeCannotRaiseTheHeadAfterAnEarlierValley() {
HydrologyTerrainSampler base = terrain(Set.of(), true);
HydrologyTerrainSampler ridge = (x, z) -> x >= 200 && x <= 224
? HydrologyTerrainSample.openLand(95, 0D, "land") : base.sample(x, z);
HydrologyPlanner planner = planner(ridge);
List<HydrologyPoint> guide = guide(ridge, Set.of());
assertCoarseFeasible(planner, ridge, guide);
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, ridge).isEmpty());
AtomicInteger admitted = new AtomicInteger();
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, ridge,
candidate -> {
admitted.incrementAndGet();
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
});
assertFalse(selected.accepted());
assertEquals(0, admitted.get());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, selected.refinement().rejection());
}
private static void assertCoarseFeasible(HydrologyPlanner planner, HydrologyTerrainSampler terrain, List<HydrologyPoint> guide) {
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int available = Integer.MAX_VALUE;
for (HydrologyPoint point : guide) {
HydrologyTerrainSample sample = terrain.sample(point.x(), point.z());
available = Math.min(available, hydraulics.maximumHead(sample));
assertTrue(point.toString(), available >= hydraulics.minimumHead(sample));
}
}
private static HydrologyRegionalRoute.Attempt attempt(HydrologyPlanner planner, Object context,
HydrologyRegionalRoute.Refinement candidate) {
try {
Method attempt = HydrologyRegionalPlanner.class.getDeclaredMethod("attempt", context.getClass(), HydrologyRegionalRoute.Refinement.class);
attempt.setAccessible(true);
return (HydrologyRegionalRoute.Attempt) attempt.invoke(planner.regional, context, candidate);
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
}
}
private static Object context(List<HydrologyPoint> guide, HydrologyTerrainSampler terrain) throws Exception {
Class<?> type = Class.forName(HydrologyRegionalPlanner.class.getName() + "$BuildContext");
Constructor<?> constructor = type.getDeclaredConstructors()[0];
constructor.setAccessible(true);
int[] contributions = new int[guide.size()];
Arrays.fill(contributions, 1);
RiverOutlet outlet = new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 8L, new HydrologyPoint(1024, 63, 0),
new HydrologyPoint(1025, 63, 0), 63, true);
return constructor.newInstance(new HydrologyGridNode(0, 0, 0, 0, 0, 100L, terrain.sample(0, 0)),
null, 123L, outlet, "default", new HydrologyRegionalFlow(guide, contributions),
HydrologyRegionalMorphology.sample(guide, terrain, HydrologyRegionalPlannerTest.settings(false, 1)));
}
private static List<HydrologyPoint> guide(HydrologyTerrainSampler terrain, Set<Integer> hills) {
ArrayList<HydrologyPoint> guide = new ArrayList<>();
for (int index = 0; index < 9; index++) {
int x = index * 128;
int z = index != 8 && hills.contains(index) ? 128 : 0;
guide.add(new HydrologyPoint(x, terrain.sample(x, z).naturalHeight(), z));
}
return List.copyOf(guide);
}
private static HydrologyTerrainSampler terrain(Set<Integer> hills, boolean coast) {
return (x, z) -> {
if (coast && x >= 1025) {
return HydrologyTerrainSample.ocean(60, "ocean");
}
int height = coast ? 80 - Math.max(0, x - 752) / 16 : 80;
if (x >= 32 && x <= 96) {
height = 75;
}
for (int hill : hills) {
if (Math.abs(x - hill * 128) <= 24 && Math.abs(z - (hill == 8 ? 0 : 128)) <= 24) {
height = 93;
}
}
return HydrologyTerrainSample.openLand(height, 0D, "land");
};
}
private static HydrologyPlanner planner(HydrologyTerrainSampler terrain) {
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Surface surface = base.surface();
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(), base.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
surface.minimumDepth(), surface.maximumDepth(), 16, surface.shoreWidth(), surface.banks()),
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 8;
case SURFACE_DEPTH -> 2;
default -> request.minimum();
};
return new HydrologyPlanner(71L, settings, terrain, geometry, -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, 63, -4096, 4096));
}
}
@@ -0,0 +1,107 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalSourceTrialsTest {
@Test
public void longClusterCannotConsumeEveryOutletTrial() {
ArrayList<Source> sources = new ArrayList<>();
for (int index = 0; index < 40; index++) {
sources.add(new Source(0, 10000D - index, index));
}
sources.add(new Source(1, 6000D, 4096));
sources.add(new Source(2, 3000D, 8192));
Fixture fixture = fixture(sources);
HydrologyRegionalSourceTrials trials = fixture.trials();
assertEquals(0, trials.next(Set.of()).orElseThrow());
assertEquals(40, trials.next(Set.of()).orElseThrow());
assertEquals(41, trials.next(Set.of()).orElseThrow());
}
@Test
public void acceptedOutletsDoNotConsumeTheRemainingTrialBudget() {
Fixture fixture = fixture(List.of(new Source(0, 10000D, 0), new Source(0, 9000D, 128),
new Source(1, 6000D, 4096), new Source(1, 3000D, 8192)));
HydrologyRegionalSourceTrials trials = fixture.trials();
assertEquals(0, trials.next(Set.of()).orElseThrow());
assertEquals(2, trials.next(Set.of(100L)).orElseThrow());
assertEquals(3, trials.next(Set.of(100L)).orElseThrow());
assertTrue(trials.next(Set.of(100L)).isEmpty());
}
@Test
public void lengthBandsReachDownstreamAlternativesBeforeNearbyLongestSources() {
Fixture fixture = fixture(List.of(new Source(0, 10000D, 0), new Source(0, 9990D, 1),
new Source(0, 8000D, 128), new Source(0, 6000D, 256), new Source(0, 4000D, 384)));
HydrologyRegionalSourceTrials trials = fixture.trials();
assertEquals(0, trials.next(Set.of()).orElseThrow());
assertEquals(2, trials.next(Set.of()).orElseThrow());
assertEquals(3, trials.next(Set.of()).orElseThrow());
assertEquals(4, trials.next(Set.of()).orElseThrow());
assertEquals(1, trials.next(Set.of()).orElseThrow());
}
@Test
public void laterBandTrialsPreferDifferentBranchesAndIgnoreInputOrder() {
Fixture fixture = fixture(List.of(new Source(0, 10000D, 0), new Source(0, 9999D, 1),
new Source(0, 9998D, 4096), new Source(0, 2000D, -4096)));
ArrayList<Integer> reversed = new ArrayList<>(fixture.sources());
Collections.reverse(reversed);
HydrologyRegionalSourceTrials first = fixture.trials();
HydrologyRegionalSourceTrials second = new HydrologyRegionalSourceTrials(fixture.grid(), fixture.tree(), reversed);
List<Integer> expected = List.of(0, 3, 2, 1);
for (int source : expected) {
assertEquals(source, first.next(Set.of()).orElseThrow());
assertEquals(source, second.next(Set.of()).orElseThrow());
}
assertFalse(first.next(Set.of()).isPresent());
assertFalse(second.next(Set.of()).isPresent());
}
private static Fixture fixture(List<Source> sources) {
ArrayList<HydrologyGridNode> nodes = new ArrayList<>(sources.size());
ArrayList<Integer> indices = new ArrayList<>(sources.size());
HydrologyRegionalGraph.Label[] selected = new HydrologyRegionalGraph.Label[sources.size()];
int maximumRoot = 0;
for (int index = 0; index < sources.size(); index++) {
Source source = sources.get(index);
selected[index] = new HydrologyRegionalGraph.Label(new HydrologyRegionalGraph.LabelState(
index, index, 63, 0D, source.length(), source.root()), null);
maximumRoot = Math.max(maximumRoot, source.root());
nodes.add(new HydrologyGridNode(index, index, 0, source.x(), 0, index,
HydrologyTerrainSample.openLand(70, 0D, "land")));
indices.add(index);
}
ArrayList<OutletCandidate> outlets = new ArrayList<>();
for (int root = 0; root <= maximumRoot; root++) {
HydrologyPoint point = new HydrologyPoint(root * 256, 63, 0);
outlets.add(new OutletCandidate(0, 0,
new RiverOutlet(100L + root, HydrologyFeatureType.MOUTH, root, point, point, 63, true)));
}
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 16384, sources.size(), 1, nodes);
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, new int[sources.size()], outlets);
return new Fixture(grid, tree, indices);
}
private record Source(int root, double length, int x) {
}
private record Fixture(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree, List<Integer> sources) {
private HydrologyRegionalSourceTrials trials() {
return new HydrologyRegionalSourceTrials(grid, tree, sources);
}
}
}
@@ -8,9 +8,11 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.OptionalLong;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalSurfacePolicyTest {
@@ -81,6 +83,31 @@ public class HydrologyRegionalSurfacePolicyTest {
assertEquals(onlyOrdinary.selectedNodeIndices(), select(planner(1337L, planner.settings), grid, routing, true).selectedNodeIndices());
}
@Test
public void rerootedSurfaceSourceRetainsItsAdmissionSlot() {
HydrologySampledGrid grid = grid((x, z) -> land(TROPICAL, false));
HydrologyPlanner planner = planner(1337L, HydrologyPlannerSettings.defaults());
SourceSelection selected = select(planner, grid, routing(grid, false), true);
List<Integer> original = selected.selectedNodeIndices();
ArrayList<DrainageNode> nodes = new ArrayList<>();
ArrayList<RiverCourse> courses = new ArrayList<>();
for (int nodeIndex : original) {
HydrologyGridNode source = grid.node(nodeIndex);
long courseId = HydrologyHash.mix(source.id(), 81L);
long sourceId = HydrologyHash.mix(source.id(), 82L);
nodes.add(new DrainageNode(sourceId, source.x(), source.z(), source.terrain(), 100D, 99L));
courses.add(new RiverCourse(courseId, RiverCourseType.SURFACE, OptionalLong.of(sourceId), OptionalLong.of(99L),
"default", 1, List.of(), List.of(new HydraulicSegment(HydrologyHash.mix(courseId, 1L), courseId,
HydrologyFeatureType.SURFACE_POOL, 80, 80, 4, 2, false, false,
List.of(new HydrologyPoint(source.x(), 80, source.z()), new HydrologyPoint(source.x() + 32, 80, source.z())),
HydraulicChannelProfile.uniform(4, 2)))));
}
assertFalse(original.isEmpty());
assertFalse(selected.advanceAfterPublication(new HydrologyCaveCourseFilter.Result(nodes, List.of(), List.of(),
courses, List.of()), grid));
assertEquals(original, selected.selectedNodeIndices());
}
@Test
public void localTributaryCapsApplyToAdmissionAndReplacement() {
SurfaceRiverPolicy branching = new SurfaceRiverPolicy("region:tropical", 8D, 160, 3, 3, 4, null, null);
@@ -90,7 +117,7 @@ public class HydrologyRegionalSurfacePolicyTest {
assertEquals(4, selected.selectedNodeIndices().size());
Set<Integer> original = new HashSet<>(selected.selectedNodeIndices());
assertTrue(selected.advanceAfterPublication(List.of(), grid));
assertTrue(selected.advanceAfterPublication(new HydrologyCaveCourseFilter.Result(List.of(), List.of(), List.of(), List.of(), List.of()), grid));
assertEquals(4, selected.selectedNodeIndices().size());
assertTrue(selected.selectedNodeIndices().stream().noneMatch(original::contains));
@@ -109,7 +136,7 @@ public class HydrologyRegionalSurfacePolicyTest {
assertEquals(2, selectedIn(selected, grid, requiredArea));
Set<Integer> original = new HashSet<>(selected.selectedNodeIndices());
assertTrue(selected.advanceAfterPublication(List.of(), grid));
assertTrue(selected.advanceAfterPublication(new HydrologyCaveCourseFilter.Result(List.of(), List.of(), List.of(), List.of(), List.of()), grid));
assertEquals(2, selectedIn(selected, grid, requiredArea));
assertTrue(selected.selectedNodeIndices().stream().noneMatch(original::contains));
assertEquals(0, selectedIn(selected, grid, disabledArea));
@@ -0,0 +1,473 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
import org.junit.Test;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.lang.reflect.Method;
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 HydrologyRegionalTerrainRefinerTest {
@Test
public void aHigherHeadDetourSurvivesACheaperInfeasibleArrival() {
int[][] heights = {
{86, 94, 77, 91, 75, 85, 82, 88, 74},
{84, 94, 83, 85, 72, 76, 73, 80, 72},
{73, 85, 78, 77, 73, 82, 84, 81, 79},
{83, 77, 92, 81, 84, 80, 93, 94, 96},
{90, 73, 76, 84, 74, 73, 91, 80, 85},
{91, 96, 90, 75, 91, 84, 94, 96, 95},
{84, 76, 80, 92, 72, 73, 96, 75, 86},
{94, 93, 86, 81, 80, 89, 75, 93, 77},
{85, 95, 85, 73, 88, 82, 81, 94, 92}
};
HydrologyTerrainSampler terrain = (x, z) -> {
int gridX = Math.floorDiv(x, 16);
int gridZ = Math.floorDiv(z, 16) + 4;
return gridX < 0 || gridX >= 9 || gridZ < 0 || gridZ >= 9
? HydrologyTerrainSample.ocean(60, "outside")
: HydrologyTerrainSample.openLand(heights[gridZ][gridX], 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(128, 85, 0));
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
List<HydrologyPoint> path = refiner.refine(guide, "default", false, terrain);
assertFalse(path.isEmpty());
assertEquals(guide.getFirst(), path.getFirst());
assertEquals(guide.getLast(), path.getLast());
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
int available = 90;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
assertFalse(sample.ocean());
int supported = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
centerline.x()[station], centerline.z()[station], centerline.tangentX()[station],
centerline.tangentZ()[station], sample, available), terrain);
assertTrue(supported <= available);
available = supported;
assertTrue("station=" + station, available >= hydraulics.minimumHead(sample));
}
assertEquals(path, refiner.refine(guide, "default", false, terrain));
assertEquals(path, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain));
}
@Test
public void shortRepairedPrefixDoesNotHideALongEnoughGuide() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 406 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 400 ? 60 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(0, 70, 96),
new HydrologyPoint(128, 70, 96), new HydrologyPoint(256, 70, 96), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0));
HydrologyRegionalRoute.Refinement selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
candidate -> {
double length = 0D;
for (int index = 1; index < candidate.points().size(); index++) {
length += StrictMath.sqrt(candidate.points().get(index - 1).distanceSquared2D(candidate.points().get(index)));
}
assertTrue(length >= planner.settings.routing().regional().minimumLength());
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
}).refinement();
assertNull(selected.toString(), selected.rejection());
assertEquals(new HydrologyPoint(399, 63, 0), selected.oceanEntry().landward());
assertEquals(new HydrologyPoint(406, 63, 0), selected.oceanEntry().receiving());
}
@Test
public void onlyTheRetainedDryPrefixNeedsTerrainRepair() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 806 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(x >= 800 ? 60 : x >= 96 && x <= 160 && Math.abs(z) < 32 ? 86 : 70,
0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0), new HydrologyPoint(1024, 70, 0));
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain).isEmpty());
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertEquals(new HydrologyPoint(799, 63, 0), repaired.oceanEntry().landward());
assertEquals(new HydrologyPoint(806, 63, 0), repaired.oceanEntry().receiving());
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
assertTrue(repaired.points().stream().allMatch(point -> point.x() < 800));
}
@Test
public void aCoarseOceanHitBeforeMinimumLengthStillAllowsADryTerrainDetour() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 || x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0),
new HydrologyPoint(512, 70, 0), new HydrologyPoint(799, 70, 0));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
assertEquals(guide.getLast(), repaired.points().getLast());
}
@Test
public void naturallyFloodedLandCannotBeUsedAsAnOwnedDryReach() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 60 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
assertFalse(repaired.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sampled = terrain.sample(centerline.x()[station], centerline.z()[station]);
assertFalse(sampled.ocean());
assertTrue(sampled.naturalHeight() >= planner.settings.seaLevel());
}
}
@Test
public void repeatedCoarseAnchorsCannotCreateRetracedWaterways() {
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(128, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertEquals(List.of(guide.getFirst(), guide.getLast()), refined);
}
@Test
public void redundantCoarseAnchorHairpinsUseTheClearTerrainShortcut() {
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(160, 70, 32), new HydrologyPoint(144, 70, 48), new HydrologyPoint(160, 70, 48),
new HydrologyPoint(192, 70, 16), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertEquals(guide.getFirst(), refined.getFirst());
assertEquals(guide.getLast(), refined.getLast());
assertTrue(refined.size() <= 3);
for (int index = 1; index < refined.size(); index++) {
assertTrue(refined.get(index).x() > refined.get(index - 1).x());
}
}
@Test
public void repairsUnsampledOceanPocketsAtPositiveAndNegativeCoordinates() {
for (int origin : new int[]{0, -512}) {
HydrologyTerrainSampler terrain = (x, z) -> x >= origin + 96 && x <= origin + 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
List<HydrologyPoint> guide = List.of(new HydrologyPoint(origin, 70, 0), new HydrologyPoint(origin + 256, 70, 0));
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
List<HydrologyPoint> first = refiner.refine(guide, "default", false, planner.sampler);
assertFalse(first.isEmpty());
assertEquals(guide.getFirst(), first.getFirst());
assertEquals(guide.getLast(), first.getLast());
SurfaceCenterline centerline = SurfaceCenterline.densify(first);
for (int station = 0; station < centerline.size(); station++) {
assertFalse(terrain.sample(centerline.x()[station], centerline.z()[station]).ocean());
}
assertEquals(first, refiner.refine(guide, "default", false, planner.sampler));
assertEquals(first, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler));
}
}
@Test
public void aWaterCourseDetoursAroundALavaOnlyBiome() {
HydrologyTerrainSampler terrain = (x, z) -> land(x >= 96 && x <= 160 && Math.abs(z) < 32 ? "lava" : "water");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "water", false, planner.sampler);
assertFalse(refined.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(refined);
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
@Test
public void repairedTerrainRouteStillPassesOrganicShapeValidation() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
HydrologyRegionalRoute.Refinement refined = new HydrologyRegionalRoute(planner).select(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(refined.toString(), refined.rejection());
assertTrue(refined.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
}
@Test
public void anUnavoidableOceanBarrierRejectsWithinTheTerrainSampleBudget() {
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
return x >= 96 && x <= 160 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
assertTrue(refined.isEmpty());
assertTrue("terrain samples=" + calls.get(), calls.get() <= 65536);
}
@Test
public void unavailableTerrainIsSampledOnceAndCountsTowardTheBudget() {
Map<Long, Integer> unavailable = new HashMap<>();
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
if (x >= 96 && x <= 160 && Math.abs(z) < 32) {
unavailable.merge(RiverFootprint.pack(x, z), 1, Integer::sum);
return null;
}
return HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertFalse(unavailable.isEmpty());
assertTrue(unavailable.values().stream().allMatch(count -> count == 1));
assertTrue(calls.get() <= 65536);
}
@Test
public void anUnsampledRidgeUsesTheLowerValleyWithoutIncreasingIncision() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 94 : 70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
for (int station = 0; station < centerline.size(); station++) {
assertEquals(70, terrain.sample(centerline.x()[station], centerline.z()[station]).naturalHeight());
}
}
@Test
public void anImpossibleIntermediateAnchorCanBeSkippedWithinTheSameValley() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x < 48 || x >= 120 && x <= 136 && Math.abs(z) < 8 ? 90 : 65, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(128, 90, 0),
new HydrologyPoint(256, 65, 0));
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
List<HydrologyPoint> repaired = refiner.refine(guide, "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertEquals(guide.getFirst(), repaired.getFirst());
assertEquals(guide.getLast(), repaired.getLast());
assertFalse(repaired.contains(guide.get(1)));
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int head = Integer.MAX_VALUE;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
head = Math.min(head, hydraulics.maximumHead(sample));
assertTrue(hydraulics.minimumHead(sample) <= head);
}
assertEquals(repaired, refiner.refine(guide, "default", false, planner.sampler));
}
@Test
public void aLowFineDetourCannotResetTheHeadBeforeTheNextRidge() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 65 : x >= 384 ? 92 : 80, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(256, 80, 0),
new HydrologyPoint(512, 92, 0));
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertTrue(repaired.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
int head = Integer.MAX_VALUE;
for (int station = 0; station < centerline.size(); station++) {
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
head = Math.min(head, hydraulics.maximumHead(sample));
assertTrue(hydraulics.minimumHead(sample) <= head);
}
}
@Test
public void ridgeRepairRespectsTheLocalIncisionPolicy() {
SurfaceRiverPolicy limited = new SurfaceRiverPolicy("valley", null, null, null, null, null, null, 4);
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 76 : 70, 0D, "land").withSurfacePolicy(limited);
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
assertFalse(repaired.isEmpty());
assertTrue(repaired.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
}
@Test
public void detourLengthRejectsBeforeDetailedSamplingOfTheRemainingCourse() {
AtomicInteger maximumX = new AtomicInteger(Integer.MIN_VALUE);
HydrologyTerrainSampler terrain = (x, z) -> {
boolean coarseAnchor = Math.floorMod(x + 1, 256) <= 2 && Math.abs(z) <= 1;
if (!coarseAnchor) {
maximumX.accumulateAndGet(x, Math::max);
}
return x >= 96 && x <= 160 && Math.abs(z) < 32 ? HydrologyTerrainSample.ocean(60, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
ArrayList<HydrologyPoint> guide = new ArrayList<>();
for (int x = 0; x <= planner.settings.routing().maximumRouteLength(); x += 256) {
guide.add(new HydrologyPoint(x, 70, 0));
}
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler).isEmpty());
assertTrue("A rejected first detour must not sample later reaches", maximumX.get() < 512);
}
@Test
public void anAlreadyOverlongGuideDoesNotSampleTerrain() {
AtomicInteger calls = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
calls.incrementAndGet();
return HydrologyTerrainSample.openLand(70, 0D, "land");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
int before = calls.get();
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(4096, 70, 0)), "default", false, planner.sampler).isEmpty());
assertEquals(before, calls.get());
}
@Test
public void clearancePreferenceDoesNotExcludeTheOnlyNarrowCorridor() {
HydrologyTerrainSampler terrain = (x, z) -> {
boolean horizontal = Math.abs(z) <= 3 && (x <= 80 || x >= 176);
boolean detour = Math.abs(z - 16) <= 3 && x >= 61 && x <= 195;
boolean vertical = (Math.abs(x - 64) <= 3 || Math.abs(x - 192) <= 3) && z >= -3 && z <= 19;
return land(horizontal || detour || vertical ? "water" : "lava");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "water", false, planner.sampler);
assertFalse(repaired.isEmpty());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
@Test
public void oceanDetourPreservesTheShippingTwentyDegreeTurnLimit() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 96 && x <= 160 && Math.abs(z) < 32
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 20D)), terrain);
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
}
@Test
public void shortRepairReversalsReceiveEnoughReachToMeetTwentyDegrees() {
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 1, 20D)),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(160, 70, 32), new HydrologyPoint(160, 70, 16),
new HydrologyPoint(192, 70, 16), new HydrologyPoint(192, 70, 128), new HydrologyPoint(512, 70, 128));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
assertEquals(guide.getFirst(), repaired.points().getFirst());
assertEquals(guide.getLast(), repaired.points().getLast());
}
@Test
public void roundedCurveTurnLimitIsMeasuredBeforeBlockQuantization() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 20D)),
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
Method interpolate = HydrologyRegionalRoute.class.getDeclaredMethod("interpolateLinear", List.class);
Method round = HydrologyRegionalRoute.class.getDeclaredMethod("roundBends", List.class, double.class);
Method resample = HydrologyRegionalRoute.class.getDeclaredMethod("resample", List.class);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateRounded", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
interpolate.setAccessible(true);
round.setAccessible(true);
resample.setAccessible(true);
validate.setAccessible(true);
Object linear = interpolate.invoke(route, guide);
Object rounded = round.invoke(route, linear, 128D);
Object curve = resample.invoke(route, rounded);
HydrologyRegionalRoute.Refinement accepted = (HydrologyRegionalRoute.Refinement) validate.invoke(route, curve, "default", false, planner.sampler);
assertNull(accepted.toString(), accepted.rejection());
assertEquals(guide.getFirst(), accepted.points().getFirst());
assertEquals(guide.getLast(), accepted.points().getLast());
Object sharper = resample.invoke(route, round.invoke(route, linear, 64D));
HydrologyRegionalRoute.Refinement rejected = (HydrologyRegionalRoute.Refinement) validate.invoke(route, sharper, "default", false, planner.sampler);
assertEquals(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, rejected.rejection());
}
@Test
public void splineOvershootCannotDiscardAValidNarrowDetour() {
HydrologyTerrainSampler terrain = (x, z) -> {
boolean first = Math.abs(z) <= 8 && x >= -8 && x <= 136;
boolean middle = Math.abs(x - 128) <= 8 && z >= -8 && z <= 136;
boolean last = Math.abs(z - 128) <= 8 && x >= 120 && x <= 264;
return land(first || middle || last ? "water" : "lava");
};
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 150D)), terrain);
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
assertEquals(guide, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "water", false, planner.sampler));
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "water", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
assertNull(repaired.toString(), repaired.rejection());
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired.points());
for (int station = 0; station < centerline.size(); station++) {
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
}
}
private static HydrologyPlannerSettings withMeanders(HydrologyPlannerSettings.Meanders meanders) {
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
return new HydrologyPlannerSettings(settings.seaLevel(), settings.routing(), settings.surface(), settings.hydraulics(),
settings.underground(), settings.outlets(), new HydrologyPlannerSettings.Geometry(meanders,
geometry.surface(), geometry.underground(), geometry.grottos(), geometry.drops()), settings.deepFluids(),
settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(), settings.surfacePolicyBounds());
}
private static HydrologyTerrainSample land(String profile) {
return new HydrologyTerrainSample(70, 0D, false, false, 38, 40,
true, true, true, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
}
}
@@ -0,0 +1,123 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalTributariesTest {
@Test
public void crossingLocalRiverBecomesAConnectedRegionalTributary() {
HydrologyTerrainSampler terrain = terrain(false);
HydrologyPlanner planner = new HydrologyPlanner(19L, HydrologyRegionalPlannerTest.settings(false, 8), terrain);
Fixture fixture = fixture(terrain);
HydrologyFootprintCompiler footprints = new HydrologyFootprintCompiler(planner.settings, terrain);
footprints.regionalNetwork = fixture.regional();
ArrayList<RiverCourse> courses = new ArrayList<>(List.of(fixture.local()));
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
CompiledGraph connected = new HydrologyRegionalTributaries(planner).connect(fixture.graph(), courses, footprints, diagnostics);
assertEquals("diagnostics=" + diagnostics, 1, courses.size());
RiverCourse tributary = courses.getFirst();
assertEquals(fixture.local().id(), tributary.id());
assertNotEquals(fixture.local().sourceNodeId(), tributary.sourceNodeId());
assertEquals(fixture.regional().courses().getFirst().outletId(), tributary.outletId());
assertEquals(256, tributary.segments().getLast().end().x());
assertEquals(63, tributary.segments().getLast().downstreamHeadY());
assertFalse(tributary.segments().getLast().type() == HydrologyFeatureType.MOUTH);
assertTrue(tributary.hydraulicallyNonRising());
assertEquals(12L, tributary.drainageEdges().getLast().downstreamNodeId());
SurfaceFootprint water = footprints.surfaceFootprint(tributary);
assertTrue(water.accepted());
HydrologyPoint join = tributary.segments().getLast().end();
assertTrue(water.columns().stream().anyMatch(column -> column.x() == join.x() && column.z() == join.z()
&& column.layer().fluidOwned() && column.layer().fluidHeadY() == 63 && column.layer().bedY() < 63));
HydrologyCaveCourseFilter.Result combined = planner.regional.include(new HydrologyCaveCourseFilter.Result(
connected.nodes(), connected.edges(), connected.outlets(), courses, List.of()), fixture.regional());
HydrologyTile tile = new HydrologyTile(new HydrologyTileKey(0, 0), 19L, 0L, 512,
combined.nodes(), combined.edges(), combined.outlets(), combined.courses(), List.of(), diagnostics,
RiverFootprint.empty());
assertTrue(tile.acyclic());
DrainageNode source = tile.node(tributary.sourceNodeId().orElseThrow()).orElseThrow();
assertEquals(0, source.x());
assertEquals(512, source.z());
}
@Test
public void localJunctionCannotLeaveItsConfinedRegion() {
HydrologyTerrainSampler terrain = terrain(true);
HydrologyPlanner planner = new HydrologyPlanner(19L, HydrologyRegionalPlannerTest.settings(false, 8), terrain);
Fixture fixture = fixture(terrain);
HydrologyFootprintCompiler footprints = new HydrologyFootprintCompiler(planner.settings, terrain);
footprints.regionalNetwork = fixture.regional();
ArrayList<RiverCourse> courses = new ArrayList<>(List.of(fixture.local()));
new HydrologyRegionalTributaries(planner).connect(fixture.graph(), courses, footprints, new ArrayList<>());
assertTrue(courses.isEmpty());
}
private static Fixture fixture(HydrologyTerrainSampler terrain) {
List<HydrologyPoint> localPath = line(0, 512, 768, 512);
List<HydrologyPoint> regionalPath = line(256, 0, 256, 1024);
DrainageNode localSource = new DrainageNode(1L, 0, 512, terrain.sample(0, 512), 768D, 3L);
DrainageNode localOutlet = new DrainageNode(2L, 768, 512, terrain.sample(768, 512), 0D, 3L);
DrainageEdge localEdge = new DrainageEdge(4L, 1L, 2L, 3L, 768D, 1, 0, localPath);
RiverOutlet outlet = new RiverOutlet(3L, HydrologyFeatureType.MOUTH, 2L,
new HydrologyPoint(768, 65, 512), new HydrologyPoint(769, 63, 512), 63, true);
RiverCourse local = new RiverCourse(5L, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(3L),
"default", 1, List.of(localEdge), List.of(segment(6L, 5L, localPath)));
DrainageNode regionalSource = new DrainageNode(11L, 256, 0, terrain.sample(256, 0), 1024D, 13L);
DrainageNode regionalOutlet = new DrainageNode(12L, 256, 1024, terrain.sample(256, 1024), 0D, 13L);
DrainageEdge regionalEdge = new DrainageEdge(14L, 11L, 12L, 13L, 1024D, 16, 0, regionalPath);
RiverOutlet sea = new RiverOutlet(13L, HydrologyFeatureType.MOUTH, 12L,
new HydrologyPoint(256, 65, 1024), new HydrologyPoint(256, 63, 1025), 63, true);
RiverCourse regional = new RiverCourse(15L, RiverCourseType.SURFACE, OptionalLong.of(11L), OptionalLong.of(13L),
"default", 16, List.of(regionalEdge), List.of(segment(16L, 15L, regionalPath),
new HydraulicSegment(17L, 15L, HydrologyFeatureType.MOUTH, 63, 63, 8, 2, false, false,
List.of(new HydrologyPoint(256, 63, 1024), new HydrologyPoint(256, 63, 1025)),
HydraulicChannelProfile.uniform(8, 2))));
return new Fixture(new CompiledGraph(List.of(localSource, localOutlet), List.of(localEdge), List.of(outlet), Map.of()),
local, new HydrologyRegionalNetwork(List.of(regionalSource, regionalOutlet), List.of(regionalEdge), List.of(sea), List.of(regional), List.of(), List.of()));
}
private static HydraulicSegment segment(long id, long courseId, List<HydrologyPoint> points) {
return new HydraulicSegment(id, courseId, HydrologyFeatureType.SURFACE_POOL, 63, 63, 8, 2,
false, false, points, HydraulicChannelProfile.uniform(8, 2));
}
private static List<HydrologyPoint> line(int fromX, int fromZ, int toX, int toZ) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
int steps = Math.max(Math.abs(toX - fromX), Math.abs(toZ - fromZ)) / 4;
for (int step = 0; step <= steps; step++) {
points.add(new HydrologyPoint(fromX + (toX - fromX) * step / steps, 63,
fromZ + (toZ - fromZ) * step / steps));
}
return points;
}
private static HydrologyTerrainSampler terrain(boolean confined) {
return (x, z) -> {
if (x > 768 || z > 1024) {
return HydrologyTerrainSample.ocean(60, "ocean");
}
HydrologyTerrainSample land = HydrologyTerrainSample.openLand(65, 0D, "land");
if (!confined || x >= 224) {
return land;
}
return new HydrologyTerrainSample(65, 0D, false, false, 33, 35,
true, true, true, false, false, false, 0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"land", "land", "land", "land", "land", "land", List.of("default"), List.of(),
Double.NaN, "region:west", Double.NaN, true, land.surfacePolicy());
};
}
private record Fixture(CompiledGraph graph, RiverCourse local, HydrologyRegionalNetwork regional) {
}
}
@@ -0,0 +1,209 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class HydrologyRegionalWidthAdaptationTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyRegionalPlannerTest.settings(false, 1);
@Test
public void openValleysPreserveAccumulationAndAuthoredWidthVariation() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyGeometrySampler geometry = request -> request.field() == HydrologyGeometrySampler.Field.SURFACE_WIDTH
? 4 + Math.floorMod(request.x() / 256, 5) : request.minimum();
HydrologyPlanner planner = planner(terrain, geometry);
List<HydrologyPoint> guide = guide(terrain);
int[] contributions = {1, 4, 16, 64, 256};
HydrologyRegionalFlow flow = new HydrologyRegionalFlow(guide, contributions);
Object context = context(planner, guide, contributions, false);
int previous = 0;
for (HydrologyPoint point : guide) {
double fraction = 0.25D + 0.75D * flow.fraction(point.x(), point.z());
double accumulated = 4D + 4D * fraction;
int authored = 4 + Math.floorMod(point.x() / 256, 5);
int expected = (int) StrictMath.round(accumulated * 0.75D + authored * 0.25D);
int actual = width(planner, context, point.x());
assertEquals(expected, actual);
assertTrue(actual >= previous);
previous = actual;
}
HydrologyPlanner narrow = planner(terrain, geometry(4));
HydrologyPlanner wide = planner(terrain, geometry(8));
assertTrue(width(wide, context(wide, guide, contributions, false), 512)
> width(narrow, context(narrow, guide, contributions, false), 512));
}
@Test
public void steeperAndConfinedValleysNarrowWithinTheAuthoredBounds() throws Exception {
HydrologyTerrainSampler open = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyTerrainSampler steep = (x, z) -> HydrologyTerrainSample.openLand(80 + Math.floorDiv(x, 4), 0D, "land");
HydrologyTerrainSampler confined = (x, z) -> HydrologyTerrainSample.openLand(Math.abs(z) >= 4 ? 120 : 80, 0D, "land");
for (HydrologyTerrainSampler terrain : List.of(open, steep, confined)) {
HydrologyPlanner planner = planner(terrain, geometry(8));
List<HydrologyPoint> guide = guide(terrain);
Object context = context(planner, guide, uniform(guide), false);
int width = width(planner, context, 512);
assertEquals(terrain == open ? 8 : 4, width);
}
}
@Test
public void theCompleteGuideProfileIsReusedWithoutSamplingAtWidthQueries() throws Exception {
AtomicBoolean sampled = new AtomicBoolean();
HydrologyTerrainSampler terrain = (x, z) -> {
assertTrue("Width lookup must reuse the source profile", !sampled.get());
return HydrologyTerrainSample.openLand(x >= 512 && Math.abs(z) >= 4 ? 120 : 80, 0D, "land");
};
HydrologyPlanner planner = planner(terrain, geometry(8));
List<HydrologyPoint> guide = guide(terrain);
Object context = context(planner, guide, uniform(guide), false);
sampled.set(true);
int previous = width(planner, context, 0);
for (int x = 8; x <= 1024; x += 8) {
int width = width(planner, context, x);
assertTrue(width >= 4 && width <= 8);
assertTrue(Math.abs(width - previous) <= 1);
assertEquals(width, width(planner, context, x));
previous = width;
}
assertEquals(4, previous);
}
@Test
public void narrowingDoesNotChangeDepthSampling() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(Math.abs(z) >= 4 ? 120 : 80, 0D, "land");
HydrologyPlanner planner = planner(terrain, request -> request.maximum());
List<HydrologyPoint> guide = guide(terrain);
Object context = context(planner, guide, uniform(guide), false);
assertEquals(4, width(planner, context, 512));
assertEquals(3, regionalGeometry(planner, context, new HydrologyGeometrySampler.Request(
HydrologyGeometrySampler.Field.SURFACE_DEPTH, "default", 512, 0, 1L, 2, 3)));
}
@Test
public void inlandProfilesStillApplyTheBiomeWidthMultiplier() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> withWidthMultiplier(HydrologyTerrainSample.openLand(80, 0D, "land"), 1.25D);
HydrologyPlanner planner = planner(terrain, geometry(8));
List<HydrologyPoint> guide = guide(terrain);
Object context = context(planner, guide, uniform(guide), false);
HydrologyGeometrySampler geometry = request -> regionalGeometry(planner, context, request);
SurfaceCourseResult built = new SurfaceCourseBuilder(SETTINGS.surface(), terrain, geometry, SETTINGS.seaLevel())
.build(71L, 91L, "default", guide, SurfaceTerminal.SINKHOLE, 40, 512);
assertTrue(built.accepted());
assertEquals(10D, storedWidth(built.segments(), 512), 0D);
}
@Test
public void coastalPublicationKeepsFractionalBiomeWidthsAndCeilsItsEnvelope() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> x < 0 || x > 1024 ? HydrologyTerrainSample.ocean(60, "ocean")
: withWidthMultiplier(HydrologyTerrainSample.openLand(63, 0D, "land"), x < 256 ? 0.1D : x < 768 ? 1.1D : 3D);
HydrologyPlanner planner = planner(terrain, geometry(8));
List<HydrologyPoint> guide = guide(terrain);
Object context = context(planner, guide, uniform(guide), true);
Method attempt = HydrologyRegionalPlanner.class.getDeclaredMethod("attempt", context.getClass(), HydrologyRegionalRoute.Refinement.class);
attempt.setAccessible(true);
HydrologyRegionalRoute.Attempt accepted = (HydrologyRegionalRoute.Attempt) attempt.invoke(planner.regional, context,
new HydrologyRegionalRoute.Refinement(guide, null, null, 0, null));
assertTrue(accepted.toString(), accepted.accepted());
RiverCourse course = accepted.network().courses().getFirst();
assertTrue(course.hydraulicallyNonRising());
assertEquals(4D, storedWidth(course.segments(), 0), 0D);
assertEquals(8.8D, storedWidth(course.segments(), 512), 0.000000001D);
assertEquals(16D, storedWidth(course.segments(), 1024), 0D);
for (HydraulicSegment segment : course.segments()) {
assertEquals((int) StrictMath.ceil(segment.channelProfile().maximumWidth()), segment.width());
assertEquals(2, segment.depth());
}
}
private static double storedWidth(List<HydraulicSegment> segments, int x) {
for (HydraulicSegment segment : segments) {
for (int index = 0; index < segment.centerline().size(); index++) {
if (segment.centerline().get(index).x() == x) {
return segment.channelProfile().widthAt(index);
}
}
}
throw new AssertionError("Missing station " + x);
}
private static int width(HydrologyPlanner planner, Object context, int x) {
return regionalGeometry(planner, context, new HydrologyGeometrySampler.Request(
HydrologyGeometrySampler.Field.SURFACE_WIDTH, "default", x, 0, 1L, 4, 8));
}
private static int regionalGeometry(HydrologyPlanner planner, Object context, HydrologyGeometrySampler.Request request) {
try {
Method geometry = HydrologyRegionalPlanner.class.getDeclaredMethod("regionalGeometry", HydrologyGeometrySampler.Request.class, context.getClass());
geometry.setAccessible(true);
return (int) geometry.invoke(planner.regional, request, context);
} catch (ReflectiveOperationException failure) {
throw new AssertionError(failure);
}
}
private static Object context(HydrologyPlanner planner, List<HydrologyPoint> guide, int[] contributions, boolean coastal) throws Exception {
Class<?> type = Class.forName(HydrologyRegionalPlanner.class.getName() + "$BuildContext");
Constructor<?> constructor = type.getDeclaredConstructors()[0];
constructor.setAccessible(true);
int seaLevel = planner.settings.seaLevel();
HydrologyPoint source = guide.getFirst();
HydrologyPoint destination = guide.getLast();
RiverOutlet outlet = new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 8L, destination,
new HydrologyPoint(destination.x() + 1, seaLevel, destination.z()), seaLevel, true);
OutletCandidate origin = coastal ? new OutletCandidate(0, 0, new RiverOutlet(3L, HydrologyFeatureType.MOUTH, 0L, source,
new HydrologyPoint(source.x() - 1, seaLevel, source.z()), seaLevel, true)) : null;
HydrologyTerrainSampler terrain = planner.sampler;
return constructor.newInstance(new HydrologyGridNode(0, 0, 0, source.x(), source.z(), 100L, terrain.sample(source.x(), source.z())),
origin, 123L, outlet, "default", new HydrologyRegionalFlow(guide, contributions),
HydrologyRegionalMorphology.sample(guide, terrain, planner.settings));
}
private static List<HydrologyPoint> guide(HydrologyTerrainSampler terrain) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (int x = 0; x <= 1024; x += 256) {
points.add(new HydrologyPoint(x, terrain.sample(x, 0).naturalHeight(), 0));
}
return List.copyOf(points);
}
private static int[] uniform(List<HydrologyPoint> guide) {
int[] contributions = new int[guide.size()];
Arrays.fill(contributions, 1);
return contributions;
}
private static HydrologyGeometrySampler geometry(int width) {
return request -> request.field() == HydrologyGeometrySampler.Field.SURFACE_WIDTH ? width : request.minimum();
}
private static HydrologyPlanner planner(HydrologyTerrainSampler terrain, HydrologyGeometrySampler geometry) {
return new HydrologyPlanner(71L, SETTINGS, terrain, geometry, -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, 63, -4096, 4096));
}
private static HydrologyTerrainSample withWidthMultiplier(HydrologyTerrainSample source, double multiplier) {
return new HydrologyTerrainSample(source.naturalHeight(), source.slope(), source.ocean(), source.caveAvailable(),
source.caveFloorY(), source.caveFluidY(), source.transitAllowed(), source.outletAllowed(), source.surfaceSourceAllowed(),
source.surfaceSourceRequired(), source.undergroundSourceAllowed(), source.undergroundSourceRequired(), source.routingCost(),
source.surfaceSourceWeight(), source.undergroundSourceWeight(), multiplier, source.depthMultiplier(), source.incisionMultiplier(),
source.routingMultiplier(), source.bankMultiplier(), source.parentBiomeKey(), source.surfaceBiomeKey(), source.mouthBiomeKey(),
source.shoreBiomeKey(), source.bankBiomeKey(), source.floodedCaveBiomeKey(), source.preferredProfileKeys(), source.surfacePoolKeys(),
source.shoreBiomeWidth(), source.confinesKey(), source.shoreWidth(), source.erosion(), source.surfacePolicy());
}
}
@@ -0,0 +1,201 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.surface.SurfaceBankSupport;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
import org.junit.Test;
import java.lang.reflect.Method;
import java.util.List;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicInteger;
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 HydrologyRegionalWidthSupportTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyRegionalPlannerTest.settings(false, 1);
private static final List<HydrologyPoint> PATH = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(512, 80, 0));
@Test
public void aWiderChannelContainsTheCenterDepressionWithoutRaisingLand() throws Exception {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && Math.abs(z) <= 1 ? 65 : x >= 256 && x <= 288 ? 86 : 80, 0D, "land");
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
assertEquals(80, hydraulics.supportedHead(station(terrain, 80), terrain));
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateHydraulics",
HydrologyRegionalRoute.Refinement.class, String.class, boolean.class, HydrologyTerrainSampler.class);
validate.setAccessible(true);
HydrologyRegionalRoute.Refinement refined = (HydrologyRegionalRoute.Refinement) validate.invoke(new HydrologyRegionalRoute(planner),
new HydrologyRegionalRoute.Refinement(PATH, null, null, 0, null), "default", false, terrain);
assertNull(refined.toString(), refined.rejection());
HydrologyGeometrySampler geometry = geometry(8);
SurfaceCourseResult built = build(terrain, geometry);
assertTrue(built.accepted());
RiverCourse course = new RiverCourse(91L, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(2L),
"default", 1, List.of(), built.segments());
SurfaceFootprint footprint = new SurfaceFootprintCompiler(SETTINGS, terrain, geometry).compile(course);
assertTrue(footprint.accepted());
assertEquals(0, footprint.uncontainedWetCells());
assertTrue(course.hydraulicallyNonRising());
SurfaceLayerColumn center = null;
for (SurfaceLayerColumn column : footprint.columns()) {
assertTrue(column.layer().bedY() <= column.terrain().naturalHeight());
assertTrue(column.terrain().naturalHeight() - column.layer().bedY() <= SETTINGS.surface().maximumIncision());
if (column.x() == 132 && column.z() == 0) {
center = column;
}
}
assertTrue(center != null);
assertEquals(65, center.layer().bedY());
assertEquals(80, center.layer().fluidHeadY());
SurfaceCourseResult narrow = build(terrain, geometry(4));
assertFalse(narrow.accepted());
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, narrow.rejection());
assertEquals(23, narrow.rejectionDetail());
}
@Test
public void anInteriorWidthCanHoldHeadWhenBothAuthoredExtremaFail() {
HydrologyTerrainSampler terrain = narrowBanks();
assertEquals(65, bankHead(terrain, 4));
assertEquals(80, bankHead(terrain, 6));
assertEquals(65, bankHead(terrain, 8));
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler bounded = (x, z) -> {
assertTrue("A supported interior width must stop wider probes", Math.abs(z) <= 7);
samples.incrementAndGet();
return terrain.sample(x, z);
};
assertEquals(80, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), bounded));
assertTrue(samples.get() > 0);
}
@Test
public void fractionalLocalWidthIsCheckedBeforeUnneededWiderCandidates() {
HydrologyTerrainSampler source = narrowBanks();
HydrologyTerrainSampler terrain = (x, z) -> withWidthMultiplier(source.sample(x, z), 0.8125D);
assertEquals(80, bankHead(terrain, 6.5D));
HydrologyTerrainSampler bounded = (x, z) -> {
assertTrue("The local width6.5 already supports the incoming head", Math.abs(z) <= 6);
return terrain.sample(x, z);
};
assertEquals(80, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), bounded));
}
@Test
public void minimumWidthSupportStaysLazyAndCannotRaiseIncomingHead() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && z == 0 ? 65 : 80, 0D, "land");
AtomicInteger expectedSamples = new AtomicInteger();
bankHead((x, z) -> {
expectedSamples.incrementAndGet();
return terrain.sample(x, z);
}, 4);
AtomicInteger actualSamples = new AtomicInteger();
HydrologyTerrainSampler counted = (x, z) -> {
actualSamples.incrementAndGet();
return terrain.sample(x, z);
};
assertEquals(78, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 78), counted));
assertEquals(expectedSamples.get(), actualSamples.get());
}
@Test
public void incompleteWiderPerimetersCannotCertifyAHighHead() {
HydrologyTerrainSampler terrain = narrowBanks();
HydrologyTerrainSampler incomplete = (x, z) -> x == 133 ? null : terrain.sample(x, z);
assertEquals(65, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), incomplete));
}
@Test
public void additionalSamplingAndRadiusStayBoundedWhenWiderPerimetersAreIncomplete() {
HydrologyPlannerSettings settings = wideSettings();
HydrologyTerrainSample ground = withWidthMultiplier(HydrologyTerrainSample.openLand(65, 0D, "land"), 0.75D);
HydrologyTerrainSample bank = HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyTerrainSampler incomplete = (x, z) -> x == 133 ? null : x == 132 && z == 0 ? ground : bank;
AtomicInteger minimumSamples = new AtomicInteger();
HydrologyTerrainSampler minimum = (x, z) -> {
minimumSamples.incrementAndGet();
return incomplete.sample(x, z);
};
SurfaceBankSupport support = new SurfaceBankSupport(settings.surface(), settings.seaLevel());
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(132, 0, 1D, 0D, 2D);
SurfaceBankSupport.CrossSection cross = support.crossSection(minimum, station, true);
assertFalse(support.perimeter(minimum, station, cross.minimum()).complete());
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler terrain = (x, z) -> {
assertTrue(Math.abs(x - 132) <= 96 && Math.abs(z) <= 96);
samples.incrementAndGet();
return incomplete.sample(x, z);
};
assertEquals(65, new HydrologyRegionalHydraulics(settings).supportedHead(
new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D, ground, 80), terrain));
assertEquals(minimumSamples.get() + 2048, samples.get());
}
private static HydrologyTerrainSampler narrowBanks() {
return (x, z) -> HydrologyTerrainSample.openLand(
x >= 128 && x <= 136 && Math.abs(z) <= 1 || Math.abs(z) >= 7 ? 65 : 80, 0D, "land");
}
private static int bankHead(HydrologyTerrainSampler terrain, double width) {
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(132, 0, 1D, 0D, width / 2D);
SurfaceBankSupport.CrossSection cross = support.crossSection(terrain, station, true);
assertFalse(cross.blocked());
SurfaceBankSupport.Perimeter perimeter = support.perimeter(terrain, station, cross.minimum());
assertTrue(perimeter.complete());
return perimeter.minimum();
}
private static HydrologyRegionalHydraulics.HeadStation station(HydrologyTerrainSampler terrain, int incoming) {
return new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D, terrain.sample(132, 0), incoming);
}
private static HydrologyGeometrySampler geometry(int width) {
return request -> switch (request.field()) {
case SURFACE_WIDTH -> width;
case SURFACE_DEPTH -> 2;
default -> request.minimum();
};
}
private static SurfaceCourseResult build(HydrologyTerrainSampler terrain, HydrologyGeometrySampler geometry) {
return new SurfaceCourseBuilder(SETTINGS.surface(), terrain, geometry, SETTINGS.seaLevel())
.build(71L, 91L, "default", PATH, SurfaceTerminal.SINKHOLE, 40, 256);
}
private static HydrologyTerrainSample withWidthMultiplier(HydrologyTerrainSample source, double multiplier) {
return new HydrologyTerrainSample(source.naturalHeight(), source.slope(), source.ocean(), source.caveAvailable(),
source.caveFloorY(), source.caveFluidY(), source.transitAllowed(), source.outletAllowed(), source.surfaceSourceAllowed(),
source.surfaceSourceRequired(), source.undergroundSourceAllowed(), source.undergroundSourceRequired(), source.routingCost(),
source.surfaceSourceWeight(), source.undergroundSourceWeight(), multiplier, source.depthMultiplier(), source.incisionMultiplier(),
source.routingMultiplier(), source.bankMultiplier(), source.parentBiomeKey(), source.surfaceBiomeKey(), source.mouthBiomeKey(),
source.shoreBiomeKey(), source.bankBiomeKey(), source.floodedCaveBiomeKey(), source.preferredProfileKeys(), source.surfacePoolKeys(),
source.shoreBiomeWidth(), source.confinesKey(), source.shoreWidth(), source.erosion(), source.surfacePolicy());
}
private static HydrologyPlannerSettings wideSettings() {
HydrologyPlannerSettings.Surface surface = SETTINGS.surface();
return new HydrologyPlannerSettings(SETTINGS.seaLevel(), SETTINGS.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), 256,
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), surface.banks()),
SETTINGS.hydraulics(), SETTINGS.underground(), SETTINGS.outlets(), SETTINGS.geometry(), SETTINGS.deepFluids(), SETTINGS.surfacePools(),
SETTINGS.widestShoreBiomeWidth(), SETTINGS.seaCaves(), SETTINGS.surfacePolicyBounds());
}
}
@@ -121,6 +121,6 @@ public class HydrologyRouteLengthsTest {
private static HydrologyPlannerSettings.Routing routing(int spacing, int maximumLength) {
return new HydrologyPlannerSettings.Routing(spacing * 8, spacing, 1_000_000, maximumLength,
0, 0, 1D, 1D, 1D, 1D, 1D, 0);
0, 0, 1D, 1D, 1D, 1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled());
}
}
@@ -197,7 +197,7 @@ public class HydrologyStyledGeometryTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(
surfaceDensity > 0D,
surfaceSources,
@@ -0,0 +1,437 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.CavePosition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.hydrology.surface.SurfaceBounds;
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
import art.arcane.iris.engine.hydrology.surface.SurfaceExcavationMetrics;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
import art.arcane.iris.engine.hydrology.surface.SurfaceRole;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class HydrologySurfaceDropRasterTest {
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
private static final HydrologyTerrainSampler TERRAIN = (x, z) -> HydrologyTerrainSample.openLand(89, 0D, "parent");
private static final HydrologyGeometrySampler GEOMETRY = HydrologyGeometrySampler.deterministic(TERRAIN);
@Test
public void fallingRasterPublishesExactThroatAndReceiverFluidIntervals() {
RiverCourse course = course(true);
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
assertFalse(raster.columns().isEmpty());
assertTrue(raster.connects(0, 0, 80));
assertTrue(raster.connects(0, 0, 90));
assertFalse(raster.connects(0, 0, 79));
assertFalse(raster.connects(0, 0, 91));
assertFalse(raster.connects(100, 100, 85));
assertTrue(raster.columns().stream().flatMap(column -> column.layers().stream())
.anyMatch(HydrologyColumnLayer::receivingPool));
assertThrows(UnsupportedOperationException.class, () -> raster.columns().clear());
}
@Test
public void surfaceDropsDoNotOwnSubmergedGroundOrRaiseWaterOnSeaLevelLand() {
RiverCourse course = course(true);
for (int naturalHeight : new int[] {SETTINGS.seaLevel() - 1, SETTINGS.seaLevel()}) {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(naturalHeight, 0D, "parent");
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, GEOMETRY, course);
assertTrue(raster.columns().isEmpty());
assertFalse(raster.containsRequiredFluid(course, null));
SurfaceFootprint surface = new SurfaceFootprintCompiler(SETTINGS, terrain, GEOMETRY).compile(course);
assertFalse(surface.accepted());
assertEquals(HydrologyCandidateRejection.SURFACE_DROP_UNSUPPORTED, surface.rejection());
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, terrain, GEOMETRY);
assertTrue(compiler.compile(List.of(course)).columns().isEmpty());
assertTrue(compiler.compileValidation(List.of(course)).columnsForCourse(course.id()).isEmpty());
}
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
SETTINGS.seaLevel() + 10, SETTINGS.seaLevel(), 4, 3, true, true,
List.of(new HydrologyPoint(0, SETTINGS.seaLevel() + 10, 0), new HydrologyPoint(12, SETTINGS.seaLevel(), 0)),
HydraulicChannelProfile.uniform(4, 3));
RiverCourse coastal = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(fall));
HydrologyTerrainSampler sill = (x, z) -> HydrologyTerrainSample.openLand(
x < 6 ? SETTINGS.seaLevel() + 10 : SETTINGS.seaLevel(), 0D, "parent");
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, sill, GEOMETRY, coastal);
assertTrue(raster.connects(12, 0, SETTINGS.seaLevel()));
HydrologyColumnSample receiver = raster.sample(12, 0).orElseThrow();
assertEquals(SETTINGS.seaLevel(), receiver.naturalHeight());
assertTrue(receiver.layers().stream().anyMatch(layer -> layer.fluidOwned() && layer.fluidHeadY() == SETTINGS.seaLevel()));
}
@Test
public void nonfallingSurfaceTypesDoNotEnterDropRasterOrCaveValidation() {
RiverCourse course = course(false);
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
assertSame(HydrologySurfaceDropRaster.empty(), raster);
assertFalse(new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY).hasCaveSegment(course));
assertEquals(null, HydrologyCaveCourseFilter.representativeCaveSegment(course));
}
@Test
public void dryDropGradingHonorsDepthAndWidthWithoutChangingTheWetThroat() {
RiverCourse course = course(true);
HydraulicSegment segment = course.segments().getFirst();
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
HydrologyPlannerSettings.Excavation limits = SETTINGS.surface().banks().erosion().excavation();
FootprintLayerShape shape = new FootprintLayerShape(2, 2D, 40D, 79, 90, 90,
false, false, false, false, true);
HydrologyFeatureRef feature = new HydrologyFeatureRef(5L, segment.type(), course.id(), segment.id(),
0, 90, 0, 1, 0, false);
HydrologyTerrainSample highTerrain = HydrologyTerrainSample.openLand(160, 0D, "parent");
HydrologyColumnLayer shore = compiler.regularLayer(course, segment, shape, highTerrain, 3D,
0, 3, false, true, false, false, false, feature);
assertEquals(160 - limits.maximumDepth(), shore.bedY());
assertTrue(shore.terrainOwned());
assertTrue(shore.shore());
double outsideWidth = shape.channelRadius() + limits.maximumWidth() + 1D;
HydrologyColumnLayer outside = compiler.regularLayer(course, segment, shape, highTerrain, outsideWidth,
0, (int) outsideWidth, false, false, true, false, false, feature);
assertEquals(160, outside.bedY());
assertFalse(outside.terrainOwned());
assertFalse(outside.grading());
HydrologyColumnLayer throat = compiler.regularLayer(course, segment, shape, TERRAIN.sample(0, 0), 0D,
0, 0, true, false, false, true, false, feature);
assertEquals(79, throat.bedY());
assertEquals(90, throat.fluidHeadY());
assertFalse(throat.terrainOwned());
assertTrue(throat.fluidOwned());
assertTrue(throat.fallingFluid());
}
@Test
public void combinedExcavationCountsTheDeepestDryCutOnceAndExcludesWetColumns() {
RiverCourse course = course(true);
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
SurfaceCenterline centerline = SurfaceCenterline.densify(course.segments().getFirst().centerline());
int[] baselineVolumes = new int[centerline.size()];
long baseline = SurfaceExcavationMetrics.accumulate(course, centerline, SurfaceFootprint.empty(), drops,
null, baselineVolumes);
assertTrue(baseline > 0L);
HydrologyColumnSample bank = drops.columns().stream()
.filter(column -> column.layers().stream().noneMatch(HydrologyColumnLayer::fluidOwned))
.filter(column -> column.layers().stream().anyMatch(layer -> layer.terrainOwned() && layer.bedY() < column.naturalHeight()))
.findFirst().orElseThrow();
int existingCut = bank.layers().stream().filter(HydrologyColumnLayer::terrainOwned)
.mapToInt(layer -> bank.naturalHeight() - layer.bedY()).max().orElseThrow();
SurfaceLayerColumn deeper = dryColumn(course, bank.x(), bank.z(), existingCut + 3, 1);
SurfaceFootprint exposed = new SurfaceFootprint(List.of(deeper, deeper,
dryColumn(course, 0, 0, 8, 0), dryColumn(course, 100, 100, 7, 5)), 0, null, 0, 0L);
int[] combinedVolumes = new int[centerline.size()];
long combined = SurfaceExcavationMetrics.accumulate(course, centerline, exposed, drops, null, combinedVolumes);
assertEquals(baseline + 10L, combined);
int[] boundedVolumes = new int[centerline.size()];
long bounded = SurfaceExcavationMetrics.accumulate(course, centerline, exposed, drops,
new SurfaceBounds(-100, -100, -1, 100), boundedVolumes);
bounded += SurfaceExcavationMetrics.accumulate(course, centerline, exposed, drops,
new SurfaceBounds(0, -100, 100, 100), boundedVolumes);
assertEquals(combined, bounded);
assertArrayEquals(combinedVolumes, boundedVolumes);
int[] bankVolumes = new int[centerline.size()];
assertEquals(existingCut + 3L, SurfaceExcavationMetrics.accumulate(course, centerline, exposed, drops,
new SurfaceBounds(bank.x(), bank.z(), bank.x(), bank.z()), bankVolumes));
assertEquals(existingCut + 3, bankVolumes[1]);
SurfaceFootprint footprint = new SurfaceFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY).compile(course);
assertEquals(baseline, footprint.bankExcavation());
}
@Test
public void receiverBasinRespectsLocalIncisionAndRejectsAnImpossibleDrop() {
RiverCourse course = course(true);
SurfaceRiverPolicy policy = new SurfaceRiverPolicy("", null, null, null, null, null, null, 4);
HydrologyTerrainSampler shallow = (x, z) -> HydrologyTerrainSample.openLand(81, 0D, "parent").withSurfacePolicy(policy);
HydrologySurfaceDropRaster capped = HydrologySurfaceDropRaster.compile(SETTINGS, shallow, GEOMETRY, course);
assertTrue(capped.containsRequiredFluid(course, null));
assertTrue(capped.connects(12, 0, 80));
for (HydrologyColumnSample column : capped.columns()) {
for (HydrologyColumnLayer layer : column.layers()) {
if (layer.channel()) {
assertTrue(column.naturalHeight() - layer.bedY() <= 4);
}
}
}
HydrologyColumnLayer receiver = capped.sample(12, 0).orElseThrow().layers().stream()
.filter(HydrologyColumnLayer::receivingPool).findFirst().orElseThrow();
assertEquals(77, receiver.bedY());
HydrologyTerrainSampler blocked = (x, z) -> HydrologyTerrainSample.openLand(84, 0D, "parent").withSurfacePolicy(policy);
HydrologySurfaceDropRaster invalid = HydrologySurfaceDropRaster.compile(SETTINGS, blocked, GEOMETRY, course);
assertFalse(invalid.containsRequiredFluid(course, null));
assertFalse(invalid.connects(12, 0, 80));
for (HydrologyColumnSample column : invalid.columns()) {
for (HydrologyColumnLayer layer : column.layers()) {
assertTrue(!layer.fluidOwned() || layer.bedY() < layer.fluidHeadY());
if (!layer.channel() && layer.terrainOwned()) {
assertTrue(column.naturalHeight() - layer.bedY()
<= SETTINGS.surface().banks().erosion().excavation().maximumDepth());
}
}
}
HydrologyTerrainSampler uneven = (x, z) -> HydrologyTerrainSample.openLand(z == 0 ? 81 : 84, 0D, "parent")
.withSurfacePolicy(policy);
HydrologySurfaceDropRaster partial = HydrologySurfaceDropRaster.compile(SETTINGS, uneven, GEOMETRY, course);
assertTrue(partial.containsRequiredFluid(course, null));
assertTrue(partial.sample(12, 1).orElseThrow().layers().stream()
.anyMatch(layer -> !layer.channel() && layer.terrainOwned() && !layer.fluidOwned()));
SurfaceCenterline centerline = SurfaceCenterline.densify(course.segments().getFirst().centerline());
int[] dryVolumes = new int[centerline.size()];
assertTrue(SurfaceExcavationMetrics.accumulate(course, centerline, SurfaceFootprint.empty(), partial,
new SurfaceBounds(12, 1, 12, 1), dryVolumes) > 0L);
SurfaceFootprint rejected = new SurfaceFootprintCompiler(SETTINGS, blocked, GEOMETRY).compile(course);
assertFalse(rejected.accepted());
assertEquals(HydrologyCandidateRejection.SURFACE_DROP_UNSUPPORTED, rejected.rejection());
}
@Test
public void oneBlockBankWidthFollowsTheSurvivingWetReceiverInEveryRasterPath() {
RiverCourse course = course(true);
SurfaceRiverPolicy policy = new SurfaceRiverPolicy("", null, null, null, null, null, null, 4);
HydrologyTerrainSampler uneven = (x, z) -> HydrologyTerrainSample.openLand(z == 0 ? 81 : 84, 0D, "parent")
.withSurfacePolicy(policy);
HydrologyPlannerSettings narrow = withBankWidth(1);
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(narrow, uneven, GEOMETRY, course);
assertTrue(drops.containsRequiredFluid(course, null));
HydrologyColumnSample near = drops.sample(12, 1).orElseThrow();
HydrologyColumnSample far = drops.sample(12, 2).orElseThrow();
assertTrue(near.layers().stream().anyMatch(layer -> !layer.channel() && layer.terrainOwned()));
assertTrue(far.layers().stream().noneMatch(HydrologyColumnLayer::terrainOwned));
assertEquals(far.naturalHeight(), far.terrainHeight());
HashSet<Long> wet = new HashSet<>();
for (HydrologyColumnSample column : drops.columns()) {
if (column.layers().stream().anyMatch(layer -> layer.fluidOwned() && layer.bedY() < layer.fluidHeadY())) {
wet.add(RiverFootprint.pack(column.x(), column.z()));
}
}
for (HydrologyColumnSample column : drops.columns()) {
if (column.layers().stream().anyMatch(layer -> !layer.channel() && layer.terrainOwned())) {
assertTrue("Dry ownership at " + column.x() + "," + column.z() + ": " + column.layers(),
wet.contains(RiverFootprint.pack(column.x(), column.z()))
|| wet.contains(RiverFootprint.pack(column.x() - 1, column.z()))
|| wet.contains(RiverFootprint.pack(column.x() + 1, column.z()))
|| wet.contains(RiverFootprint.pack(column.x(), column.z() - 1))
|| wet.contains(RiverFootprint.pack(column.x(), column.z() + 1)));
}
}
RiverFootprint full = new HydrologyFootprintCompiler(narrow, uneven, GEOMETRY).compile(List.of(course));
assertEquals(far, full.sample(12, 2).orElseThrow());
HydrologyFootprintCompiler.ValidationRaster validation = new HydrologyFootprintCompiler(narrow, uneven, GEOMETRY)
.compileValidation(List.of(course));
assertTrue(validation.columnsForCourse(course.id()).contains(far));
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(narrow, uneven, GEOMETRY, course,
new SurfaceBounds(12, 1, 12, 3));
assertEquals(far, bounded.sample(12, 2).orElseThrow());
}
@Test
public void overlappingDryCutsThatRestoreNaturalHeightDoNotOwnTerrain() {
RiverCourse course = course(true);
HydrologyColumnLayer cut = dryColumn(course, 0, 0, 3, 0).layer();
HydrologyColumnLayer natural = new HydrologyColumnLayer(cut.feature(), 89, 89, 89,
false, false, true, false, false, false, false, false, false,
"water", "parent", "parent", "parent", "parent", "parent");
FootprintMutableColumn column = new FootprintMutableColumn(0, 0, TERRAIN.sample(0, 0), SETTINGS.seaLevel());
column.add(cut);
column.add(natural);
HydrologyColumnLayer merged = column.build().layers().getFirst();
assertEquals(89, merged.bedY());
assertFalse(merged.terrainOwned());
assertTrue(merged.grading());
}
@Test
public void boundedDropRasterMatchesTheSameSubsetOfFullGeometry() {
RiverCourse course = course(true);
SurfaceBounds bounds = new SurfaceBounds(-2, -2, 1, 2);
HydrologySurfaceDropRaster full = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course, bounds);
List<HydrologyColumnSample> expected = full.columns().stream()
.filter(column -> bounds.contains(column.x(), column.z())).toList();
assertEquals(expected, bounded.columns());
AtomicInteger samples = new AtomicInteger();
HydrologyTerrainSampler counting = (x, z) -> {
samples.incrementAndGet();
return TERRAIN.sample(x, z);
};
assertSame(HydrologySurfaceDropRaster.empty(), HydrologySurfaceDropRaster.compile(
SETTINGS, counting, GEOMETRY, course, new SurfaceBounds(1000, 1000, 1015, 1015)));
assertEquals(0, samples.get());
}
@Test
public void directValidationRetainsEveryDropActionAndOnlyOpensAboveTheReceiver() {
RiverCourse course = course(true);
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
HydrologyFootprintCompiler.ValidationRaster validation = compiler.compileSurfaceDropValidation(
course, SurfaceFootprint.empty(), raster);
HydrologyCaveCourseFilter filter = filter(null);
CaveCandidateBuilder builder = filter.candidateBuilders(List.of(course), validation).get(course.id());
HydrologyCaveCandidate candidate = builder.build(filter.options);
for (HydrologyColumnSample column : raster.columns()) {
for (HydrologyColumnLayer layer : column.layers()) {
if (!layer.channel() || !layer.fluidOwned()) {
continue;
}
assertTrue(filter.isCaveLayer(layer, course));
for (int y = layer.bedY() + 1; y <= layer.fluidHeadY(); y++) {
assertTrue(candidate.actions().containsKey(new CavePosition(column.x(), y, column.z())));
}
}
}
assertTrue(candidate.intentionalOpenings().contains(new CavePosition(0, 82, 0)));
assertFalse(candidate.intentionalOpenings().stream().anyMatch(position -> position.y() <= 81));
assertEquals(HydrologyCaveAction.FALLING_FLUID, candidate.actions().get(new CavePosition(0, 85, 0)));
assertTrue(validation.surfaceColumnAt(0, 0).layers().stream().anyMatch(HydrologyColumnLayer::fallingFluid));
assertEquals(89, validation.plannedSurface().resolve(0, 0, 89));
assertFalse(validation.plannedSurface().ownsTerrain(0, 0));
}
@Test
public void biomeOnlyShoreDoesNotReplaceNaturalCaveTerrain() {
RiverCourse course = course(true);
HydrologyColumnLayer original = dryColumn(course, 100, 100, 0, 12).layer();
HydrologyColumnLayer shore = new HydrologyColumnLayer(original.feature(), 89, 89, 89,
false, true, false, false, false, false, false, false, false,
"water", "parent", "parent", "parent", "parent", "parent");
SurfaceLayerColumn column = new SurfaceLayerColumn(100, 100, TERRAIN.sample(100, 100), shore,
SurfaceRole.SHORE, false, 12);
SurfaceFootprint surface = new SurfaceFootprint(List.of(column), 0, null, 0, 0L);
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
HydrologyFootprintCompiler.ValidationRaster validation = compiler.compileSurfaceDropValidation(
course, surface, HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course));
assertFalse(validation.plannedSurface().ownsTerrain(100, 100));
assertEquals(89, validation.plannedSurface().resolve(100, 100, 89));
HydrologyColumnSample sample = new HydrologyColumnSample(100, 100, 89, SETTINGS.seaLevel(),
false, "parent", List.of(shore));
HydrologyFootprintCompiler.ValidationRaster materialized = validation.withMaterializedSurface(
new RiverFootprint(Map.of(RiverFootprint.pack(100, 100), sample)));
assertFalse(materialized.plannedSurface().ownsTerrain(100, 100));
}
@Test
public void fullCourseContainmentAcceptsTheFallButRejectsReceiverExposure() {
RiverCourse course = course(true);
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
HydrologyFootprintCompiler.ValidationRaster validation = compiler.compileSurfaceDropValidation(
course, SurfaceFootprint.empty(), raster);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
HydrologyCaveCourseFilter.Result accepted = filter(new CavePosition(0, 82, 0)).filter(
nodes(), List.of(), outlets(), List.of(course), validation, diagnostics);
assertEquals(1, accepted.courses().size());
assertEquals(1, accepted.cavePlans().size());
Map<Long, HydrologyColumnSample> columns = new HashMap<>();
for (HydrologyColumnSample sample : raster.columns()) {
columns.put(RiverFootprint.pack(sample.x(), sample.z()), sample);
}
HydrologyTile tile = new HydrologyTile(new HydrologyTileKey(0, 0), 1L, 1L, 128,
accepted.nodes(), accepted.edges(), accepted.outlets(), accepted.courses(), accepted.cavePlans(), List.of(), new RiverFootprint(columns));
assertEquals(1, tile.courses().size());
for (boolean connectToExistingCaves : new boolean[] {false, true}) {
diagnostics.clear();
HydrologyCaveCourseFilter.Result rejected = filter(new CavePosition(12, 80, 0), connectToExistingCaves).filter(
nodes(), List.of(), outlets(), List.of(course), validation, diagnostics);
assertTrue(rejected.courses().isEmpty());
assertFalse(diagnostics.isEmpty());
}
}
private static RiverCourse course(boolean falling) {
HydraulicSegment segment = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
90, 80, 4, 3, falling, true,
List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(12, 80, 0)),
HydraulicChannelProfile.uniform(4, 3));
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
"water", 1, List.of(), List.of(segment));
}
private static SurfaceLayerColumn dryColumn(RiverCourse course, int x, int z, int cut, int station) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(10L, HydrologyFeatureType.SURFACE_POOL,
course.id(), course.segments().getFirst().id(), x, 89 - cut, z, 1, 0, false);
HydrologyColumnLayer layer = new HydrologyColumnLayer(feature, 89 - cut, 89 - cut, 89 - cut,
false, false, true, false, false, false, true, false, false,
"water", "parent", "parent", "parent", "parent", "parent");
return new SurfaceLayerColumn(x, z, TERRAIN.sample(x, z), layer, SurfaceRole.BANK, false, station);
}
private static HydrologyPlannerSettings withBankWidth(int width) {
HydrologyPlannerSettings.Surface surface = SETTINGS.surface();
HydrologyPlannerSettings.Banks banks = surface.banks();
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
HydrologyPlannerSettings.Erosion bounded = new HydrologyPlannerSettings.Erosion(erosion.enabled(),
erosion.smoothingRadius(), erosion.thalwegFraction(), erosion.blendCurve(), erosion.bedNoise(),
erosion.style(), erosion.terraceSteps(), erosion.cliffFraction(), erosion.bedProfile(), erosion.shoreRise(),
erosion.blendBaseWidth(), new HydrologyPlannerSettings.Excavation(erosion.excavation().maximumDepth(),
width, erosion.excavation().maximumVolumePerBlock()));
HydrologyPlannerSettings.Banks boundedBanks = new HydrologyPlannerSettings.Banks(banks.sink(), banks.blendSlope(),
banks.minimumBlendWidth(), banks.maximumBlendWidth(), banks.roughness(), banks.roughnessWavelength(),
banks.cascadeRun(), banks.waterfallMinimumDrop(), banks.mouthFlareRatio(), banks.inlet(),
banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), bounded, banks.ponds(),
banks.channel(), banks.flow());
HydrologyPlannerSettings.Surface boundedSurface = new HydrologyPlannerSettings.Surface(surface.enabled(),
surface.sources(), surface.minimumWidth(), surface.maximumWidth(), surface.minimumDepth(), surface.maximumDepth(),
surface.maximumIncision(), surface.shoreWidth(), boundedBanks);
return new HydrologyPlannerSettings(SETTINGS.seaLevel(), SETTINGS.routing(), boundedSurface, SETTINGS.hydraulics(),
SETTINGS.underground(), SETTINGS.outlets(), SETTINGS.geometry(), SETTINGS.deepFluids(), SETTINGS.surfacePools(),
SETTINGS.widestShoreBiomeWidth(), SETTINGS.seaCaves(), SETTINGS.surfacePolicyBounds());
}
private static List<DrainageNode> nodes() {
return List.of(new DrainageNode(3L, 0, 0, TERRAIN.sample(0, 0), 1D, 4L));
}
private static List<RiverOutlet> outlets() {
HydrologyPoint receiver = new HydrologyPoint(12, 80, 0);
return List.of(new RiverOutlet(4L, HydrologyFeatureType.INLAND_GROTTO, 3L,
receiver, receiver, SETTINGS.seaLevel(), false));
}
private static HydrologyCaveCourseFilter filter(CavePosition exposed) {
return filter(exposed, false);
}
private static HydrologyCaveCourseFilter filter(CavePosition exposed, boolean connectToExistingCaves) {
CaveVoxelView view = new CaveVoxelView() {
@Override
public boolean isInWorld(CavePosition position) {
return position.y() >= 0 && position.y() < 128;
}
@Override
public CaveVoxel voxelAt(CavePosition position) {
return CaveVoxel.SOLID;
}
@Override
public boolean isOpenToSurface(CavePosition position) {
return false;
}
@Override
public boolean isAboveTerrainSurface(CavePosition position) {
return position.equals(exposed);
}
};
return new HydrologyCaveCourseFilter(view, new HydrologyCaveCourseFilter.Options(connectToExistingCaves, 8192, 8192));
}
}
@@ -190,11 +190,11 @@ public class HydrologySurfaceProfilesTest {
RiverCourse main = new RiverCourse(30L, RiverCourseType.SURFACE,
OptionalLong.of(1L), OptionalLong.of(99L), "volcanic_lava", 1, List.of(), List.of(
new HydraulicSegment(31L, 30L, HydrologyFeatureType.MOUTH,
84, 84, 4, 2, false, false, List.of(point(256), point(512)))));
84, 84, 4, 2, false, false, List.of(point(256), point(512)), HydraulicChannelProfile.uniform(4, 2))));
RiverCourse tributary = new RiverCourse(40L, RiverCourseType.SURFACE,
OptionalLong.of(2L), OptionalLong.of(99L), "volcanic_lava", 1, List.of(), List.of(
new HydraulicSegment(41L, 40L, HydrologyFeatureType.SURFACE_POOL,
84, 84, 4, 2, false, false, List.of(point(800), point(1024)))));
84, 84, 4, 2, false, false, List.of(point(800), point(1024)), HydraulicChannelProfile.uniform(4, 2))));
ArrayList<RiverCourse> courses = new ArrayList<>(List.of(tributary, main));
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
@@ -230,15 +230,15 @@ public class HydrologySurfaceProfilesTest {
RiverCourse surface = new RiverCourse(30L, RiverCourseType.SURFACE,
OptionalLong.of(1L), OptionalLong.of(99L), "volcanic_lava", 1, List.of(), List.of(
new HydraulicSegment(31L, 30L, HydrologyFeatureType.SURFACE_POOL,
84, 84, 4, 2, false, false, List.of(point(0), lip)),
84, 84, 4, 2, false, false, List.of(point(0), lip), HydraulicChannelProfile.uniform(4, 2)),
new HydraulicSegment(32L, 30L, HydrologyFeatureType.SINKHOLE,
84, 70, 4, 2, true, true, List.of(lip, pool)),
84, 70, 4, 2, true, true, List.of(lip, pool), HydraulicChannelProfile.uniform(4, 2)),
new HydraulicSegment(33L, 30L, HydrologyFeatureType.INLAND_GROTTO,
70, 70, 4, 2, false, true, List.of(pool))));
70, 70, 4, 2, false, true, List.of(pool), HydraulicChannelProfile.uniform(4, 2))));
RiverCourse underground = new RiverCourse(40L, RiverCourseType.UNDERGROUND,
OptionalLong.of(2L), OptionalLong.of(99L), "water", 1, List.of(), List.of(
new HydraulicSegment(41L, 40L, HydrologyFeatureType.INLAND_GROTTO,
70, 70, 4, 2, false, true, List.of(pool))));
70, 70, 4, 2, false, true, List.of(pool), HydraulicChannelProfile.uniform(4, 2))));
HydrologyPlanner planner = planner((x, z) -> sample(List.of("volcanic_lava"), false, "volcanoes"));
assertEquals(List.of(surface, underground),
@@ -287,7 +287,7 @@ public class HydrologySurfaceProfilesTest {
private static RiverCourse stem(String profile) {
HydraulicSegment segment = new HydraulicSegment(31L, 30L, HydrologyFeatureType.SURFACE_POOL,
84, 84, 4, 2, false, false, List.of(point(256), point(512)));
84, 84, 4, 2, false, false, List.of(point(256), point(512)), HydraulicChannelProfile.uniform(4, 2));
return new RiverCourse(30L, RiverCourseType.SURFACE, OptionalLong.of(2L), OptionalLong.of(99L),
profile, 1, List.of(), List.of(segment));
}
@@ -0,0 +1,24 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class HydrologyTerrainSamplerTest {
@Test
public void receivingWaterRequiresKnownOceanBelowSeaLevel() {
HydrologyTerrainSampler sampler = (x, z) -> switch (x) {
case 0 -> HydrologyTerrainSample.ocean(59, "ocean");
case 1 -> HydrologyTerrainSample.ocean(60, "ocean");
case 2 -> HydrologyTerrainSample.ocean(70, "ocean");
case 3 -> HydrologyTerrainSample.openLand(59, 0D, "land");
default -> null;
};
assertTrue(sampler.receivingWater(0, 0, 60));
for (int x = 1; x <= 4; x++) {
assertFalse(sampler.receivingWater(x, 0, 60));
}
}
}
@@ -300,7 +300,7 @@ public class HydrologyTileCacheTest {
private static HydrologyPlannerSettings stalePrefetchSettings() {
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
when(settings.routing()).thenReturn(new HydrologyPlannerSettings.Routing(
1024, 64, 4096, 2048, 0, 0, 0D, 0D, 0D, 0D, 1D, 0));
1024, 64, 4096, 2048, 0, 0, 0D, 0D, 0D, 0D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()));
when(settings.publicationRadius()).thenReturn(364);
return settings;
}
@@ -624,7 +624,7 @@ public class HydrologyTileCacheTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D, 1D, 0),
new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(3),
@@ -676,7 +676,7 @@ public class HydrologyTileCacheTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()),
new HydrologyPlannerSettings.Hydraulics(3),
HydrologyPlannerSettings.Underground.of(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
@@ -1674,4 +1674,54 @@ public class HydrologyTileCacheTest {
assertTrue(cache.isPlanned(40, 40));
}
@Test
public void authoritativeSnapshotsDistinguishPendingPlansFromPlannedNaturalTerrain() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
List<Runnable> queued = new ArrayList<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add, () -> true);
assertFalse(cache.columnSnapshot(40, 40).available());
verify(planner, never()).plan(any(HydrologyTileKey.class));
assertFalse(queued.isEmpty());
for (Runnable task : List.copyOf(queued)) {
task.run();
}
HydrologyColumnSnapshot snapshot = cache.columnSnapshot(40, 40);
assertTrue(snapshot.available());
assertEquals(null, snapshot.column());
}
@Test
public void nonblockingCompositionRetainsItsTileSnapshotAcrossCacheEviction() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
List<Runnable> queued = new ArrayList<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add, () -> true);
assertFalse(cache.columnSnapshot(40, 40).available());
for (Runnable task : List.copyOf(queued)) {
task.run();
}
AtomicBoolean evicted = new AtomicBoolean();
when(tile.columnAt(anyInt(), anyInt())).thenAnswer(invocation -> {
if (evicted.compareAndSet(false, true)) {
cache.clear();
}
return Optional.empty();
});
HydrologyColumnSnapshot snapshot = cache.columnSnapshot(40, 40);
assertTrue(evicted.get());
assertTrue(snapshot.available());
assertEquals(null, snapshot.column());
assertFalse(cache.columnSnapshot(40, 40).available());
}
}
@@ -104,7 +104,8 @@ public class StudioHydrologyTileStoreTest {
2,
false,
false,
List.of(new HydrologyPoint(0, 72, 0), new HydrologyPoint(1, 72, 0))
List.of(new HydrologyPoint(0, 72, 0), new HydrologyPoint(1, 72, 0)),
new HydraulicChannelProfile(new double[]{2.5D, 4D}, new double[]{1D, 2D})
);
RiverCourse course = new RiverCourse(
4L,
@@ -0,0 +1,170 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.hydrology.HydraulicChannelProfile;
import art.arcane.iris.engine.hydrology.HydraulicSegment;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyRoutingTerrainSampler;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.RiverCourse;
import art.arcane.iris.engine.hydrology.RiverCourseType;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverProfile;
import art.arcane.volmlib.util.collection.KList;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
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;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisHydrologyPhysicalOceanTest {
private static final int SEA_LEVEL = 306;
private static IrisSettings previousSettings;
@BeforeClass
public static void useDefaultSettings() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
}
@AfterClass
public static void restoreSettings() {
IrisSettings.settings = previousSettings;
}
@Test
public void detailedAndFastQueriesAgreeOnTheActualOceanWaterlineInEitherOrder() throws Exception {
for (boolean classifyFirst : new boolean[]{true, false}) {
Map<Integer, Integer> sampledHeights = new HashMap<>();
try (IrisHydrologyRuntime runtime = runtime((x, z) -> {
sampledHeights.merge(x, 1, Integer::sum);
return switch (x) {
case 0 -> 308D;
case 1 -> 306D;
case 2 -> 305D;
default -> 305.6D;
};
}, (x, z) -> true)) {
IrisHydrologyRoutingTerrainSampler sampler = sampler(runtime);
for (int x = 0; x < 4; x++) {
HydrologyRoutingTerrainSampler.NaturalClassification expected = x == 2
? HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
: HydrologyRoutingTerrainSampler.NaturalClassification.LAND;
if (classifyFirst) {
assertEquals(expected, sampler.classifyNatural(x, 0));
}
HydrologyTerrainSample terrain = sampler.sampleBasisWithoutSlope(x, 0);
assertEquals(x == 2, terrain.ocean());
assertEquals(x != 2, terrain.transitAllowed());
assertEquals(expected, sampler.classifyNatural(x, 0));
assertEquals(1, sampledHeights.get(x).intValue());
}
}
}
}
@Test
public void aMouthCrossesDryOceanIntentShoreAndReachesOnlyTheSubmergedReservoir() throws Exception {
try (IrisHydrologyRuntime runtime = runtime((x, z) -> x >= 480 ? 305D
: x == 479 ? 306D : x >= 400 ? 308D : 310D, (x, z) -> x >= 400)) {
IrisHydrologyRoutingTerrainSampler sampler = sampler(runtime);
assertEquals(HydrologyRoutingTerrainSampler.NaturalClassification.LAND, sampler.classifyNatural(400, 0));
assertEquals(HydrologyRoutingTerrainSampler.NaturalClassification.LAND, sampler.classifyNatural(479, 0));
assertEquals(HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN, sampler.classifyNatural(480, 0));
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
case SURFACE_WIDTH -> 6;
case SURFACE_DEPTH -> 3;
default -> request.minimum();
};
SurfaceCourseResult result = new SurfaceCourseBuilder(runtime.settings().surface(),
sampler::sampleBasisWithoutSlope, geometry, SEA_LEVEL).build(17L, 42L, "water",
List.of(new HydrologyPoint(0, 310, 0), new HydrologyPoint(480, SEA_LEVEL, 0)),
SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 384);
assertNull(result.toString(), result.rejection());
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
segments.add(new HydraulicSegment(99L, 42L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL,
result.lastWidth(), result.lastDepth(), false, false,
List.of(result.pathEnd(), new HydrologyPoint(480, SEA_LEVEL, 0)),
HydraulicChannelProfile.uniform(result.lastWidth(), result.lastDepth())));
RiverCourse course = new RiverCourse(42L, RiverCourseType.SURFACE, OptionalLong.of(1L),
OptionalLong.of(2L), "water", 1, List.of(), segments);
SurfaceFootprint footprint = new SurfaceFootprintCompiler(runtime.settings(),
sampler::sampleBasisWithoutSlope, geometry).compile(course);
assertNull(footprint.toString(), footprint.rejection());
Map<Integer, SurfaceLayerColumn> center = new HashMap<>();
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.z() == 0) {
center.put(column.x(), column);
}
if (column.terrain().ocean()) {
assertFalse(column.layer().terrainOwned());
assertFalse(column.layer().fluidOwned());
}
}
for (int x = 400; x < 480; x++) {
SurfaceLayerColumn column = center.get(x);
assertNotNull("Dry ocean-intent channel at " + x, column);
HydrologyColumnLayer layer = column.layer();
assertTrue("Owned water at " + x, layer.channel() && layer.fluidOwned());
assertTrue(layer.bedY() < layer.fluidHeadY());
}
assertEquals(SEA_LEVEL, center.get(479).layer().fluidHeadY());
assertTrue(sampler.sampleBasisWithoutSlope(480, 0).naturalHeight() < SEA_LEVEL);
}
}
@SuppressWarnings("unchecked")
private static IrisHydrologyRuntime runtime(IrisHydrologyNaturalHeightProvider heights,
IrisHydrologyNaturalOceanClassifier oceanIntent) {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(loader);
when(loader.getPossibleKeys()).thenReturn(new String[0]);
when(loader.loadAll(any(String[].class))).thenReturn(new KList<>());
IrisDimension dimension = new IrisDimension().setRegions(new KList<>());
dimension.setFluidHeight(SEA_LEVEL + dimension.getMinHeight());
dimension.setLoadKey("coast");
dimension.setLoader(data);
dimension.getHydrology().getRivers().setProfiles(new KList<>(new IrisRiverProfile().setId("water")));
IrisRegion region = new IrisRegion();
region.setLoadKey("region");
IrisBiome shore = new IrisBiome();
shore.setLoadKey("shore");
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(x, z, height) -> new IrisHydrologyNaturalSample(height, oceanIntent.isOcean(x, z), shore, region),
heights, (x, z) -> "coast", oceanIntent, footprint -> null, () -> false));
}
private static IrisHydrologyRoutingTerrainSampler sampler(IrisHydrologyRuntime runtime) throws Exception {
Field field = IrisHydrologyRuntime.class.getDeclaredField("routingTerrainSampler");
field.setAccessible(true);
return (IrisHydrologyRoutingTerrainSampler) field.get(runtime);
}
}
@@ -45,7 +45,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
coordinateCalls.merge(pack(x, z), 1, Integer::sum);
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(65_536)
);
@@ -121,7 +122,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
heightCalls.incrementAndGet();
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
);
@@ -145,7 +147,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
calls.incrementAndGet();
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
);
@@ -177,7 +180,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
calls.add("height:" + x + "," + z);
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
);
@@ -210,7 +214,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
(int x, int z) -> {
classifierCalls.incrementAndGet();
return x >= 0;
}
},
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
);
@@ -242,7 +247,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
calls.merge(pack(x, z), 1, Integer::sum);
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
);
@@ -276,7 +282,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
heightCalls.incrementAndGet();
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
);
@@ -332,7 +339,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
(int x, int z) -> {
classifierCalls.merge(pack(x, z), 1, Integer::sum);
return false;
}
},
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(2)
);
@@ -371,7 +379,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
heightCalls.incrementAndGet();
return Double.NaN;
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16)
);
@@ -480,7 +489,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
heightCalls.incrementAndGet();
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(
65_536,
@@ -516,7 +526,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
}
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(
65_536,
@@ -542,7 +553,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
new IrisHydrologyRoutingTerrainSampler.Sources(
(int x, int z, double naturalHeight) -> basis(x, z, naturalHeight),
(int x, int z) -> height(x, z),
(int x, int z) -> x >= 1
(int x, int z) -> x >= 1,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(maximumEntries)
);
@@ -567,7 +579,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
coordinateCalls.merge(pack(x, z), 1, Integer::sum);
return height(x, z);
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(
maximumEntries,
@@ -651,7 +664,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
}
return 90D;
},
(int x, int z) -> false
(int x, int z) -> false,
128
),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(64)
);
@@ -335,7 +335,7 @@ public class IrisHydrologyRuntimeSettingsTest {
HydrologyPlannerSettings.Banks banks = settings(dimension).surface().banks();
assertEquals(new HydrologyPlannerSettings.Erosion(true, 7, 0.35D, 1.25D, 0.4D,
IrisRiverBlendStyle.TERRACED, 6, 0.3D, IrisRiverBedProfile.V, 1.5D, 3D), banks.erosion());
IrisRiverBlendStyle.TERRACED, 6, 0.3D, IrisRiverBedProfile.V, 1.5D, 3D, HydrologyPlannerSettings.Excavation.defaults()), banks.erosion());
assertEquals(new HydrologyPlannerSettings.Channel(9, 0.5D, 1.9D, 2.5D), banks.channel());
assertEquals(new HydrologyPlannerSettings.Flow(0.4D, 3, 1.5D, 2), banks.flow());
assertEquals(new HydrologyPlannerSettings.Inlet(80, 4, 36, 0.3D, 1.75D), banks.inlet());
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
@@ -30,6 +31,68 @@ public class ErosionFieldCompilerTest {
default -> request.minimum();
};
@Test
public void mouthCutsDrySeaLevelSillBeforeReachingOceanWater() {
HydrologyTerrainSampler coast = (int x, int z) -> x >= 100
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 92 ? SEA_LEVEL : 66, 0D, "land");
Compiled compiled = compile(zeroRoughnessSurface(), coast, 110, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
for (int x = 92; x < 100; x++) {
SurfaceColumn sill = compiled.field().column(x, 0);
assertNotNull("dry sill at " + x, sill);
assertTrue("sill must be carved at " + x, !sill.apron());
assertEquals(SEA_LEVEL, sill.headY());
assertTrue(sill.height() < SEA_LEVEL);
}
assertNull(compiled.field().rejection());
}
@Test
public void mouthLabelWithoutReceivingOceanIsRejected() {
Compiled compiled = compile(300, (x, z) -> 66, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertEquals(HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, compiled.field().rejection());
}
@Test
public void bankExcavationPreservesHighSideTerrainWithinDepthAndWidthLimits() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z - 4) * 4, 0D, "land");
Compiled compiled = compile(zeroRoughnessSurface(), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
for (SurfaceColumn column : compiled.field().columns().values()) {
if (column.role() == SurfaceRole.CHANNEL || column.apron()) {
continue;
}
assertTrue(column.terrain().naturalHeight() - column.height() <= 8);
if (column.x() > 30 && column.x() < 270 && Math.abs(column.z()) > 19) {
assertEquals(column.terrain().naturalHeight(), column.height());
}
}
SurfaceColumn bank = compiled.field().column(150, 9);
assertNotNull(bank);
assertTrue(bank.height() >= hillside.sample(150, 9).naturalHeight() - 8);
}
@Test
public void boundedRasterMatchesTheCompleteCourseAcrossTileCuts() {
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(new HydrologyPoint(-150, 0, 0), new HydrologyPoint(150, 0, 0)));
ChannelProfile channel = new ChannelProfileBuilder(surface, hillside, CONSTANT_GEOMETRY).build(centerline, "water", false);
ValleyProfile valley = new ValleyProfileSolver(surface, hillside, SEA_LEVEL, 64).solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
ErosionFieldCompiler compiler = new ErosionFieldCompiler(surface, hillside, SEA_LEVEL);
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8);
SurfaceBounds bounds = new SurfaceBounds(-16, -16, 15, 15);
ErosionField bounded = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8, surface.banks().ponds(), SurfaceRasterContext.bounded(bounds));
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
assertEquals("bounded column " + x + "," + z, complete.column(x, z), bounded.column(x, z));
}
}
}
@Test
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
@@ -187,7 +250,7 @@ public class ErosionFieldCompilerTest {
}
@Test
public void inletBanksAreCutToSeaLevelAndBlendBackToTheNaturalValley() {
public void inletBanksPreserveHighTerrainAndBlendBackWithinTheirExcavationBudget() {
HydrologyTerrainSampler coast = (int x, int z) -> {
if (x >= 240) {
return HydrologyTerrainSample.ocean(50, "ocean");
@@ -230,11 +293,11 @@ public class ErosionFieldCompilerTest {
assertTrue(shoreZ > 0);
SurfaceColumn shore = compiled.field().column(pinned, shoreZ);
assertEquals(SurfaceRole.SHORE, shore.role());
assertEquals(SEA_LEVEL, shore.height());
assertEquals(shore.terrain().naturalHeight() - surface.banks().erosion().excavation().maximumDepth(), shore.height());
assertTrue(coast.sample(pinned, shoreZ).naturalHeight() > SEA_LEVEL + 10);
int previous = SEA_LEVEL;
int previous = shore.height();
int banks = 0;
int lastHeight = SEA_LEVEL;
int lastHeight = shore.height();
for (int z = shoreZ + 1; z < 60; z++) {
SurfaceColumn column = compiled.field().column(pinned, z);
if (column == null) {
@@ -242,12 +305,12 @@ public class ErosionFieldCompilerTest {
}
assertTrue(column.role() == SurfaceRole.SHORE || column.role() == SurfaceRole.BANK);
assertTrue(column.height() >= previous);
assertTrue(column.height() - previous <= 1);
assertTrue(column.terrain().naturalHeight() - column.height() <= surface.banks().erosion().excavation().maximumDepth());
previous = column.height();
lastHeight = column.height();
banks++;
}
assertTrue(banks > 8);
assertTrue(banks > 0);
assertTrue(lastHeight >= coast.sample(pinned, 0).naturalHeight() - 1);
}
@@ -377,7 +440,7 @@ public class ErosionFieldCompilerTest {
double blendBaseWidth
) {
return new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D, style, terraceSteps, cliffFraction,
bedProfile, shoreRise, blendBaseWidth);
bedProfile, shoreRise, blendBaseWidth, HydrologyPlannerSettings.Excavation.defaults());
}
private static void assertSameField(Compiled expected, Compiled actual) {
@@ -742,14 +805,18 @@ public class ErosionFieldCompilerTest {
HydrologyTerrainSampler shelf = (int x, int z) ->
HydrologyTerrainSample.openLand(80 + Math.min(16, Math.max(0, Math.abs(z) - 4) * 4), 0D, "land");
Compiled terraced = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.TERRACED, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
IrisRiverBlendStyle.TERRACED, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D,
new HydrologyPlannerSettings.Excavation(64, 64, 8192))),
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
Set<Integer> steps = bankHeights(terraced, 150, 9, 40);
assertTrue(steps.toString(), steps.size() >= 2 && steps.size() <= 4);
assertHolds(terraced);
Compiled cliff = compile(
zeroRoughnessSurface(0, shapedErosion(IrisRiverBlendStyle.CLIFF, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D)),
zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
IrisRiverBlendStyle.CLIFF, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D,
new HydrologyPlannerSettings.Excavation(64, 64, 8192))),
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
Set<Integer> bench = bankHeights(cliff, 150, 9, 40);
assertEquals(bench.toString(), 2, bench.size());
@@ -0,0 +1,194 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class SurfaceBendProfileTest {
private static final HydrologyTerrainSampler FLAT = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
@Test
public void curvedChannelMovesDepthToTheOuterBendWithinTheExistingEnvelope() {
SurfaceCenterline centerline = curve(false);
ErosionField field = compile(centerline, SurfaceRasterContext.none());
SurfaceColumn outer = field.column(38, 10);
SurfaceColumn inner = field.column(30, 18);
assertNotNull(outer);
assertNotNull(inner);
assertEquals(SurfaceRole.CHANNEL, outer.role());
assertEquals(SurfaceRole.CHANNEL, inner.role());
assertTrue(outer.height() < symmetricBed(centerline, outer));
assertTrue(inner.height() > symmetricBed(centerline, inner));
assertTrue(outer.height() < inner.height());
assertNull(field.rejection());
assertEquals(0, field.uncontainedWetCells());
for (SurfaceColumn column : field.columns().values()) {
assertTrue(column.height() <= column.terrain().naturalHeight());
if (column.role() == SurfaceRole.CHANNEL) {
assertEquals(80, column.headY());
assertTrue(column.height() >= 74 && column.height() <= 79);
} else {
assertEquals(80, column.height());
}
}
}
@Test
public void mirroredBendsMirrorTheBedAndKeepBothChannelsContained() {
ErosionField first = compile(curve(false), SurfaceRasterContext.none());
ErosionField mirrored = compile(curve(true), SurfaceRasterContext.none());
assertNull(mirrored.rejection());
assertEquals(0, mirrored.uncontainedWetCells());
for (SurfaceColumn column : first.columns().values()) {
SurfaceColumn other = mirrored.column(-column.x(), column.z());
assertNotNull(other);
assertEquals(column.height(), other.height());
assertEquals(column.headY(), other.headY());
assertEquals(column.role(), other.role());
}
}
@Test
public void tileClippingCannotChangeTheBendBed() {
SurfaceCenterline centerline = curve(false);
ErosionField complete = compile(centerline, SurfaceRasterContext.none());
SurfaceBounds bounds = new SurfaceBounds(24, 0, 39, 31);
ErosionField clipped = compile(centerline, SurfaceRasterContext.bounded(bounds));
assertNull(clipped.rejection());
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
assertEquals(complete.column(x, z), clipped.column(x, z));
}
}
}
@Test
public void straightAndPointChannelsKeepTheirSymmetricProfiles() {
for (List<HydrologyPoint> points : List.of(List.of(new HydrologyPoint(0, 80, 0)),
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(200, 80, 0)))) {
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
double[] offsets = SurfaceBendProfile.offsets(centerline, channel(centerline));
for (double offset : offsets) {
assertEquals(0D, offset, 0D);
}
}
for (double distance = 0D; distance <= 1D; distance += 0.125D) {
assertEquals(distance, SurfaceBendProfile.normalizedDistance(distance, -1D, 0D), 0D);
assertEquals(distance, SurfaceBendProfile.normalizedDistance(distance, 1D, 0D), 0D);
}
}
@Test
public void alternatingBendsStayBoundedAndFadeAtTheirTerminals() {
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (int x = 0; x <= 512; x += 2) {
points.add(new HydrologyPoint(x, 80, (int) StrictMath.round(32D * StrictMath.sin(x / 64D))));
}
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
double[] offsets = SurfaceBendProfile.offsets(centerline, channel(centerline));
double minimum = 0D;
double maximum = 0D;
for (int station = 0; station < offsets.length; station++) {
minimum = Math.min(minimum, offsets[station]);
maximum = Math.max(maximum, offsets[station]);
assertTrue(StrictMath.abs(offsets[station]) <= 0.35D);
if (station > 0) {
assertTrue(StrictMath.abs(offsets[station] - offsets[station - 1]) < 0.06D);
}
}
assertTrue(minimum < -0.05D && maximum > 0.05D);
assertEquals(0D, offsets[0], 0D);
assertEquals(0D, offsets[offsets.length - 1], 0D);
}
@Test
public void mirroredHairpinsRetainOppositeBendDirectionsAcrossAHalfTurn() {
List<HydrologyPoint> points = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(32, 80, 0),
new HydrologyPoint(44, 80, 12), new HydrologyPoint(32, 80, 24), new HydrologyPoint(0, 80, 24));
ArrayList<HydrologyPoint> reflected = new ArrayList<>(points.size());
for (HydrologyPoint point : points) {
reflected.add(new HydrologyPoint(point.x(), point.y(), -point.z()));
}
SurfaceCenterline first = SurfaceCenterline.densify(points);
SurfaceCenterline second = SurfaceCenterline.densify(reflected);
double[] left = SurfaceBendProfile.offsets(first, channel(first));
double[] right = SurfaceBendProfile.offsets(second, channel(second));
assertEquals(left.length, right.length);
assertTrue(left[44] < -0.3D);
assertTrue(right[44] > 0.3D);
for (int station = 0; station < left.length; station++) {
assertEquals(-left[station], right[station], 1.0E-12D);
}
}
private static SurfaceCenterline curve(boolean mirrored) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (int degrees = 0; degrees <= 90; degrees += 3) {
double angle = StrictMath.toRadians(degrees);
int x = (int) StrictMath.round(48D * StrictMath.sin(angle));
int z = (int) StrictMath.round(48D * (1D - StrictMath.cos(angle)));
points.add(new HydrologyPoint(x, 80, z));
}
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
if (!mirrored) {
return centerline;
}
int[] xs = centerline.x().clone();
double[] tangents = centerline.tangentX().clone();
for (int station = 0; station < xs.length; station++) {
xs[station] = -xs[station];
tangents[station] = -tangents[station];
}
return new SurfaceCenterline(xs, centerline.z().clone(), tangents,
centerline.tangentZ().clone(), centerline.pathIndex().clone());
}
private static int symmetricBed(SurfaceCenterline centerline, SurfaceColumn column) {
double normalized = Math.min(1D, centerline.distanceToSegment(column.station(), column.x(), column.z()) / 8D);
double depth = 1D + 5D * ErosionFieldCompiler.bedFactor(normalized, surface().banks().erosion());
return 80 - (int) StrictMath.round(depth);
}
private static ChannelProfile channel(SurfaceCenterline centerline) {
double[] halfWidth = new double[centerline.size()];
double[] depth = new double[centerline.size()];
double[] bank = new double[centerline.size()];
Arrays.fill(halfWidth, 8D);
Arrays.fill(depth, 6D);
Arrays.fill(bank, 1D);
return new ChannelProfile(halfWidth, depth, bank);
}
private static ErosionField compile(SurfaceCenterline centerline, SurfaceRasterContext context) {
HydrologyPlannerSettings.Surface surface = surface();
ChannelProfile channel = channel(centerline);
ValleyProfile valley = new ValleyProfileSolver(surface, FLAT, 60, 32)
.solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
assertTrue(valley.accepted());
return new ErosionFieldCompiler(surface, FLAT, 60).compile(42L, centerline, channel, valley,
SurfaceTerminal.SINKHOLE, 8, HydrologyPlannerSettings.Ponds.none(), context);
}
private static HydrologyPlannerSettings.Surface surface() {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
return new HydrologyPlannerSettings.Surface(true, defaults.sources(), 16, 16, 6, 6, 16, 1.5D,
HydrologyPlannerSettings.Banks.of(0, 3D, 4, 32, 0D, 16, 2, 6, 1.6D,
HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.none()));
}
}
@@ -19,4 +19,10 @@ public class SurfaceCellAdmissionTest {
assertFalse(SurfaceCellAdmission.writable(HydrologyTerrainSample.openLand(40, 0D, "land"), 60));
assertFalse(SurfaceCellAdmission.writable(null, 60));
}
@Test
public void mouthMayCutADrySillButCannotOwnOceanGround() {
assertTrue(SurfaceCellAdmission.mouthLand(HydrologyTerrainSample.openLand(60, 0D, "land"), 60));
assertFalse(SurfaceCellAdmission.mouthLand(HydrologyTerrainSample.ocean(50, "ocean"), 60));
}
}
@@ -116,10 +116,19 @@ public class SurfaceCourseBuilderTest {
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
assertNull(gentleResult.rejection());
assertNull(steepResult.rejection());
assertNull("detail=" + steepResult.rejectionDetail(), steepResult.rejection());
assertTrue(gentleResult.segments().stream().anyMatch(segment -> segment.type() == HydrologyFeatureType.RIFFLE));
assertTrue(gentleResult.segments().stream().noneMatch(segment -> segment.type() == HydrologyFeatureType.CASCADE));
assertTrue(steepResult.segments().stream().anyMatch(segment -> segment.type() == HydrologyFeatureType.CASCADE));
for (HydraulicSegment segment : steepResult.segments()) {
for (int station = 0; station < segment.centerline().size(); station++) {
HydrologyPoint point = segment.centerline().get(station);
int cut = steep.sample(point.x(), point.z()).naturalHeight() - point.y()
+ (int) StrictMath.round(segment.channelProfile().depthAt(station));
assertTrue("cut=" + cut + " at " + point.x(),
cut <= HydrologyPlannerSettings.defaults().surface().maximumIncision());
}
}
}
@Test
@@ -0,0 +1,58 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public final class SurfaceExcavationMetricsTest {
@Test
public void compactSourceExcavationIsAveragedAcrossSixteenBlocks() {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0)));
int[] volumes = new int[centerline.size()];
volumes[0] = 1000;
assertEquals(63, SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, volumes));
}
@Test
public void centeredWindowIncludesBothEightBlockEndpoints() {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 70, 0), new HydrologyPoint(32, 70, 0)));
int[] volumes = new int[centerline.size()];
volumes[0] = 16;
volumes[16] = 16;
assertEquals(2, SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, volumes));
volumes[16] = 0;
volumes[17] = 16;
assertEquals(1, SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, volumes));
}
@Test
public void diagonalStationSpacingUsesArcLength() {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 70, 0), new HydrologyPoint(24, 70, 24)));
int[] volumes = new int[centerline.size()];
Arrays.fill(volumes, 16);
assertEquals(11, SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, volumes));
}
@Test
public void invalidVolumesFailWithoutWrappingTheirSum() {
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 70, 0), new HydrologyPoint(16, 70, 0)));
assertThrows(IllegalArgumentException.class,
() -> SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, new int[1]));
int[] negative = new int[centerline.size()];
negative[0] = -1;
assertThrows(IllegalArgumentException.class,
() -> SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, negative));
int[] large = new int[centerline.size()];
Arrays.fill(large, Integer.MAX_VALUE);
assertEquals(Integer.MAX_VALUE, SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, large));
}
}
@@ -1,11 +1,14 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydraulicChannelProfile;
import art.arcane.iris.engine.hydrology.HydraulicSegment;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologySurfaceDropRaster;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.RiverCourse;
@@ -32,6 +35,47 @@ public class SurfaceFootprintCompilerTest {
default -> request.minimum();
};
@Test
public void mouthUsesConnectedFloodedLandWithoutChangingTerrainMetadataOrOwningWater() {
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
HydrologyTerrainSampler sampler = (x, z) -> x >= 132
? HydrologyTerrainSample.ocean(seaLevel - 5, "ocean")
: HydrologyTerrainSample.openLand(x >= 100 ? seaLevel - 5 : seaLevel, 0D, "land");
RiverCourse course = course(List.of(
segment(1L, HydrologyFeatureType.MOUTH, seaLevel, seaLevel, points(0, 140, seaLevel))));
SurfaceFootprint footprint = compiler(sampler).compile(course);
assertNull(footprint.rejection());
assertEquals(0, footprint.uncontainedWetCells());
SurfaceLayerColumn receiver = column(footprint, 100, 0);
assertNotNull(receiver);
assertEquals(sampler.sample(100, 0), receiver.terrain());
assertFalse(receiver.terrain().ocean());
assertTrue(receiver.layer().oceanApron());
assertFalse(receiver.layer().terrainOwned());
assertFalse(receiver.layer().fluidOwned());
SurfaceBounds bounds = new SurfaceBounds(90, -8, 110, 8);
SurfaceFootprint bounded = compiler(sampler).compile(course, bounds);
assertEquals(footprint.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(),
bounded.columns());
}
@Test
public void mouthCannotUseAnInlandPoolSeparatedFromTheOceanByDryGround() {
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
HydrologyTerrainSampler sampler = (x, z) -> x >= 132
? HydrologyTerrainSample.ocean(seaLevel - 5, "ocean")
: HydrologyTerrainSample.openLand(x >= 100 && x != 120 ? seaLevel - 5 : seaLevel, 0D, "land");
RiverCourse course = course(List.of(
segment(1L, HydrologyFeatureType.MOUTH, seaLevel, seaLevel, points(0, 140, seaLevel))));
SurfaceFootprint footprint = compiler(sampler).compile(course);
assertNotNull(footprint.rejection());
assertNull(column(footprint, 100, 0));
}
@Test
public void compiledLayersDescribeAContainedCarveOnlyChannelWithShoreAndBank() {
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 150 && z > 8
@@ -62,7 +106,7 @@ public class SurfaceFootprintCompilerTest {
assertTrue(layer.bedY() < layer.fluidHeadY());
} else {
assertTrue(layer.grading());
assertTrue(layer.terrainOwned());
assertEquals(layer.bedY() != column.terrain().naturalHeight(), layer.terrainOwned());
assertFalse(layer.fluidOwned());
assertEquals(layer.bedY(), layer.fluidHeadY());
}
@@ -156,8 +200,9 @@ public class SurfaceFootprintCompilerTest {
assertTrue(shoreZ > 3);
SurfaceLayerColumn shore = column(footprint, 90, shoreZ);
assertTrue(shore.layer().shore());
assertEquals(SEA_LEVEL, shore.layer().bedY());
assertEquals(SEA_LEVEL, shore.layer().fluidHeadY());
assertEquals(shore.terrain().naturalHeight() - HydrologyPlannerSettings.Excavation.defaults().maximumDepth(),
shore.layer().bedY());
assertEquals(shore.layer().bedY(), shore.layer().fluidHeadY());
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.terrain().ocean()) {
assertTrue(column.layer().oceanApron());
@@ -166,6 +211,98 @@ public class SurfaceFootprintCompilerTest {
}
}
@Test
public void aContainedFallRetainsBothExposedRunsAndWholeCourseStationIndices() {
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
SurfaceTerminal.TRIBUTARY, 40, 64);
assertNull(built.rejection());
RiverCourse course = course(built.segments());
HydraulicSegment fall = course.segments().stream().filter(HydraulicSegment::fallingFluid).findFirst().orElseThrow();
assertEquals(100, fall.start().x());
SurfaceFootprintCompiler compiler = compiler(sampler);
SurfaceFootprint full = compiler.compile(course);
assertNull(full.rejection());
assertEquals(0, full.uncontainedWetCells());
SurfaceLayerColumn upstream = column(full, 99, 0);
SurfaceLayerColumn downstream = column(full, 130, 0);
assertNotNull(upstream);
assertNotNull(downstream);
assertTrue(upstream.layer().channel());
assertTrue(downstream.layer().channel());
assertEquals(100, upstream.layer().fluidHeadY());
assertEquals(90, downstream.layer().fluidHeadY());
assertEquals(130, downstream.station());
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, sampler, CONSTANT_GEOMETRY, course);
assertTrue(drops.connects(100, 0, upstream.layer().fluidHeadY()));
assertTrue(drops.connects(100, 0, downstream.layer().fluidHeadY()));
assertTrue(drops.connects(101, 0, downstream.layer().fluidHeadY()));
SurfaceBounds bounds = new SurfaceBounds(80, -6, 140, 6);
SurfaceFootprint bounded = compiler.compile(course, bounds);
assertNull(bounded.rejection());
assertEquals(full.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(), bounded.columns());
for (SurfaceLayerColumn value : full.columns()) {
if (value.layer().terrainOwned()) {
assertTrue(value.layer().bedY() <= value.terrain().naturalHeight());
}
}
}
@Test
public void aSteepContinuousSlopeKeepsItsRoundedBanksWithinTheChannelCutLimit() {
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
SurfaceTerminal.TRIBUTARY, 40, 64);
assertNull("detail=" + built.rejectionDetail(), built.rejection());
SurfaceFootprint footprint = compiler(sampler).compile(course(built.segments()));
assertNull("detail=" + footprint.rejectionDetail(), footprint.rejection());
assertEquals(0, footprint.uncontainedWetCells());
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.layer().channel()) {
assertTrue(column.terrain().naturalHeight() - column.layer().bedY()
<= settings.surface().maximumIncision());
}
}
}
@Test
public void aDiagonalFourteenBlockFallPreservesTheUpstreamBedIncisionLimit() {
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(
x + z < 200 ? 100 : 86, 0D, "land");
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 200)),
SurfaceTerminal.TRIBUTARY, 40, 64);
assertNull(built.rejection());
RiverCourse course = course(built.segments());
assertTrue(course.segments().stream().anyMatch(segment -> segment.fallingFluid() && segment.drop() == 14));
SurfaceFootprint footprint = compiler(sampler).compile(course);
assertNull(footprint.rejection());
assertEquals(0, footprint.uncontainedWetCells());
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, sampler, CONSTANT_GEOMETRY, course);
assertTrue(drops.connects(100, 100, 100));
assertFalse(drops.connects(100, 99, 100));
for (HydrologyColumnSample column : drops.columns()) {
for (HydrologyColumnLayer layer : column.layers()) {
if (layer.channel() && layer.fluidOwned()) {
int cut = sampler.sample(column.x(), column.z()).naturalHeight() - layer.bedY();
assertTrue("cut=" + cut + " at " + column.x() + "," + column.z(),
cut <= settings.surface().maximumIncision());
}
}
}
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.layer().channel()) {
assertTrue(column.terrain().naturalHeight() - column.layer().bedY()
<= settings.surface().maximumIncision());
}
}
}
private static SurfaceLayerColumn column(SurfaceFootprint footprint, int x, int z) {
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.x() == x && column.z() == z) {
@@ -184,7 +321,7 @@ public class SurfaceFootprintCompilerTest {
}
private static HydraulicSegment segment(long id, HydrologyFeatureType type, int upstream, int downstream, List<HydrologyPoint> centerline) {
return new HydraulicSegment(id, 1L, type, upstream, downstream, 6, 3, false, false, centerline);
return new HydraulicSegment(id, 1L, type, upstream, downstream, 6, 3, false, false, centerline, HydraulicChannelProfile.uniform(6, 3));
}
private static List<HydrologyPoint> points(int fromX, int toX, int y) {
@@ -0,0 +1,56 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydraulicChannelProfile;
import art.arcane.iris.engine.hydrology.HydraulicSegment;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class SurfaceSegmentLabelerTest {
@Test
public void longLevelReachRetainsEveryAcceptedWidthAndDepth() {
double[] widths = new double[]{4D, 5.5D, 8.25D, 6D};
double[] depths = new double[]{2D, 3D, 4.5D, 2.5D};
List<HydraulicSegment> segments = SurfaceSegmentLabeler.label(1L, 2L,
new int[]{0, 10, 20, 30}, new int[]{0, 0, 0, 0}, new int[]{80, 80, 80, 80},
widths, depths, HydrologyPlannerSettings.Banks.defaults());
assertEquals(1, segments.size());
HydraulicSegment segment = segments.getFirst();
assertEquals(HydrologyFeatureType.SURFACE_POOL, segment.type());
assertEquals(9, segment.width());
assertEquals(5, segment.depth());
for (int station = 0; station < widths.length; station++) {
assertEquals(widths[station], segment.channelProfile().widthAt(station), 0D);
assertEquals(depths[station], segment.channelProfile().depthAt(station), 0D);
}
}
@Test
public void storedDimensionsCannotBeMutatedThroughInputOrAccessors() {
double[] widths = new double[]{4D, 6D};
double[] depths = new double[]{2D, 3D};
HydraulicChannelProfile profile = new HydraulicChannelProfile(widths, depths);
widths[0] = 80D;
depths[1] = 90D;
profile.widths()[0] = 100D;
profile.depths()[1] = 100D;
assertEquals(4D, profile.widthAt(0), 0D);
assertEquals(3D, profile.depthAt(1), 0D);
assertEquals(new HydraulicChannelProfile(new double[]{4D, 6D}, new double[]{2D, 3D}), profile);
}
@Test
public void rejectsInvalidCurrentFormatDimensions() {
assertThrows(IllegalArgumentException.class, () ->
new HydraulicChannelProfile(new double[]{4D, 6D}, new double[]{2D}));
assertThrows(IllegalArgumentException.class, () -> HydraulicChannelProfile.uniform(Double.NaN, 2D));
assertThrows(IllegalArgumentException.class, () -> HydraulicChannelProfile.uniform(4D, 0D));
}
}
@@ -23,6 +23,47 @@ public class ValleyProfileSolverTest {
default -> request.minimum();
};
@Test
public void diagonalCourseUsesDistanceInBlocksForItsMinimumLength() {
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 0, 0), new HydrologyPoint(48, 0, 48)));
ChannelProfile channel = new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(),
terrain, CONSTANT_GEOMETRY).build(centerline, "water", false);
ValleyProfile valley = solver(terrain).solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
assertEquals(49, centerline.size());
assertNull(valley.rejection());
}
@Test
public void minimumLengthAcceptsItsExactDistanceAndRejectsOneBlockLess() {
ValleyProfile exact = solve(65, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
ValleyProfile shortCourse = solve(64, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
assertNull(exact.rejection());
assertEquals(HydrologyCandidateRejection.COURSE_TOO_SHORT, shortCourse.rejection());
assertEquals(63, shortCourse.rejectionDetail());
}
@Test
public void minimumLengthExcludesTheUnexposedOceanTerminal() {
HydrologyTerrainSampler terrain = (x, z) -> x >= 48
? HydrologyTerrainSample.ocean(50, "ocean") : HydrologyTerrainSample.openLand(66, 0D, "land");
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(0, 0, 0), new HydrologyPoint(80, 0, 80)));
HydrologyPlannerSettings.Surface surface = HydrologyPlannerSettings.defaults().surface();
ChannelProfile channel = new ChannelProfileBuilder(surface, terrain, CONSTANT_GEOMETRY)
.build(centerline, "water", false);
ValleyProfile valley = new ValleyProfileSolver(surface, terrain, SEA_LEVEL, 67)
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertEquals(HydrologyCandidateRejection.COURSE_TOO_SHORT, valley.rejection());
assertEquals(66, valley.rejectionDetail());
}
@Test
public void flatTerrainHoldsTheHeadAtTheLowestBankByDefault() {
ValleyProfile valley = solve(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
@@ -109,15 +150,16 @@ public class ValleyProfileSolverTest {
}
@Test
public void landAtSeaLevelAlsoEndsTheExposedCourse() {
public void dryLandAtSeaLevelRemainsPartOfTheExposedCourse() {
ValleyProfile valley = solve(300, (x, z) -> x >= 200 ? SEA_LEVEL : 90, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
assertTrue(valley.exposedStations() <= 200);
assertTrue(valley.exposedStations() >= 190);
assertEquals(300, valley.exposedStations());
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations() - 1]);
assertEquals(90, valley.head()[valley.exposedStations() - 1 - HydrologyPlannerSettings.Inlet.defaults().length() * 3 / 2]);
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations()]);
assertEquals(90, valley.head()[100]);
for (int station = 200; station < valley.exposedStations(); station++) {
assertEquals(SEA_LEVEL, valley.head()[station]);
}
}
@Test
@@ -134,6 +176,71 @@ public class ValleyProfileSolverTest {
assertEquals(90, valley.head()[200]);
}
@Test
public void aTwoBlockTerraceBoundsTheRoundedWetPerimeterBeforeTheNose() {
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(x < 150 ? 100 : 98, 0D, "land");
SurfaceCenterline centerline = straight(300);
ChannelProfile channel = channel(300, terrain);
ValleyProfile valley = solver(terrain).solve(centerline, channel, SurfaceTerminal.TRIBUTARY, 40);
assertNull(valley.rejection());
assertEquals(100, valley.head()[130]);
assertEquals(98, valley.head()[149]);
ErosionField field = new ErosionFieldCompiler(HydrologyPlannerSettings.defaults().surface(), terrain, SEA_LEVEL)
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
HydrologyPlannerSettings.Ponds.none());
assertEquals(0, field.uncontainedWetCells());
for (SurfaceColumn column : field.columns().values()) {
if (column.role() != SurfaceRole.CHANNEL) {
assertTrue(column.height() <= column.terrain().naturalHeight());
}
}
}
@Test
public void aRidgeExitUsesTheIncomingWaterHeadToBoundItsLowPerimeter() {
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(
x < 96 ? 120 : x <= 128 ? 152 : 118, 0D, "land");
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), 64, defaults.shoreWidth(), defaults.banks());
SurfaceCenterline centerline = straight(300);
ChannelProfile channel = channel(300, terrain);
ValleyProfile valley = new ValleyProfileSolver(surface, terrain, SEA_LEVEL, 64)
.solve(centerline, channel, SurfaceTerminal.TRIBUTARY, 40);
assertNull(valley.rejection());
assertEquals(120, valley.head()[110]);
assertEquals(118, valley.head()[128]);
assertContained(valley);
ErosionField field = new ErosionFieldCompiler(surface, terrain, SEA_LEVEL)
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
HydrologyPlannerSettings.Ponds.none());
assertEquals(0, field.uncontainedWetCells());
}
@Test
public void aCoastalGrottoEndsTheExposedCourseBeforeItsRoundedCapReachesOcean() {
HydrologyTerrainSampler terrain = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean") : HydrologyTerrainSample.openLand(92, 0D, "land");
SurfaceCenterline centerline = straight(240);
ChannelProfile channel = channel(240, terrain);
ValleyProfile valley = solver(terrain).solve(centerline, channel, SurfaceTerminal.COASTAL_GROTTO, SEA_LEVEL);
assertNull(valley.rejection());
assertTrue(valley.exposedStations() < 240);
assertEquals(92, valley.head()[valley.exposedStations() - 1]);
ErosionField field = new ErosionFieldCompiler(HydrologyPlannerSettings.defaults().surface(), terrain, SEA_LEVEL)
.compile(77L, centerline, channel, valley, SurfaceTerminal.COASTAL_GROTTO, 0,
HydrologyPlannerSettings.Ponds.none());
assertEquals(0, field.uncontainedWetCells());
for (SurfaceColumn column : field.columns().values()) {
assertTrue(column.x() < 240);
assertTrue(column.height() <= column.terrain().naturalHeight());
}
}
@Test
public void submergedCourseIsRejectedAsTooShort() {
ValleyProfile valley = solve(300, (x, z) -> 50, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
@@ -144,7 +144,7 @@ public class MantleHydrologyCaveVoxelViewTest {
@Test
@SuppressWarnings("unchecked")
public void footprintOwnedBedsFillNaturalGapsEvenWhenTheirHeightIsUnchanged() {
public void biomeOnlyShorePreservesNaturalUndercuts() {
Mantle<Matter> mantle = mock(Mantle.class);
Engine engine = configuredEngine(mantle);
IrisComplex complex = undercutTerrain();
@@ -156,6 +156,27 @@ public class MantleHydrologyCaveVoxelViewTest {
RiverFootprint footprint = new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), sample));
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(engine, complex, footprint);
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 50, 0)));
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 70, 0)));
verify(complex).isNaturalTerrainSolid(0, 50, 0);
}
@Test
@SuppressWarnings("unchecked")
public void footprintOwnedBedsFillNaturalGapsEvenWhenTheirHeightIsUnchanged() {
Mantle<Matter> mantle = mock(Mantle.class);
Engine engine = configuredEngine(mantle);
IrisComplex complex = undercutTerrain();
HydrologyColumnSample sample = mock(HydrologyColumnSample.class);
when(sample.x()).thenReturn(0);
when(sample.z()).thenReturn(0);
when(sample.terrainHeight()).thenReturn(80);
HydrologyColumnLayer layer = mock(HydrologyColumnLayer.class);
when(layer.terrainOwned()).thenReturn(true);
when(sample.primarySurfaceLayer()).thenReturn(Optional.of(layer));
RiverFootprint footprint = new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), sample));
MantleHydrologyCaveVoxelView view = new MantleHydrologyCaveVoxelView(engine, complex, footprint);
assertEquals(CaveVoxel.SOLID, view.voxelAt(new CavePosition(0, 50, 0)));
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(1, 50, 0)));
assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(new CavePosition(0, 81, 0)));
@@ -652,6 +652,32 @@ public class MantleHydrologyComponentTest {
}
}
@Test
public void acceptedSurfaceDropAndReceiverPublishTheirGuardsWithoutOwningTheRemainingRiver() {
for (boolean falling : new boolean[]{true, false}) {
HydrologyColumnLayer drop = new HydrologyColumnLayer(
feature(HydrologyFeatureType.WATERFALL, 71L, 14L, 80), 70, 80, 80,
true, false, false, true, falling, !falling, !falling, true, false,
"river", "surface", "mouth", "shore", "dry", "flooded");
HydrologyColumnLayer exposed = layer(HydrologyFeatureType.SURFACE_POOL, 72L, 15L,
76, 80, 80, false, false, "river");
HydrologyColumnSample throat = sample(8, 8, false, 90, List.of(drop));
HydrologyColumnSample downstream = sample(12, 8, false, 90, List.of(exposed));
HydrologyCavePlan plan = acceptedPlan(throat, drop);
MantleHydrologyComponent.Publication publication = compile(samples(throat, downstream),
List.of(plan), new TestCaveVoxelView());
assertEquals(HydrologyCaveAction.SEAL_GUARD,
publication.caveCells().get(new CavePosition(9, 72, 8)).action());
assertEquals(falling ? HydrologyCaveAction.FALLING_FLUID : HydrologyCaveAction.WET_SOURCE,
publication.caveCells().get(new CavePosition(8, 75, 8)).action());
assertEquals(HydrologyCaveAction.WET_SOURCE,
publication.surfaceWrites().get(new CavePosition(12, 78, 8)).action());
assertFalse(publication.caveCells().containsKey(new CavePosition(12, 78, 8)));
}
}
@Test
public void acceptedExactPlanPublishesWetDryAndGuardActions() {
HydrologyColumnLayer pool = layer(
@@ -1607,7 +1633,7 @@ public class MantleHydrologyComponentTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
@@ -83,6 +83,11 @@ public class IrisRiverConfigurationTest {
assertEquals(0.5D, surface.getErosion().getCliffFraction(), 0D);
assertEquals(IrisRiverBedProfile.BOWL, surface.getErosion().getBedProfile());
IrisRiverGeometryConfig geometry = dimension.getHydrology().getRivers().getGeometry();
assertTrue(geometry.getBanks3D().isEnabled());
assertEquals(8D, geometry.getBanks3D().getAmplitude(), 0D);
assertEquals(48D, geometry.getBanks3D().getHorizontalScale(), 0D);
assertEquals(8D, geometry.getBanks3D().getVerticalScale(), 0D);
assertEquals(8, geometry.getBanks3D().getMaximumOverhang());
IrisSurfaceRiverShapeConfig surfaceShape = geometry.getSurface();
assertEquals(2D, surfaceShape.getBedRoundness(), 0D);
assertNull(surfaceShape.getBedRoughness());
@@ -0,0 +1,306 @@
package art.arcane.iris.engine.terrain;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyColumnSnapshot;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.engine.object.IrisRiverBank3DConfig;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyBankTerrainRuntimeTest {
@Test
public void unavailableSupportColumnsDoNotPoisonLaterAcceptedBankGeometry() {
AtomicBoolean ready = new AtomicBoolean();
Terrain3DColumn natural = Terrain3DColumn.unshaped(80, 256);
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> natural,
(x, z) -> x == 0 && z == 0 || ready.get()
? HydrologyColumnSnapshot.ready(bank(50, false, true))
: HydrologyColumnSnapshot.unavailable()),
options(4096), (x, y, z) -> StrictMath.sin((y - 50D) * StrictMath.PI / 4D));
assertTrue(runtime.columnIfReady(0, 0).isEmpty());
assertSame(natural, runtime.column(0, 0));
assertTrue(runtime.columnIfReady(0, 0).isEmpty());
ready.set(true);
Terrain3DColumn accepted = runtime.column(0, 0);
HydrologyBankTerrainRuntime fresh = runtime(4096, (x, z) -> bank(50, false, true), (x, z) -> natural);
assertEquals(fresh.column(0, 0), accepted);
assertEquals(1, accepted.spanCount());
assertTrue(runtime.columnIfReady(0, 0).isPresent());
assertFalse(accepted.isSolid(59));
}
@Test
public void anUnplannedBankIsNotCachedAsAPlannedNaturalColumn() {
AtomicBoolean ready = new AtomicBoolean();
Terrain3DColumn natural = Terrain3DColumn.unshaped(80, 256);
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> natural,
(x, z) -> ready.get() ? HydrologyColumnSnapshot.ready(bank(50, true, true))
: HydrologyColumnSnapshot.unavailable()), options(16), (x, y, z) -> 0D);
assertSame(natural, runtime.column(0, 0));
ready.set(true);
assertEquals(50, runtime.column(0, 0).topY());
}
@Test
public void clearingDuringAColumnSampleCannotRepublishItsOldGeometry() {
AtomicReference<HydrologyBankTerrainRuntime> reference = new AtomicReference<>();
AtomicBoolean clear = new AtomicBoolean(true);
AtomicInteger bed = new AtomicInteger(50);
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> {
if (clear.compareAndSet(true, false)) {
reference.get().clear();
bed.set(60);
}
return Terrain3DColumn.unshaped(80, 256);
}, (x, z) -> HydrologyColumnSnapshot.ready(bank(bed.get(), true, true))),
options(16), (x, y, z) -> 0D);
reference.set(runtime);
assertTrue(runtime.columnIfReady(0, 0).isEmpty());
assertEquals(60, runtime.column(0, 0).topY());
}
@Test
public void configuredProductionNoiseChangesVolumesWhenOnlyVerticalScaleChanges() {
HydrologyBankTerrainRuntime.Sources sources = new HydrologyBankTerrainRuntime.Sources(
(x, z) -> Terrain3DColumn.unshaped(80, 256),
(x, z) -> HydrologyColumnSnapshot.ready(x < 2 ? bank(50, false, true) : null));
IrisRiverBank3DConfig shortLayers = new IrisRiverBank3DConfig().setAmplitude(16)
.setHorizontalScale(24).setVerticalScale(4)
.setDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX));
IrisRiverBank3DConfig tallLayers = new IrisRiverBank3DConfig().setAmplitude(16)
.setHorizontalScale(24).setVerticalScale(64)
.setDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX));
HydrologyBankTerrainRuntime shortRuntime = new HydrologyBankTerrainRuntime(sources,
new HydrologyBankTerrainRuntime.Options(812, 256, 4096, null, shortLayers));
HydrologyBankTerrainRuntime tallRuntime = new HydrologyBankTerrainRuntime(sources,
new HydrologyBankTerrainRuntime.Options(812, 256, 4096, null, tallLayers));
int changed = 0;
int overhangs = 0;
for (int z = -32; z <= 32; z++) {
Terrain3DColumn shortColumn = shortRuntime.column(0, z);
Terrain3DColumn tallColumn = tallRuntime.column(0, z);
changed += shortColumn.equals(tallColumn) ? 0 : 1;
overhangs += shortColumn.spanCount() > 1 ? 1 : 0;
assertTrue(shortColumn.topY() <= 66);
assertTrue(shortColumn.isSolid(50));
}
assertTrue("Changing only Y scale must change configured XYZ terrain", changed > 0);
assertTrue("Configured XYZ noise must produce supported covered shelves", overhangs > 0);
}
@Test
public void disabledAndZeroAmplitudeFieldsDoNotCompileNoise() {
HydrologyBankTerrainRuntime.Sources sources = new HydrologyBankTerrainRuntime.Sources(
(x, z) -> Terrain3DColumn.unshaped(80, 256),
(x, z) -> HydrologyColumnSnapshot.ready(bank(50, false, true)));
for (IrisRiverBank3DConfig config : List.of(
new IrisRiverBank3DConfig().setEnabled(false), new IrisRiverBank3DConfig().setAmplitude(0))) {
config.setDensityStyle(new FailingStyle());
HydrologyBankTerrainRuntime runtime = new HydrologyBankTerrainRuntime(sources,
new HydrologyBankTerrainRuntime.Options(812, 256, 16, null, config));
Terrain3DColumn column = runtime.column(0, 0);
assertEquals(50, column.topY());
assertEquals(1, column.spanCount());
}
}
@Test
public void overlappingBanksPreserveTheirHighestRequiredContainmentFloor() {
HydrologyColumnSample low = bank(50, false, true);
HydrologyColumnSample high = bank(55, false, true);
HydrologyColumnSample overlapping = new HydrologyColumnSample(0, 0, 80, 32, false, "plains",
List.of(low.layers().getFirst(), high.layers().getFirst()));
HydrologyBankTerrainRuntime runtime = runtime(16, (x, z) -> overlapping,
(x, z) -> Terrain3DColumn.unshaped(80, 256));
Terrain3DColumn column = runtime.column(0, 0);
for (int y = 0; y <= 55; y++) {
assertTrue("Overlapping bank floor " + y, column.isSolid(y));
}
}
@Test
public void xyzDensityLeavesSupportedShelvesAboveAnIntactWaterline() {
HydrologyBankTerrainRuntime runtime = runtime(4096,
(x, z) -> x < 2 ? bank(50, false, true) : null,
(x, z) -> Terrain3DColumn.unshaped(80, 256));
Terrain3DColumn column = runtime.column(0, 0);
assertTrue(column.shaped());
assertTrue(column.spanCount() > 1);
assertFalse(column.isSolid(56));
assertTrue(column.isSolid(59));
assertTrue(column.isSolid(60));
assertFalse(column.isSolid(66));
for (int y = 0; y <= 50; y++) {
assertTrue("Protected bank block " + y, column.isSolid(y));
}
for (int y = 81; y < 256; y++) {
assertFalse(column.isSolid(y));
}
}
@Test
public void detachedShelvesAreRemovedWithoutRequestingAnUnboundedNeighbourhood() {
AtomicInteger sampled = new AtomicInteger();
HydrologyBankTerrainRuntime runtime = runtime(4096,
(x, z) -> {
sampled.incrementAndGet();
assertTrue(Math.abs(x) <= 8 && Math.abs(z) <= 8);
return bank(50, false, true);
}, (x, z) -> Terrain3DColumn.unshaped(80, 256));
Terrain3DColumn column = runtime.column(0, 0);
assertEquals(1, column.spanCount());
assertFalse(column.isSolid(58));
assertTrue(sampled.get() <= 33);
}
@Test
public void separateNaturalUpperSpansSurviveWhenTheyStillJoinTheBank() {
Terrain3DColumn natural = Terrain3DColumnFixtures.spans(70, 0, 70, 82, 88);
HydrologyBankTerrainRuntime runtime = runtime(4096,
(x, z) -> x < 2 ? bank(50, false, true) : null,
(x, z) -> x < 2 ? natural : Terrain3DColumn.unshaped(100, 256));
Terrain3DColumn column = runtime.column(0, 0);
assertEquals(88, column.topY());
for (int y = 82; y <= 88; y++) {
assertTrue(column.isSolid(y));
}
assertFalse(column.isSolid(81));
}
@Test
public void unchangedBiomeBandsAndNaturalColumnsSkipDensityAndKeepTheirSpans() {
Terrain3DColumn natural = Terrain3DColumnFixtures.spans(70, 0, 70, 82, 88);
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> natural,
(x, z) -> HydrologyColumnSnapshot.ready(x == 0 ? bank(70, false, false) : null)),
options(16), (x, y, z) -> {
throw new AssertionError("Density sampled outside an eroded bank");
});
assertSame(natural, runtime.column(0, 0));
assertSame(natural, runtime.column(1, 0));
}
@Test
public void wetChannelsRemainOpenAndDoNotSampleBankNoise() {
HydrologyBankTerrainRuntime runtime = HydrologyBankTerrainRuntime.withNoise(
new HydrologyBankTerrainRuntime.Sources((x, z) -> Terrain3DColumn.unshaped(80, 256),
(x, z) -> HydrologyColumnSnapshot.ready(bank(50, true, true))),
options(16), (x, y, z) -> {
throw new AssertionError("Density sampled inside the wet core");
});
Terrain3DColumn column = runtime.column(0, 0);
assertEquals(50, column.topY());
assertTrue(column.isSolid(50));
for (int y = 51; y <= 80; y++) {
assertFalse(column.isSolid(y));
}
}
@Test
public void negativeBoundariesEvictionAndConcurrentQueriesProduceTheSameVolumes() throws Exception {
BiFunction<Integer, Integer, HydrologyColumnSample> hydrology = (x, z) -> Math.floorMod(x, 8) < 6
? bank(50, false, true) : null;
HydrologyBankTerrainRuntime.NaturalSource natural = (x, z) -> Terrain3DColumn.unshaped(80, 256);
HydrologyBankTerrainRuntime reference = runtime(4096, hydrology, natural);
HydrologyBankTerrainRuntime evicted = runtime(2, hydrology, natural);
List<Callable<Terrain3DColumn>> queries = new ArrayList<>();
for (int x = -18; x <= 18; x++) {
int sampleX = x;
queries.add(() -> evicted.column(sampleX, -17));
}
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
List<Future<Terrain3DColumn>> columns = executor.invokeAll(queries);
for (int index = 0; index < columns.size(); index++) {
assertEquals(reference.column(index - 18, -17), columns.get(index).get());
}
}
evicted.clear();
for (int x = 18; x >= -18; x--) {
assertEquals(reference.column(x, -17), evicted.column(x, -17));
}
}
@Test
public void nonfiniteDensityAndInvalidConfigurationFailBeforePublishingGeometry() {
HydrologyBankTerrainRuntime.Sources sources = new HydrologyBankTerrainRuntime.Sources(
(x, z) -> Terrain3DColumn.unshaped(80, 256),
(x, z) -> HydrologyColumnSnapshot.ready(bank(50, false, true)));
HydrologyBankTerrainRuntime invalidNoise = HydrologyBankTerrainRuntime.withNoise(
sources, options(16), (x, y, z) -> Double.NaN);
assertThrows(IllegalStateException.class, () -> invalidNoise.column(0, 0));
assertThrows(IllegalArgumentException.class, () -> new IrisRiverBank3DConfig().setAmplitude(-1).validate());
assertThrows(IllegalArgumentException.class, () -> new IrisRiverBank3DConfig().setVerticalScale(Double.NaN).validate());
assertThrows(IllegalArgumentException.class, () -> new IrisRiverBank3DConfig().setMaximumOverhang(17).validate());
}
private static HydrologyBankTerrainRuntime runtime(int cacheSize,
BiFunction<Integer, Integer, HydrologyColumnSample> hydrology,
HydrologyBankTerrainRuntime.NaturalSource natural) {
return HydrologyBankTerrainRuntime.withNoise(new HydrologyBankTerrainRuntime.Sources(natural,
(x, z) -> HydrologyColumnSnapshot.ready(hydrology.apply(x, z))),
options(cacheSize), (x, y, z) -> StrictMath.sin((y - 50D) * StrictMath.PI / 4D));
}
private static HydrologyBankTerrainRuntime.Options options(int cacheSize) {
IrisRiverBank3DConfig config = new IrisRiverBank3DConfig().setAmplitude(16)
.setHorizontalScale(64).setVerticalScale(64).setMaximumOverhang(8);
return new HydrologyBankTerrainRuntime.Options(812L, 256, cacheSize, null, config);
}
private static HydrologyColumnSample bank(int height, boolean channel, boolean owned) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(1L, HydrologyFeatureType.SURFACE_POOL,
2L, 3L, 0, height, 0, 1, 0, false);
HydrologyColumnLayer layer = new HydrologyColumnLayer(feature, height, height, height,
channel, false, !channel, channel, false, false, owned, channel, false,
"default", "river", "mouth", "shore", "bank", "cave");
return new HydrologyColumnSample(0, 0, 80, 32, false, "plains", List.of(layer));
}
private static final class FailingStyle extends IrisGeneratorStyle {
@Override
public CNG createNoCache(RNG rng, IrisData data) {
throw new AssertionError("Disabled bank geometry compiled a density style");
}
}
}