mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Hydrology fixes
This commit is contained in:
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,
|
||||
|
||||
+88
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-26
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+69
@@ -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));
|
||||
}
|
||||
}
|
||||
+11
@@ -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(
|
||||
|
||||
+70
-69
@@ -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,
|
||||
|
||||
+79
@@ -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));
|
||||
}
|
||||
}
|
||||
+27
-4
@@ -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));
|
||||
}
|
||||
}
|
||||
+71
@@ -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,
|
||||
|
||||
+1
-1
@@ -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),
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+6
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+16
-16
@@ -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))),
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+82
-4
@@ -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),
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
+157
@@ -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));
|
||||
}
|
||||
}
|
||||
+202
@@ -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());
|
||||
}
|
||||
}
|
||||
+138
@@ -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));
|
||||
}
|
||||
}
|
||||
+67
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+124
@@ -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) {
|
||||
}
|
||||
}
|
||||
+40
@@ -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));
|
||||
}
|
||||
}
|
||||
+140
@@ -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());
|
||||
}
|
||||
}
|
||||
+125
@@ -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;
|
||||
}
|
||||
}
|
||||
+234
@@ -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);
|
||||
}
|
||||
}
|
||||
+176
@@ -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);
|
||||
}
|
||||
}
|
||||
+169
@@ -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));
|
||||
}
|
||||
}
|
||||
+107
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-2
@@ -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));
|
||||
|
||||
+473
@@ -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);
|
||||
}
|
||||
}
|
||||
+123
@@ -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) {
|
||||
}
|
||||
}
|
||||
+209
@@ -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());
|
||||
}
|
||||
}
|
||||
+201
@@ -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());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+437
@@ -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));
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+170
@@ -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);
|
||||
}
|
||||
}
|
||||
+28
-14
@@ -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)
|
||||
);
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
+76
-9
@@ -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());
|
||||
|
||||
+194
@@ -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()));
|
||||
}
|
||||
}
|
||||
+6
@@ -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));
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -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
|
||||
|
||||
+58
@@ -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));
|
||||
}
|
||||
}
|
||||
+141
-4
@@ -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) {
|
||||
|
||||
+56
@@ -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));
|
||||
}
|
||||
}
|
||||
+112
-5
@@ -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);
|
||||
|
||||
+22
-1
@@ -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)));
|
||||
|
||||
+27
-1
@@ -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());
|
||||
|
||||
+306
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user