Polish surface rivers: estuaries, ponds, erosion, sea caves

Surface rivers gain a drowned inlet at the mouth, a spring pond and an
inland terminal pond, a configurable erosion profile (thalweg fraction,
blend curve, bed noise, smoothing) and a per-area shore biome width;
banks lose freeboard and inset for a single sink. Underground courses
accept tributaries, coastal grottos can be admitted as standalone sea
caves, and a region or biome can confine the rivers born in it. Tile
planning forks its ring work through HydrologyForkJoin. Pack validation,
the schema, the probes and the tests follow.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 05:03:00 -04:00
1 parent e92c0efe09
commit a4fe527d65
64 files changed
+5619 -622

No files matched your search

@@ -78,6 +78,7 @@ public class PackHydrologyValidatorTest {
"minimumSurfaceCourseLength": 0,
"minimumUndergroundCourseLength": 0,
"maximumOutletsPerTile": 1,
"maximumCoastalOutletsPerTile": 0,
"oceanOutlets": true,
"valleyPreference": 0,
"uphillPenalty": 0,
@@ -88,13 +89,12 @@ public class PackHydrologyValidatorTest {
"channel": {
"width": {"min": 1, "max": 1},
"depth": {"min": 1, "max": 1},
"inset": 0,
"sink": 0,
"maximumIncision": 1,
"roughness": 0,
"roughnessWavelength": 4
},
"banks": {
"freeboard": 0,
"shoreWidth": 0.5,
"blendSlope": 0.5,
"minimumBlendWidth": 1,
@@ -104,7 +104,15 @@ public class PackHydrologyValidatorTest {
"flow": {"cascadeRun": 1, "waterfallMinimumDrop": 2},
"mouths": {"flareRatio": 1, "maximumOceanApron": 0}
},
"underground": {"mouthLevelingDistance": 16}
"underground": {"mouthLevelingDistance": 16},
"grottos": {
"coastal": {
"horizontalRadius": 8,
"verticalRadius": 1,
"headroom": 1,
"seaCaves": {"enabled": false, "maximumPerTile": 0, "minimumSpacing": 16, "minimumCoastHeight": 1, "depth": 0}
}
}
}
""", "\"bankMultiplier\": 0"));
@@ -247,6 +255,7 @@ public class PackHydrologyValidatorTest {
"minimumSurfaceCourseLength": 32768,
"minimumUndergroundCourseLength": 32768,
"maximumOutletsPerTile": 256,
"maximumCoastalOutletsPerTile": 64,
"oceanOutlets": true,
"valleyPreference": 8,
"uphillPenalty": 128,
@@ -257,13 +266,12 @@ public class PackHydrologyValidatorTest {
"channel": {
"width": {"min": 128, "max": 128},
"depth": {"min": 64, "max": 64},
"inset": 3,
"sink": 3,
"maximumIncision": 32,
"roughness": 1,
"roughnessWavelength": 64
},
"banks": {
"freeboard": 4,
"shoreWidth": 6,
"blendSlope": 12,
"minimumBlendWidth": 64,
@@ -273,7 +281,13 @@ public class PackHydrologyValidatorTest {
"flow": {"cascadeRun": 8, "waterfallMinimumDrop": 32},
"mouths": {"flareRatio": 4, "maximumOceanApron": 32}
},
"underground": {"mouthLevelingDistance": 512}
"underground": {"mouthLevelingDistance": 512},
"grottos": {
"coastal": {
"horizontalRadius": 128,
"seaCaves": {"enabled": true, "maximumPerTile": 64, "minimumSpacing": 8192, "minimumCoastHeight": 128, "depth": 128}
}
}
}
""", "\"bankMultiplier\": 4"));
@@ -294,6 +308,7 @@ public class PackHydrologyValidatorTest {
"minimumSurfaceCourseLength": 512,
"minimumUndergroundCourseLength": 512,
"maximumOutletsPerTile": 0,
"maximumCoastalOutletsPerTile": 65,
"valleyPreference": 8.5,
"uphillPenalty": 128.5,
"slopePenalty": 16.5,
@@ -310,6 +325,7 @@ public class PackHydrologyValidatorTest {
"tileSize": 128,
"sampleSpacing": 4,
"maximumRouteLength": 128,
"maximumCoastalOutletsPerTile": -1,
"valleyPreference": -0.5,
"uphillPenalty": -0.5,
"slopePenalty": -0.5,
@@ -326,6 +342,7 @@ public class PackHydrologyValidatorTest {
assertContains(aboveResult.errors(), "minimumSurfaceCourseLength must not exceed maximumRouteLength");
assertContains(aboveResult.errors(), "minimumUndergroundCourseLength must not exceed maximumRouteLength");
assertContains(aboveResult.errors(), "maximumOutletsPerTile must be at least 1");
assertContains(aboveResult.errors(), "maximumCoastalOutletsPerTile must be at most 64");
assertContains(aboveResult.errors(), "valleyPreference must be at most 8.0");
assertContains(aboveResult.errors(), "uphillPenalty must be at most 128.0");
assertContains(aboveResult.errors(), "slopePenalty must be at most 16.0");
@@ -334,6 +351,7 @@ public class PackHydrologyValidatorTest {
assertContains(belowResult.errors(), "tileSize must be at least 256");
assertContains(belowResult.errors(), "sampleSpacing must be at least 8");
assertContains(belowResult.errors(), "maximumRouteLength must be at least 256");
assertContains(belowResult.errors(), "maximumCoastalOutletsPerTile must be at least 0");
assertContains(belowResult.errors(), "valleyPreference must be at least 0.0");
assertContains(belowResult.errors(), "uphillPenalty must be at least 0.0");
assertContains(belowResult.errors(), "slopePenalty must be at least 0.0");
@@ -349,7 +367,7 @@ public class PackHydrologyValidatorTest {
"channel": {
"width": {"min": 20, "max": 2},
"depth": {"min": 2, "max": 65},
"inset": 4,
"sink": 4,
"maximumIncision": 33,
"roughness": 1.5,
"roughnessWavelength": 65,
@@ -366,7 +384,7 @@ public class PackHydrologyValidatorTest {
"channel": {
"width": {"min": 0, "max": 8},
"depth": {"min": 0, "max": 4},
"inset": -1,
"sink": -1,
"maximumIncision": 0,
"roughness": -0.1,
"roughnessWavelength": 3,
@@ -382,7 +400,7 @@ public class PackHydrologyValidatorTest {
assertContains(aboveResult.errors(), "width.min must not exceed max");
assertContains(aboveResult.errors(), "depth.max must be at most 64.0");
assertContains(aboveResult.errors(), "inset must be at most 3");
assertContains(aboveResult.errors(), "sink must be at most 3");
assertContains(aboveResult.errors(), "maximumIncision must be at most 32");
assertContains(aboveResult.errors(), "roughness must be at most 1.0");
assertContains(aboveResult.errors(), "roughnessWavelength must be at most 64");
@@ -392,12 +410,52 @@ public class PackHydrologyValidatorTest {
assertContains(belowResult.errors(), "springLength must be at least 4");
assertContains(belowResult.errors(), "width.min must be at least 1.0");
assertContains(belowResult.errors(), "depth.min must be at least 1.0");
assertContains(belowResult.errors(), "inset must be at least 0");
assertContains(belowResult.errors(), "sink must be at least 0");
assertContains(belowResult.errors(), "maximumIncision must be at least 1");
assertContains(belowResult.errors(), "roughness must be at least 0.0");
assertContains(belowResult.errors(), "roughnessWavelength must be at least 4");
}
@Test
public void rejectsSurfaceErosionAndPondValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"surface": {
"erosion": {"enabled": "yes", "smoothingRadius": 65, "thalwegFraction": 0.96, "blendCurve": 4.5, "bedNoise": 2.5},
"ponds": {"source": {"minimumRadius": 33, "maximumRadius": 33, "depth": 9}, "terminal": {"minimumRadius": 6, "maximumRadius": 5, "depth": 1}}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"surface": {
"erosion": {"smoothingRadius": -1, "thalwegFraction": -0.1, "blendCurve": 0.1, "bedNoise": -1},
"ponds": {"source": {"minimumRadius": 0, "maximumRadius": 0, "depth": 0}}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
assertContains(aboveResult.errors(), "enabled must be a boolean");
assertContains(aboveResult.errors(), "smoothingRadius must be at most 64");
assertContains(aboveResult.errors(), "thalwegFraction must be at most 0.95");
assertContains(aboveResult.errors(), "blendCurve must be at most 4.0");
assertContains(aboveResult.errors(), "bedNoise must be at most 2.0");
assertContains(aboveResult.errors(), "minimumRadius must be at most 32");
assertContains(aboveResult.errors(), "depth must be at most 8");
assertContains(aboveResult.errors(), "ponds.terminal.minimumRadius must not exceed maximumRadius");
assertContains(belowResult.errors(), "smoothingRadius must be at least 0");
assertContains(belowResult.errors(), "thalwegFraction must be at least 0.0");
assertContains(belowResult.errors(), "blendCurve must be at least 0.25");
assertContains(belowResult.errors(), "bedNoise must be at least 0.0");
assertContains(belowResult.errors(), "minimumRadius must be at least 1");
assertContains(belowResult.errors(), "depth must be at least 1");
}
@Test
public void rejectsSurfaceBankValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension("""
@@ -405,7 +463,6 @@ public class PackHydrologyValidatorTest {
"enabled": true,
"surface": {
"banks": {
"freeboard": 5,
"shoreWidth": 6.5,
"blendSlope": 12.5,
"minimumBlendWidth": 40,
@@ -420,7 +477,6 @@ public class PackHydrologyValidatorTest {
"enabled": true,
"surface": {
"banks": {
"freeboard": -1,
"shoreWidth": 0.4,
"blendSlope": 0.4,
"minimumBlendWidth": 0,
@@ -442,12 +498,10 @@ public class PackHydrologyValidatorTest {
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation unorderedResult = validate(unordered);
assertContains(aboveResult.errors(), "freeboard must be at most 4");
assertContains(aboveResult.errors(), "shoreWidth must be at most 6.0");
assertContains(aboveResult.errors(), "blendSlope must be at most 12.0");
assertContains(aboveResult.errors(), "maximumBlendWidth must be at most 64");
assertContains(aboveResult.errors(), "exposeCutStrata must be a boolean");
assertContains(belowResult.errors(), "freeboard must be at least 0");
assertContains(belowResult.errors(), "shoreWidth must be at least 0.5");
assertContains(belowResult.errors(), "blendSlope must be at least 0.5");
assertContains(belowResult.errors(), "minimumBlendWidth must be at least 1");
@@ -461,7 +515,7 @@ public class PackHydrologyValidatorTest {
"enabled": true,
"surface": {
"flow": {"cascadeRun": 9, "waterfallMinimumDrop": 33},
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33}
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33, "inletLength": 257, "inletDepth": 17, "maximumIncision": 129}
}
}
"""));
@@ -470,22 +524,51 @@ public class PackHydrologyValidatorTest {
"enabled": true,
"surface": {
"flow": {"cascadeRun": 0, "waterfallMinimumDrop": 1},
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1}
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1, "inletLength": -1, "inletDepth": -1, "maximumIncision": -1}
}
}
"""));
File longInlet = pack(riversDimension("""
{
"enabled": true,
"routing": {"minimumSurfaceCourseLength": 128},
"surface": {
"mouths": {"inletLength": 200}
}
}
"""));
File fittingInlet = pack(riversDimension("""
{
"enabled": true,
"routing": {"minimumSurfaceCourseLength": 128},
"surface": {
"mouths": {"inletLength": 128, "inletDepth": 0, "maximumIncision": 0}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation longInletResult = validate(longInlet);
PackHydrologyValidator.Validation fittingInletResult = validate(fittingInlet);
assertContains(aboveResult.errors(), "cascadeRun must be at most 8");
assertContains(aboveResult.errors(), "waterfallMinimumDrop must be at most 32");
assertContains(aboveResult.errors(), "flareRatio must be at most 4.0");
assertContains(aboveResult.errors(), "maximumOceanApron must be at most 32");
assertContains(aboveResult.errors(), "inletLength must be at most 256");
assertContains(aboveResult.errors(), "inletDepth must be at most 16");
assertContains(aboveResult.errors(), "mouths.maximumIncision must be at most 128");
assertContains(belowResult.errors(), "cascadeRun must be at least 1");
assertContains(belowResult.errors(), "waterfallMinimumDrop must be at least 2");
assertContains(belowResult.errors(), "flareRatio must be at least 1.0");
assertContains(belowResult.errors(), "maximumOceanApron must be at least 0");
assertContains(belowResult.errors(), "inletLength must be at least 0");
assertContains(belowResult.errors(), "inletDepth must be at least 0");
assertContains(belowResult.errors(), "mouths.maximumIncision must be at least 0");
assertContains(longInletResult.errors(), "mouths.inletLength must not exceed routing.minimumSurfaceCourseLength");
assertTrue(fittingInletResult.errors().toString(),
fittingInletResult.errors().stream().noneMatch(error -> error.contains("inletLength")));
}
@Test
@@ -1153,6 +1236,58 @@ public class PackHydrologyValidatorTest {
assertContains(result.getBlockingErrors(), "tileSize must be divisible by sampleSpacing");
}
@Test
public void rejectsSeaCaveValuesOutsideTheirBoundsAndSpacingBelowTheChamberWidth() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"grottos": {
"coastal": {
"horizontalRadius": 12,
"seaCaves": {"enabled": true, "maximumPerTile": 65, "minimumSpacing": 8193, "minimumCoastHeight": 129, "depth": 129}
}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"grottos": {
"coastal": {
"horizontalRadius": 12,
"seaCaves": {"enabled": "yes", "maximumPerTile": -1, "minimumSpacing": 20, "minimumCoastHeight": 0, "depth": -1}
}
}
}
"""));
File canonical = pack(riversDimension("""
{
"enabled": true,
"grottos": {
"coastal": {
"horizontalRadius": 12,
"seaCaves": {"enabled": true, "maximumPerTile": 3, "minimumSpacing": 160, "minimumCoastHeight": 8, "depth": 12}
}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
PackHydrologyValidator.Validation canonicalResult = validate(canonical);
assertContains(aboveResult.errors(), "seaCaves.maximumPerTile must be at most 64");
assertContains(aboveResult.errors(), "seaCaves.minimumSpacing must be at most 8192");
assertContains(aboveResult.errors(), "seaCaves.minimumCoastHeight must be at most 128");
assertContains(aboveResult.errors(), "seaCaves.depth must be at most 128");
assertContains(belowResult.errors(), "seaCaves.enabled must be a boolean");
assertContains(belowResult.errors(), "seaCaves.maximumPerTile must be at least 0");
assertContains(belowResult.errors(), "seaCaves.minimumSpacing must be at least twice");
assertContains(belowResult.errors(), "seaCaves.minimumCoastHeight must be at least 1");
assertContains(belowResult.errors(), "seaCaves.depth must be at least 0");
assertTrue(canonicalResult.errors().toString(), canonicalResult.errors().isEmpty());
}
private PackHydrologyValidator.Validation validate(File pack) {
File[] dimensions = new File(pack, "dimensions").listFiles(
file -> file.isFile() && file.getName().endsWith(".json"));
@@ -1258,7 +1393,6 @@ public class PackHydrologyValidatorTest {
"roughnessWavelength": 16
},
"banks": {
"freeboard": 1,
"shoreWidth": 1.5,
"blendSlope": 3,
"minimumBlendWidth": 4,
@@ -1291,7 +1425,14 @@ public class PackHydrologyValidatorTest {
"horizontalRadius": 12,
"verticalRadius": 7,
"headroom": 4,
"maximumVolume": 8192
"maximumVolume": 8192,
"seaCaves": {
"enabled": true,
"maximumPerTile": 3,
"minimumSpacing": 160,
"minimumCoastHeight": 8,
"depth": 12
}
},
"inland": {
"enabled": true,
@@ -112,13 +112,25 @@ public class IrisRiverSchemaTest {
assertEquals(32, drops.getJSONObject("maximumBasinDepth").getInt("maximum"));
assertSnippetBackedObjectReference(channel.getJSONObject("width"));
assertSnippetBackedObjectReference(channel.getJSONObject("depth"));
assertEquals(0, channel.getJSONObject("inset").getInt("minimum"));
assertEquals(3, channel.getJSONObject("inset").getInt("maximum"));
assertEquals(0, channel.getJSONObject("sink").getInt("minimum"));
assertEquals(3, channel.getJSONObject("sink").getInt("maximum"));
assertFalse(channel.has("inset"));
assertEquals(32, channel.getJSONObject("maximumIncision").getInt("maximum"));
assertEquals(1D, channel.getJSONObject("roughness").getDouble("maximum"), 0D);
assertEquals(64, channel.getJSONObject("roughnessWavelength").getInt("maximum"));
assertEquals(0, banks.getJSONObject("freeboard").getInt("minimum"));
assertEquals(4, banks.getJSONObject("freeboard").getInt("maximum"));
assertFalse(banks.has("freeboard"));
JSONObject erosion = referencedProperties(definitions, surface.getJSONObject("erosion"));
assertEquals("boolean", erosion.getJSONObject("enabled").getString("type"));
assertEquals(64, erosion.getJSONObject("smoothingRadius").getInt("maximum"));
assertEquals(0.95D, erosion.getJSONObject("thalwegFraction").getDouble("maximum"), 0D);
assertEquals(0.25D, erosion.getJSONObject("blendCurve").getDouble("minimum"), 0D);
assertEquals(2D, erosion.getJSONObject("bedNoise").getDouble("maximum"), 0D);
JSONObject ponds = referencedProperties(definitions, surface.getJSONObject("ponds"));
JSONObject sourcePond = referencedProperties(definitions, ponds.getJSONObject("source"));
JSONObject terminalPond = referencedProperties(definitions, ponds.getJSONObject("terminal"));
assertEquals("boolean", sourcePond.getJSONObject("enabled").getString("type"));
assertEquals(32, sourcePond.getJSONObject("maximumRadius").getInt("maximum"));
assertEquals(8, terminalPond.getJSONObject("depth").getInt("maximum"));
assertEquals(6D, banks.getJSONObject("shoreWidth").getDouble("maximum"), 0D);
assertEquals(0.5D, banks.getJSONObject("blendSlope").getDouble("minimum"), 0D);
assertEquals(12D, banks.getJSONObject("blendSlope").getDouble("maximum"), 0D);
@@ -186,6 +198,9 @@ public class IrisRiverSchemaTest {
assertEquals("array", properties.getJSONObject("bankBiomes").getString("type"));
assertEquals("array", properties.getJSONObject("floodedCaveBiomes").getString("type"));
assertEquals(16D, properties.getJSONObject("widthMultiplier").getDouble("maximum"), 0D);
assertEquals(32D, properties.getJSONObject("shoreBiomeWidth").getDouble("maximum"), 0D);
assertEquals(0D, properties.getJSONObject("shoreBiomeWidth").getDouble("minimum"), 0D);
assertEquals("boolean", properties.getJSONObject("confined").getString("type"));
assertEquals(0D, properties.getJSONObject("incisionMultiplier").getDouble("minimum"), 0D);
assertEquals(64D, properties.getJSONObject("routingMultiplier").getDouble("maximum"), 0D);
assertEquals("#/definitions/erzbiomes",
@@ -6,6 +6,7 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
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.cave.HydrologyCavePlan;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlannerSettings;
import org.junit.Test;
@@ -1067,6 +1068,158 @@ public class HydrologyCaveCourseFilterTest {
assertEquals(1, diagnostics.size());
}
@Test
public void seaCaveIsContainedWithItsSeaFaceAsTheOnlyOpening() {
HydrologyTerrainSampler coast = (int x, int z) -> {
if (x >= 112) {
return HydrologyTerrainSample.ocean(48, "ocean");
}
if (z >= 9) {
return HydrologyTerrainSample.openLand(64, 0D, "beach");
}
if (x >= 110) {
return HydrologyTerrainSample.openLand(74, 0D, "face");
}
if (x >= 104) {
return HydrologyTerrainSample.openLand(66, 0D, "low");
}
return HydrologyTerrainSample.openLand(92, 0D, "cliff");
};
HydraulicSegment chamber = new HydraulicSegment(
921L,
920L,
HydrologyFeatureType.COASTAL_GROTTO,
63,
63,
4,
2,
false,
false,
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
);
RiverCourse seaCave = new RiverCourse(
920L,
RiverCourseType.SEA_CAVE,
OptionalLong.empty(),
OptionalLong.empty(),
"water",
1,
List.of(),
List.of(chamber)
);
RiverFootprint footprint = new HydrologyFootprintCompiler(
seaCaveSettings(),
coast,
request -> request.minimum()
).compile(List.of(seaCave));
HydrologyTerrainCaveVoxelView view = new HydrologyTerrainCaveVoxelView(coast, 63, -64, 320);
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
HydrologyCaveCourseFilter terrainFilter = new HydrologyCaveCourseFilter(
view,
new HydrologyCaveCourseFilter.Options(true, 32768, 32768)
);
HydrologyCaveCourseFilter.Result result = terrainFilter.filter(
List.of(),
List.of(),
List.of(),
List.of(seaCave),
footprint.columns().values(),
diagnostics
);
assertEquals("diagnostics=" + diagnostics, List.of(seaCave), result.courses());
assertTrue(diagnostics.isEmpty());
assertEquals(1, result.cavePlans().size());
HydrologyCavePlan plan = result.cavePlans().getFirst();
assertTrue(plan.accepted());
assertEquals(920L, plan.source().sourceId());
AtomicInteger seaOpenings = new AtomicInteger();
plan.forEachPrecondition((CavePosition position, CaveVoxelPrecondition precondition) -> {
if (position.x() >= 112
&& position.y() > 48
&& position.y() <= 63
&& precondition.voxel() == CaveVoxel.COMPATIBLE_FLUID) {
seaOpenings.incrementAndGet();
}
});
assertTrue(seaOpenings.get() > 0);
plan.forEachAction((CavePosition position, HydrologyCaveAction action) -> {
int naturalHeight = coast.sample(position.x(), position.z()).naturalHeight();
assertTrue(
"carve above the surface at " + position + " " + action,
action == HydrologyCaveAction.SEAL_GUARD ? position.y() <= naturalHeight : position.y() < naturalHeight
);
});
ArrayList<HydrologyColumnSample> sealed = new ArrayList<>();
for (HydrologyColumnSample sample : footprint.columns().values()) {
List<HydrologyColumnLayer> kept = sample.layers().stream()
.filter((HydrologyColumnLayer layer) -> !layer.oceanApron())
.toList();
if (!kept.isEmpty()) {
sealed.add(new HydrologyColumnSample(
sample.x(),
sample.z(),
sample.naturalHeight(),
sample.seaLevel(),
sample.ocean(),
sample.parentBiomeKey(),
kept
));
}
}
ArrayList<HydrologyDiagnosticCandidate> sealedDiagnostics = new ArrayList<>();
HydrologyCaveCourseFilter.Result control = terrainFilter.filter(
List.of(),
List.of(),
List.of(),
List.of(seaCave),
sealed,
sealedDiagnostics
);
assertTrue(control.courses().isEmpty());
assertTrue(control.cavePlans().isEmpty());
assertEquals(1, sealedDiagnostics.size());
assertEquals(HydrologyCandidateRejection.CAVE_CONTAINMENT, sealedDiagnostics.getFirst().rejection());
}
private static HydrologyPlannerSettings seaCaveSettings() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768),
base.outlets().inlandGrotto(),
base.outlets().surfaceSinkholesEnabled(),
base.outlets().coastalCliffMinimumHeight(),
base.outlets().mouthLevelingDistance(),
8,
base.outlets().maximumPerTile(),
base.outlets().maximumPerTile()
);
HydrologyPlannerSettings.Geometry geometry = new HydrologyPlannerSettings.Geometry(
base.geometry().meanders(),
base.geometry().surface(),
base.geometry().underground(),
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11),
base.geometry().drops()
);
return new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
base.surface(),
base.hydraulics(),
base.underground(),
outlets,
geometry,
base.deepFluids(),
base.surfacePools(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
private RiverCourse course(HydraulicSegment segment) {
return new RiverCourse(
segment.courseId(),
@@ -20,10 +20,10 @@ public class HydrologyFootprintCompilerTest {
/**
* Published dry cells cardinally adjacent to owned water whose final terrain sits below the
* water head plus the configured freeboard.
* water head plus the configured sink.
*/
private static int unsupportedBankCells(RiverFootprint footprint, long courseId) {
int freeboard = HydrologyPlannerSettings.defaults().surface().banks().freeboard();
int sink = HydrologyPlannerSettings.defaults().surface().banks().sink();
int unsupported = 0;
for (HydrologyColumnSample sample : footprint.columns().values()) {
HydrologyColumnLayer fluid = sample.primarySurfaceFluidLayer().orElse(null);
@@ -40,7 +40,7 @@ public class HydrologyFootprintCompilerTest {
if (neighbor == null || neighbor.primarySurfaceFluidLayer().isPresent()) {
continue;
}
if (neighbor.terrainHeight() < fluid.fluidHeadY() + freeboard) {
if (neighbor.terrainHeight() < fluid.fluidHeadY() + sink) {
unsupported++;
}
}
@@ -213,7 +213,7 @@ public class HydrologyFootprintCompilerTest {
}
@Test
public void surfaceChannelAtTerrainLevelHasNoContainingBank() {
public void surfaceChannelAtTerrainLevelIsHeldByTheGroundBesideIt() {
HydraulicSegment segment = new HydraulicSegment(
15L,
14L,
@@ -231,7 +231,7 @@ public class HydrologyFootprintCompilerTest {
HydrologyFootprintCompiler compiler = compiler(terrain);
RiverFootprint footprint = compiler.compile(List.of(course));
assertTrue(unsupportedBankCells(footprint, 14L) > 0);
assertEquals(0, unsupportedBankCells(footprint, 14L));
for (HydrologyColumnSample sample : footprint.columns().values()) {
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
}
@@ -268,7 +268,9 @@ public class HydrologyFootprintCompilerTest {
base.underground(),
base.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(deepFluid), List.of()
List.of(deepFluid), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydraulicSegment segment = new HydraulicSegment(
15L,
@@ -717,7 +719,7 @@ public class HydrologyFootprintCompilerTest {
2,
false,
false,
List.of(new HydrologyPoint(32, 40, 0))
List.of(new HydrologyPoint(96, 40, 0))
)
);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
@@ -1434,13 +1436,13 @@ public class HydrologyFootprintCompilerTest {
if (neighbor.primarySurfaceFluidLayer().isPresent()) {
continue;
}
assertTrue(neighbor.terrainHeight() > fluid.fluidHeadY());
assertTrue(neighbor.terrainHeight() >= fluid.fluidHeadY());
}
}
}
@Test
public void unsupportedWetEdgeIsReportedWithoutRaisingItsNaturalBank() {
public void aOneBlockLowBankIsLippedAndADeeperOneIsReportedWithoutRaising() {
HydraulicSegment channel = new HydraulicSegment(
529L,
528L,
@@ -1464,21 +1466,33 @@ public class HydrologyFootprintCompilerTest {
}
int lowBankZ = bankZ;
HydrologyTerrainSampler lowBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
x == 12 && z == lowBankZ ? 70 : 82,
x == 12 && z == lowBankZ ? 69 : 82,
0D,
"parent"
);
HydrologyFootprintCompiler lowBankCompiler = compiler(lowBankTerrain);
RiverFootprint lowBankFootprint = lowBankCompiler.compile(List.of(course));
HydrologyColumnSample lowBank = lowBankFootprint.sample(12, lowBankZ).orElseThrow();
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
x == 12 && z == lowBankZ ? 68 : 82,
0D,
"parent"
);
RiverFootprint deepBankFootprint = compiler(deepBankTerrain).compile(List.of(course));
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
assertEquals(70, lowBank.naturalHeight());
// One block short of the water: a one-block lip holds it and nothing is reported.
assertEquals(69, lowBank.naturalHeight());
assertEquals(70, lowBank.terrainHeight());
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
assertTrue(sample.terrainHeight() <= sample.naturalHeight() + 1);
}
// Two blocks short: the bank keeps its natural height and the edge is reported.
assertEquals(68, deepBank.naturalHeight());
assertEquals(68, deepBank.terrainHeight());
assertTrue(unsupportedBankCells(deepBankFootprint, 528L) > 0);
}
@Test
@@ -1731,6 +1745,250 @@ public class HydrologyFootprintCompilerTest {
@Test
public void coastalGrottoApronReachesTheChamberRadius() {
HydrologyTerrainSampler coast = (int x, int z) -> x >= 112
? HydrologyTerrainSample.ocean(48, "ocean")
: HydrologyTerrainSample.openLand(80, 0D, "parent");
RiverCourse seaCave = seaCaveCourse(900L);
RiverFootprint footprint = new HydrologyFootprintCompiler(
seaCaveSettings(8, 12),
coast,
request -> request.minimum()
).compile(List.of(seaCave));
RiverFootprint wideApron = new HydrologyFootprintCompiler(
seaCaveSettings(12, 12),
coast,
request -> request.minimum()
).compile(List.of(seaCave));
int chamberColumns = 0;
int apronColumns = 0;
double farthestApron = 0D;
for (HydrologyColumnSample sample : footprint.columns().values()) {
for (HydrologyColumnLayer layer : sample.layers()) {
if (layer.oceanApron()) {
apronColumns++;
assertTrue(sample.ocean());
farthestApron = Math.max(farthestApron, StrictMath.hypot(sample.x() - 112, sample.z()));
assertTrue(
"apron beyond the chamber at " + sample.x() + "," + sample.z(),
nearestCenterlineDistance(seaCave, sample.x(), sample.z()) <= 12.25D
|| touchesChamber(footprint, sample.x(), sample.z())
);
continue;
}
if (!chamberLayer(layer)) {
continue;
}
chamberColumns++;
assertFalse(sample.ocean());
for (int[] cardinal : CARDINALS) {
int neighborX = sample.x() + cardinal[0];
int neighborZ = sample.z() + cardinal[1];
if (!coast.sample(neighborX, neighborZ).ocean()) {
continue;
}
assertTrue(
"sea column " + neighborX + "," + neighborZ + " beside the chamber has no apron",
apronAt(footprint, neighborX, neighborZ)
);
}
}
}
assertTrue(chamberColumns > 0);
assertTrue(apronColumns > 0);
assertTrue("farthest apron " + farthestApron, farthestApron > 8.25D);
assertEquals(wideApron, footprint);
HydraulicSegment mouth = new HydraulicSegment(
902L,
901L,
HydrologyFeatureType.MOUTH,
63,
63,
8,
3,
false,
false,
List.of(new HydrologyPoint(96, 63, 0), new HydrologyPoint(112, 63, 0))
);
RiverFootprint mouthFootprint = new HydrologyFootprintCompiler(
seaCaveSettings(8, 12),
coast,
request -> request.minimum()
).compile(List.of(course(901L, RiverCourseType.SURFACE, mouth)));
assertTrue(mouthFootprint.sample(112, 0).isPresent());
assertTrue(mouthFootprint.sample(112 + 8, 0).isEmpty());
for (HydrologyColumnSample sample : mouthFootprint.columns().values()) {
if (sample.ocean()) {
assertTrue(sample.x() - 112 < 8);
}
}
}
@Test
public void seaCaveChamberStaysUnderTheNaturalSurface() {
HydrologyTerrainSampler coast = this::cliffCoast;
RiverCourse seaCave = seaCaveCourse(910L);
RiverFootprint footprint = new HydrologyFootprintCompiler(
seaCaveSettings(8, 12),
coast,
request -> request.minimum()
).compile(List.of(seaCave));
int chamberColumns = 0;
for (HydrologyColumnSample sample : footprint.columns().values()) {
for (HydrologyColumnLayer layer : sample.layers()) {
if (!chamberLayer(layer)) {
continue;
}
chamberColumns++;
String where = "chamber at " + sample.x() + "," + sample.z() + " " + layer;
assertFalse(where, sample.ocean());
assertTrue(where, sample.naturalHeight() != 64);
assertEquals(where, 63, layer.fluidHeadY());
assertTrue(where, layer.bedY() < 63);
assertTrue(where, layer.ceilingY() >= 64);
assertTrue(where, layer.ceilingY() <= sample.naturalHeight() - 1);
for (int[] cardinal : CARDINALS) {
HydrologyTerrainSample neighbor = coast.sample(sample.x() + cardinal[0], sample.z() + cardinal[1]);
if (neighbor.ocean()) {
continue;
}
assertTrue(where, layer.ceilingY() <= neighbor.naturalHeight() - 1);
}
}
}
assertTrue(chamberColumns > 0);
HydrologyColumnLayer deepRoof = chamberLayerAt(footprint, 100, 0);
assertTrue(deepRoof != null);
assertEquals(73, deepRoof.ceilingY());
HydrologyColumnLayer lowRoof = chamberLayerAt(footprint, 108, 0);
assertTrue(lowRoof != null);
assertEquals(65, lowRoof.ceilingY());
HydrologyColumnLayer face = chamberLayerAt(footprint, 111, 0);
assertTrue(face != null);
assertTrue(face.ceilingY() >= 64 && face.ceilingY() <= 73);
assertTrue(chamberLayerAt(footprint, 111, 9) == null);
assertTrue(chamberLayerAt(footprint, 100, 9) == null);
}
private HydrologyTerrainSample cliffCoast(int x, int z) {
if (x >= 112) {
return HydrologyTerrainSample.ocean(48, "ocean");
}
if (z >= 9) {
return HydrologyTerrainSample.openLand(64, 0D, "beach");
}
if (x >= 110) {
return HydrologyTerrainSample.openLand(74, 0D, "face");
}
if (x >= 104) {
return HydrologyTerrainSample.openLand(66, 0D, "low");
}
return HydrologyTerrainSample.openLand(92, 0D, "cliff");
}
private RiverCourse seaCaveCourse(long id) {
HydraulicSegment chamber = new HydraulicSegment(
id + 1L,
id,
HydrologyFeatureType.COASTAL_GROTTO,
63,
63,
4,
2,
false,
false,
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
);
return new RiverCourse(
id,
RiverCourseType.SEA_CAVE,
OptionalLong.empty(),
OptionalLong.empty(),
"water",
1,
List.of(),
List.of(chamber)
);
}
private static HydrologyPlannerSettings seaCaveSettings(int maximumOceanApron, int chamberRadius) {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(true, chamberRadius, 7, 10, 32768),
base.outlets().inlandGrotto(),
base.outlets().surfaceSinkholesEnabled(),
base.outlets().coastalCliffMinimumHeight(),
base.outlets().mouthLevelingDistance(),
maximumOceanApron,
base.outlets().maximumPerTile(),
base.outlets().maximumPerTile()
);
HydrologyPlannerSettings.Geometry geometry = new HydrologyPlannerSettings.Geometry(
base.geometry().meanders(),
base.geometry().surface(),
base.geometry().underground(),
new HydrologyPlannerSettings.ChannelShape(2.4D, 0D, 0D, 11),
base.geometry().drops()
);
return new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
base.surface(),
base.hydraulics(),
base.underground(),
outlets,
geometry,
base.deepFluids(),
base.surfacePools(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
private static boolean chamberLayer(HydrologyColumnLayer layer) {
return layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO
&& layer.terrainOwned()
&& !layer.oceanApron();
}
private HydrologyColumnLayer chamberLayerAt(RiverFootprint footprint, int x, int z) {
return footprint.sample(x, z)
.flatMap((HydrologyColumnSample sample) -> sample.layers().stream()
.filter(HydrologyFootprintCompilerTest::chamberLayer)
.findFirst())
.orElse(null);
}
private boolean apronAt(RiverFootprint footprint, int x, int z) {
return footprint.sample(x, z)
.map((HydrologyColumnSample sample) -> sample.layers().stream().anyMatch(HydrologyColumnLayer::oceanApron))
.orElse(false);
}
private boolean touchesChamber(RiverFootprint footprint, int x, int z) {
for (int[] cardinal : CARDINALS) {
if (chamberLayerAt(footprint, x + cardinal[0], z + cardinal[1]) != null) {
return true;
}
}
return false;
}
private double nearestCenterlineDistance(RiverCourse course, int x, int z) {
double nearest = Double.MAX_VALUE;
for (HydraulicSegment segment : course.segments()) {
for (HydrologyPoint point : segment.centerline()) {
nearest = Math.min(nearest, StrictMath.hypot(x - point.x(), z - point.z()));
}
}
return nearest;
}
private RiverCourse course(long id, RiverCourseType type, HydraulicSegment segment) {
return new RiverCourse(
id,
@@ -0,0 +1,109 @@
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.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyForkJoinTest {
@Test
public void nestedFanOutCompletesOnASaturatedForkJoinPool() throws Exception {
ForkJoinPool pool = new ForkJoinPool(1);
try {
List<Integer> results = pool.submit(() -> {
List<Callable<Integer>> outer = new ArrayList<>();
for (int index = 0; index < 3; index++) {
int value = index;
outer.add(() -> {
List<Callable<Integer>> inner = List.of(() -> value * 10, () -> value * 10 + 1);
List<Integer> nested = HydrologyForkJoin.invokeAll(inner, Runnable::run);
return nested.get(0) + nested.get(1);
});
}
return HydrologyForkJoin.invokeAll(outer, Runnable::run);
}).get(10, TimeUnit.SECONDS);
assertEquals(List.of(1, 21, 41), results);
} finally {
pool.shutdownNow();
}
}
@Test
public void outsideAPoolTasksRunOnTheFallbackExecutorInOrder() throws Exception {
List<String> threads = Collections.synchronizedList(new ArrayList<>());
ExecutorService executor = Executors.newFixedThreadPool(2, runnable -> new Thread(runnable, "fallback-executor"));
try {
List<Callable<Integer>> tasks = new ArrayList<>();
for (int index = 0; index < 4; index++) {
int value = index;
tasks.add(() -> {
threads.add(Thread.currentThread().getName());
return value;
});
}
List<Integer> results = HydrologyForkJoin.invokeAll(tasks, executor);
assertEquals(List.of(0, 1, 2, 3), results);
assertEquals(4, threads.size());
assertTrue(threads.stream().allMatch("fallback-executor"::equals));
} finally {
executor.shutdownNow();
}
}
@Test
public void withoutAFallbackTasksRunOnTheCaller() {
String caller = Thread.currentThread().getName();
List<Callable<String>> tasks = List.of(
() -> Thread.currentThread().getName(),
() -> Thread.currentThread().getName()
);
assertEquals(List.of(caller, caller), HydrologyForkJoin.invokeAll(tasks, null));
}
@Test
public void aFailingTaskRethrowsItsExceptionOnEveryPath() throws Exception {
List<Callable<Integer>> tasks = List.of(() -> 1, () -> {
throw new IllegalStateException("planning failed");
});
IllegalStateException inline = assertThrows(IllegalStateException.class, () -> HydrologyForkJoin.invokeAll(tasks, null));
assertEquals("planning failed", inline.getMessage());
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
IllegalStateException pooled = assertThrows(IllegalStateException.class, () -> HydrologyForkJoin.invokeAll(tasks, executor));
assertEquals("planning failed", pooled.getMessage());
} finally {
executor.shutdownNow();
}
ForkJoinPool pool = new ForkJoinPool(1);
try {
String message = pool.submit(() -> {
try {
HydrologyForkJoin.invokeAll(tasks, Runnable::run);
return "no failure";
} catch (IllegalStateException failure) {
return failure.getMessage();
}
}).get(10, TimeUnit.SECONDS);
assertEquals("planning failed", message);
} finally {
pool.shutdownNow();
}
}
}
@@ -185,6 +185,68 @@ public class HydrologyModelTest {
));
}
@Test
public void seaCaveCoursesAreIndependent() {
HydraulicSegment chamber = new HydraulicSegment(
1L,
21L,
HydrologyFeatureType.COASTAL_GROTTO,
63,
63,
4,
2,
false,
false,
List.of(new HydrologyPoint(100, 63, 0), new HydrologyPoint(112, 63, 0))
);
RiverCourse seaCave = new RiverCourse(
21L,
RiverCourseType.SEA_CAVE,
OptionalLong.empty(),
OptionalLong.empty(),
"water",
1,
List.of(),
List.of(chamber)
);
assertEquals(RiverCourseType.SEA_CAVE, seaCave.type());
assertTrue(seaCave.sourceNodeId().isEmpty());
assertTrue(seaCave.outletId().isEmpty());
assertFalse(seaCave.surfaceSinkholeContinuation());
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
21L,
RiverCourseType.SEA_CAVE,
OptionalLong.of(4L),
OptionalLong.empty(),
"water",
1,
List.of(),
List.of(chamber)
));
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
21L,
RiverCourseType.SEA_CAVE,
OptionalLong.empty(),
OptionalLong.of(5L),
"water",
1,
List.of(),
List.of(chamber)
));
assertThrows(IllegalArgumentException.class, () -> new RiverCourse(
21L,
RiverCourseType.SEA_CAVE,
OptionalLong.empty(),
OptionalLong.empty(),
"water",
1,
List.of(),
List.of()
));
}
@Test
public void oceanColumnsRejectOwnedOrElevatedWrites() {
HydrologyFeatureRef feature = feature(HydrologyFeatureType.MOUTH, 7L, 63);
@@ -0,0 +1,132 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* A 256-block tile whose western half is one region and whose eastern half is another. The only sea is
* along the south edge of the eastern half, plus an optional pocket of sea in the south-western corner;
* terrain falls to the south and slightly to the east, so rivers born in the west naturally want to
* cross into the east.
*/
public class HydrologyPlannerConfinementTest {
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
private static final int BOUNDARY = 128;
private static final String WEST = "region:west";
@Test
public void unconfinedWestSourcesMayDrainAcrossTheRegionBoundary() {
HydrologyTile tile = new HydrologyPlanner(91L, settings(), terrain(false, true)).plan(TILE);
List<RiverCourse> westCourses = westCourses(tile);
assertFalse("diagnostics=" + tile.diagnosticCandidates(), westCourses.isEmpty());
assertTrue("no western course crossed into the east: " + describe(westCourses),
westCourses.stream().anyMatch(course -> maximumX(course) >= BOUNDARY));
}
@Test
public void confinedWestSourcesStayWestAllTheWayToTheirOutlet() {
HydrologyTile tile = new HydrologyPlanner(91L, settings(), terrain(true, true)).plan(TILE);
List<RiverCourse> westCourses = westCourses(tile);
assertFalse("diagnostics=" + tile.diagnosticCandidates(), westCourses.isEmpty());
for (RiverCourse course : westCourses) {
assertTrue("western course left its region: " + describe(List.of(course)), maximumX(course) < BOUNDARY);
}
}
@Test
public void confinedWestSourcesWithoutAnOutletInsideAreRejectedAndSayWhy() {
HydrologyTile tile = new HydrologyPlanner(91L, settings(), terrain(true, false)).plan(TILE);
assertTrue(describe(westCourses(tile)), westCourses(tile).isEmpty());
assertTrue("diagnostics=" + tile.diagnosticCandidates(), tile.diagnosticCandidates().stream()
.anyMatch(candidate -> candidate.rejection() == HydrologyCandidateRejection.CONFINED_NO_OUTLET));
}
private static List<RiverCourse> westCourses(HydrologyTile tile) {
ArrayList<RiverCourse> courses = new ArrayList<>();
for (RiverCourse course : tile.courses()) {
if (course.type() == RiverCourseType.SURFACE && sourceX(course) < BOUNDARY) {
courses.add(course);
}
}
return courses;
}
private static int sourceX(RiverCourse course) {
return course.segments().getFirst().centerline().getFirst().x();
}
private static int maximumX(RiverCourse course) {
int maximum = Integer.MIN_VALUE;
for (HydraulicSegment segment : course.segments()) {
for (HydrologyPoint point : segment.centerline()) {
maximum = Math.max(maximum, point.x());
}
}
return maximum;
}
private static String describe(List<RiverCourse> courses) {
StringBuilder out = new StringBuilder();
for (RiverCourse course : courses) {
out.append(" course ").append(course.id()).append(" source x=").append(sourceX(course))
.append(" max x=").append(maximumX(course));
}
return out.toString();
}
private static HydrologyTerrainSampler terrain(boolean confineWest, boolean westPocket) {
return (int x, int z) -> {
String confines = confineWest && x < BOUNDARY ? WEST : null;
boolean eastSea = x >= BOUNDARY && z < 24;
boolean pocket = westPocket && x < 24 && z < 24;
if (eastSea || pocket) {
return sample(60, true, confines);
}
int height = (int) StrictMath.round(66D + z * 0.3D - x * 0.05D);
return sample(height, false, confines);
};
}
private static HydrologyTerrainSample sample(int height, boolean ocean, String confines) {
return new HydrologyTerrainSample(
height, 1D, 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
);
}
private static HydrologyPlannerSettings settings() {
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(true, 4D, 80, 1, 6, 32);
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(false, 0D, Integer.MIN_VALUE, 0, 1, 48);
HydrologyPlannerSettings.ChannelShape stableChannel = new HydrologyPlannerSettings.ChannelShape(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.Surface(true, surfaceSources, 4, 12, 2, 4, 12, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(false, undergroundSources, 68, 82, 4, 14, 2, 5, 6, 12, false, 0),
new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
false, 12, 48, 2, 8, 8),
new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
stableChannel, stableChannel, stableChannel,
HydrologyPlannerSettings.Geometry.defaults().drops()),
List.of(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
}
@@ -5,7 +5,10 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelPrecondition;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveConflictPolicy;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import org.junit.Test;
import java.util.ArrayList;
@@ -17,6 +20,7 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
private static final long SEED = 77L;
@@ -65,16 +69,46 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
assertEquals(forward.get(pair.first()).diagnosticCandidates().toString(), 1, firstSources.size());
assertEquals(forward.get(pair.second()).diagnosticCandidates().toString(), 1, secondSources.size());
assertPointNear(new HydrologyPoint(32, 121, -272), firstSources.getFirst(), 8);
assertPointNear(new HydrologyPoint(16, 126, -192), secondSources.getFirst(), 8);
// The second tile keeps the mouth on its own coast, so its heaviest required source no longer
// competes with the first tile's network and is accepted ahead of the lighter fallback source.
assertPointNear(new HydrologyPoint(16, 126, -224), secondSources.getFirst(), 8);
assertNoIncompatibleCaveOverlaps(pair, forward.get(pair.first()), forward.get(pair.second()));
}
@Test
public void boundPlatformResolvesNeighbourOwnersInParallelToTheSameTiles() {
List<HydrologyTileKey> corner = List.of(
new HydrologyTileKey(-1, -1),
new HydrologyTileKey(0, -1),
new HydrologyTileKey(-1, 0),
new HydrologyTileKey(0, 0)
);
Map<HydrologyTileKey, HydrologyTile> lazy = plans(planner(), corner);
IrisSettings previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisPlatforms.unbind();
IrisPlatforms.bind(mock(IrisPlatform.class));
Map<HydrologyTileKey, HydrologyTile> parallel;
try {
parallel = plans(planner(), corner);
} finally {
IrisPlatforms.unbind();
IrisSettings.settings = previousSettings;
}
assertEquals(lazy, parallel);
}
@Test
public void colorRankBoundsTheCompleteOwnerDependencyGeometry() {
HydrologyPlanner planner = planner();
assertEquals(1, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(0, 0)));
assertEquals(49, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(-1, -1)));
// The mouth flare reaches maximumWidth * flareRatio / 2 beside the coast, which pushes these
// 256-block tiles to a three-colour period: rank 8 at (-1, -1), two tiles of offset per rank.
assertEquals(3, settings().crossTileColorPeriod());
assertEquals(1089, planner.maximumCrossTileDependencyOwners(new HydrologyTileKey(-1, -1)));
}
@Test
@@ -275,7 +309,8 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
5,
6,
12,
true
true,
0
),
new HydrologyPlannerSettings.Outlets(
true,
@@ -285,10 +320,13 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
12,
48,
2,
8,
8
),
stableGeometry(),
List.of(deepFluid), List.of()
List.of(deepFluid), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -349,7 +387,8 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
baseUnderground.maximumDepth(),
baseUnderground.minimumHeadroom(),
baseUnderground.maximumHeadroom(),
baseUnderground.connectToExistingCaves()
baseUnderground.connectToExistingCaves(),
0
),
new HydrologyPlannerSettings.Outlets(
base.outlets().oceanEnabled(),
@@ -359,10 +398,13 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
base.outlets().coastalCliffMinimumHeight(),
base.outlets().mouthLevelingDistance(),
base.outlets().maximumOceanApron(),
base.outlets().maximumPerTile(),
base.outlets().maximumPerTile()
),
base.geometry(),
base.deepFluids(), List.of()
base.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -404,7 +446,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
"shore",
"dry",
"flooded",
List.of("water"), List.of()
List.of("water"), List.of(),
Double.NaN,
null
);
};
}
@@ -445,7 +489,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
sample.shoreBiomeKey(),
sample.bankBiomeKey(),
sample.floodedCaveBiomeKey(),
sample.preferredProfileKeys(), List.of()
sample.preferredProfileKeys(), List.of(),
Double.NaN,
null
);
};
}
@@ -478,7 +524,9 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
"shore",
"dry",
"flooded",
List.of("water"), List.of()
List.of("water"), List.of(),
Double.NaN,
null
);
}
@@ -667,8 +667,10 @@ public class HydrologyPlannerGlobalInvariantTest {
0.5D,
12D,
0.5D,
0.1D
, 1D, 0),
0.1D,
1D,
0
),
new HydrologyPlannerSettings.Surface(
surfaceEnabled,
surfaceSources,
@@ -691,7 +693,8 @@ public class HydrologyPlannerGlobalInvariantTest {
2,
2,
4,
false
false,
0
),
new HydrologyPlannerSettings.Outlets(
true,
@@ -701,10 +704,13 @@ public class HydrologyPlannerGlobalInvariantTest {
32,
16,
1,
8,
8
),
HydrologyPlannerSettings.Geometry.defaults(),
deepFluids, List.of()
deepFluids, List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -748,11 +754,14 @@ public class HydrologyPlannerGlobalInvariantTest {
underground.maximumDepth(),
underground.minimumHeadroom(),
underground.maximumHeadroom(),
underground.connectToExistingCaves()
underground.connectToExistingCaves(),
0
),
base.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
base.deepFluids(), List.of()
base.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -945,7 +954,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"shore",
"dry",
"flooded",
List.of("default"), List.of()
List.of("default"), List.of(),
Double.NaN,
null
);
}
@@ -996,7 +1007,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"shore",
"dry",
"flooded",
List.of("default"), List.of()
List.of("default"), List.of(),
Double.NaN,
null
);
}
@@ -1045,7 +1058,9 @@ public class HydrologyPlannerGlobalInvariantTest {
"shore",
"dry",
"flooded",
List.of("default"), List.of()
List.of("default"), List.of(),
Double.NaN,
null
);
}
@@ -0,0 +1,331 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashSet;
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 HydrologyPlannerSeaCaveTest {
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
private static final int SEA = 63;
private static final HydrologyPlannerSettings.Grotto CHAMBER = new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 32768);
@Test
public void seaCavesOpenFromTheSeaIntoTheCliffWithoutARiver() {
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12);
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12, seaCaves);
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
HydrologyTile tile = new HydrologyPlanner(994L, settings, cliffCoast).plan(TILE);
List<RiverCourse> caves = courses(tile, RiverCourseType.SEA_CAVE);
assertFalse("diagnostics=" + tile.diagnosticCandidates(), caves.isEmpty());
for (RiverCourse cave : caves) {
assertEquals(1, cave.segments().size());
HydraulicSegment chamber = cave.segments().getFirst();
assertEquals(HydrologyFeatureType.COASTAL_GROTTO, chamber.type());
assertEquals(SEA, chamber.upstreamHeadY());
assertEquals(SEA, chamber.downstreamHeadY());
HydrologyPoint inner = chamber.start();
HydrologyPoint connection = chamber.end();
assertTrue(cliffCoast.sample(connection.x(), connection.z()).ocean());
assertFalse(cliffCoast.sample(inner.x(), inner.z()).ocean());
assertTrue("inner=" + inner + " connection=" + connection,
inner.distanceSquared2D(connection) >= (long) seaCaves.depth() * seaCaves.depth());
HydrologyCavePlan plan = tile.cavePlan(cave.id()).orElse(null);
assertTrue("cave plan missing or rejected for " + cave.id() + " diagnostics=" + tile.diagnosticCandidates(),
plan != null && plan.accepted());
int chamberColumns = 0;
int apronColumns = 0;
for (HydrologyColumnSample column : tile.footprint().columns().values()) {
for (HydrologyColumnLayer layer : column.layers()) {
if (layer.feature().courseId() != cave.id()) {
continue;
}
assertEquals(HydrologyFeatureType.COASTAL_GROTTO, layer.feature().type());
if (layer.oceanApron()) {
apronColumns++;
assertTrue(cliffCoast.sample(column.x(), column.z()).ocean());
assertTrue(StrictMath.hypot(column.x() - connection.x(), column.z() - connection.z()) <= 12.25D);
continue;
}
if (!layer.terrainOwned() || !layer.channel()) {
continue;
}
chamberColumns++;
assertFalse(cliffCoast.sample(column.x(), column.z()).ocean());
assertTrue(layer.bedY() <= SEA);
assertEquals(SEA, layer.fluidHeadY());
assertTrue(layer.ceilingY() >= SEA);
assertTrue(layer.ceilingY() <= column.naturalHeight() - 1);
}
}
assertTrue("chamber=" + chamberColumns, chamberColumns >= 100);
assertTrue("apron=" + apronColumns, apronColumns > 0);
HydrologyColumnLayer center = tile.columnAt(inner.x(), inner.z()).orElseThrow().layers().stream()
.filter((HydrologyColumnLayer layer) -> layer.feature().courseId() == cave.id() && layer.channel())
.findFirst()
.orElseThrow();
// The chamber walls carry the grotto roughness, so the centre sits within a few blocks of the ellipsoid.
assertTrue("bed=" + center.bedY(), center.bedY() <= SEA - CHAMBER.verticalRadius() + 3
&& center.bedY() >= SEA - CHAMBER.verticalRadius() - 3);
assertTrue("ceiling=" + center.ceilingY(), center.ceilingY() <= SEA + CHAMBER.headroom()
&& center.ceilingY() >= SEA + CHAMBER.headroom() - 3);
}
boolean featured = false;
for (HydrologyFeatureRef feature : tile.features()) {
featured |= feature.type() == HydrologyFeatureType.COASTAL_GROTTO;
}
assertTrue(featured);
HydrologyTile withoutCaves = new HydrologyPlanner(
994L,
settings(128, 0D, CHAMBER, 12, HydrologyPlannerSettings.SeaCaves.disabled()),
cliffCoast
).plan(TILE);
assertTrue(courses(withoutCaves, RiverCourseType.SEA_CAVE).isEmpty());
assertEquals(withoutCaves.outlets(), tile.outlets());
}
@Test
public void seaCavesAreDeterministicAndOwnedByExactlyOneTile() {
HydrologyPlannerSettings settings = settings(128, 0D, CHAMBER, 12,
new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12));
HydrologyTerrainSampler cliffCoast = cliffCoast(112, 92);
List<HydrologyTileKey> keys = List.of(
TILE,
new HydrologyTileKey(0, -1),
new HydrologyTileKey(0, 1),
new HydrologyTileKey(-1, 0),
new HydrologyTileKey(1, 0)
);
HydrologyPlanner forward = new HydrologyPlanner(41L, settings, cliffCoast);
HydrologyPlanner reversed = new HydrologyPlanner(41L, settings, cliffCoast);
List<HydrologyTile> forwardTiles = new ArrayList<>();
for (HydrologyTileKey key : keys) {
forwardTiles.add(forward.plan(key));
}
List<HydrologyTile> reversedTiles = new ArrayList<>();
for (HydrologyTileKey key : keys.reversed()) {
reversedTiles.add(reversed.plan(key));
}
for (int index = 0; index < keys.size(); index++) {
assertEquals(keys.get(index).toString(), forwardTiles.get(index), reversedTiles.get(keys.size() - 1 - index));
}
List<RiverCourse> caves = courses(forwardTiles.getFirst(), RiverCourseType.SEA_CAVE);
assertFalse(caves.isEmpty());
Set<Long> ownedIds = new HashSet<>();
for (RiverCourse cave : caves) {
ownedIds.add(cave.id());
}
for (int index = 1; index < keys.size(); index++) {
for (RiverCourse course : forwardTiles.get(index).courses()) {
assertFalse(keys.get(index) + " republished " + course.id(), ownedIds.contains(course.id()));
}
}
}
@Test
public void seaCavesPreferTheSteeperCoastAndKeepTheirSpacing() {
// The high stretch is 128 blocks of coast; the spacing leaves room for a second cave wherever the first lands.
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 2, 32, 4, 12);
HydrologyPlannerSettings settings = settings(256, 0D, CHAMBER, 12, seaCaves);
HydrologyTerrainSampler coast = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(54, "ocean")
: HydrologyTerrainSample.openLand(Math.floorDiv(z, 128) % 2 == 0 ? 70 : 90, 2D, "land");
HydrologyTile tile = new HydrologyPlanner(17L, settings, coast).plan(TILE);
List<RiverCourse> caves = courses(tile, RiverCourseType.SEA_CAVE);
assertEquals("diagnostics=" + tile.diagnosticCandidates(), 2, caves.size());
for (RiverCourse cave : caves) {
HydrologyPoint connection = cave.segments().getFirst().end();
assertTrue("connection=" + connection + " diagnostics=" + tile.diagnosticCandidates(),
connection.z() >= 128 && connection.z() < 256);
}
HydrologyPoint first = caves.get(0).segments().getFirst().end();
HydrologyPoint second = caves.get(1).segments().getFirst().end();
long minimum = seaCaves.minimumSpacing() - 2L;
assertTrue(first + " vs " + second, first.distanceSquared2D(second) >= minimum * minimum);
int lowCoastRejections = 0;
for (HydrologyDiagnosticCandidate candidate : tile.diagnosticCandidates()) {
if (candidate.kind() != HydrologyCandidateKind.OUTLET
|| candidate.projectedType() != HydrologyFeatureType.COASTAL_GROTTO
|| candidate.point().z() < 0 || candidate.point().z() >= 128) {
continue;
}
lowCoastRejections++;
assertEquals(candidate.toString(), HydrologyCandidateRejection.SOURCE_QUOTA, candidate.rejection());
}
assertTrue(lowCoastRejections > 0);
}
@Test
public void seaCavesKeepClearOfRiverMouthsAndLowCoasts() {
HydrologyPlannerSettings settings = settings(128, 2D, CHAMBER, 12,
new HydrologyPlannerSettings.SeaCaves(true, 4, 32, 8, 12));
HydrologyTerrainSampler riverCoast = (int x, int z) -> x >= 112
? HydrologyTerrainSample.ocean(54, "ocean")
: land(92, x <= 16);
HydrologyTile tile = new HydrologyPlanner(994L, settings, riverCoast).plan(TILE);
List<HydrologyPoint> outletPoints = new ArrayList<>();
for (RiverCourse course : tile.courses()) {
if (course.type() == RiverCourseType.SEA_CAVE) {
continue;
}
for (HydraulicSegment segment : course.segments()) {
if (segment.type() == HydrologyFeatureType.MOUTH
|| segment.type() == HydrologyFeatureType.COASTAL_GROTTO
|| segment.type() == HydrologyFeatureType.INLAND_GROTTO) {
outletPoints.addAll(segment.centerline());
}
}
}
List<RiverCourse> caves = courses(tile, RiverCourseType.SEA_CAVE);
assertFalse("diagnostics=" + tile.diagnosticCandidates(), outletPoints.isEmpty());
assertFalse("diagnostics=" + tile.diagnosticCandidates(), caves.isEmpty());
long clearance = 2L * CHAMBER.horizontalRadius() + 12L;
for (RiverCourse cave : caves) {
for (HydrologyPoint point : cave.segments().getFirst().centerline()) {
for (HydrologyPoint outlet : outletPoints) {
assertTrue(point + " within " + clearance + " of " + outlet,
point.distanceSquared2D(outlet) >= clearance * clearance);
}
}
}
HydrologyTile lowCoast = new HydrologyPlanner(
994L,
settings(128, 0D, CHAMBER, 12, new HydrologyPlannerSettings.SeaCaves(true, 4, 32, 8, 12)),
cliffCoast(112, 66)
).plan(TILE);
assertTrue(courses(lowCoast, RiverCourseType.SEA_CAVE).isEmpty());
assertTrue(lowCoast.diagnosticCandidates().toString(),
hasDiagnostic(lowCoast, HydrologyCandidateRejection.SURFACE_HEAD_RANGE));
}
@Test
public void seaCaveChamberFitsTheVolumeCap() {
HydrologyPlannerSettings settings = settings(128, 0D,
new HydrologyPlannerSettings.Grotto(true, 12, 7, 10, 1000), 12,
new HydrologyPlannerSettings.SeaCaves(true, 2, 64, 8, 12));
HydrologyTile tile = new HydrologyPlanner(994L, settings, cliffCoast(112, 92)).plan(TILE);
assertTrue(courses(tile, RiverCourseType.SEA_CAVE).isEmpty());
assertTrue(tile.diagnosticCandidates().toString(),
hasDiagnostic(tile, HydrologyCandidateRejection.VOLUME_LIMIT));
}
private static boolean hasDiagnostic(HydrologyTile tile, HydrologyCandidateRejection rejection) {
for (HydrologyDiagnosticCandidate candidate : tile.diagnosticCandidates()) {
if (candidate.kind() == HydrologyCandidateKind.OUTLET
&& candidate.projectedType() == HydrologyFeatureType.COASTAL_GROTTO
&& candidate.rejection() == rejection) {
return true;
}
}
return false;
}
private static List<RiverCourse> courses(HydrologyTile tile, RiverCourseType type) {
ArrayList<RiverCourse> selected = new ArrayList<>();
for (RiverCourse course : tile.courses()) {
if (course.type() == type) {
selected.add(course);
}
}
return selected;
}
private static HydrologyTerrainSampler cliffCoast(int coastX, int height) {
return (int x, int z) -> x >= coastX
? HydrologyTerrainSample.ocean(54, "ocean")
: HydrologyTerrainSample.openLand(height, 2D, "land");
}
private static HydrologyTerrainSample land(int height, boolean surfaceSource) {
return new HydrologyTerrainSample(
height,
2D,
false,
true,
72,
74,
true,
true,
surfaceSource,
surfaceSource,
false,
false,
0D,
surfaceSource ? 1D : 0D,
0D,
1D,
1D,
1D,
1D,
1D,
"parent",
"surface",
"mouth",
"shore",
"dry",
"flooded",
List.of("beta", "alpha"),
List.of(),
Double.NaN,
null
);
}
private static HydrologyPlannerSettings settings(
int tileSize,
double surfaceDensity,
HydrologyPlannerSettings.Grotto coastal,
int maximumOceanApron,
HydrologyPlannerSettings.SeaCaves seaCaves
) {
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
true, surfaceDensity, 80, 0, 6, 24);
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
true, 0D, Integer.MIN_VALUE, 0, 4, 32);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
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),
surface,
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(
false, undergroundSources, 68, 82, 4, 12, 2, 4, 5, 9, true, 0),
new HydrologyPlannerSettings.Outlets(
true,
coastal,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
false,
12,
32,
maximumOceanApron,
4,
4
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(),
List.of(),
surface.shoreWidth(),
seaCaves
);
}
}
@@ -62,8 +62,10 @@ public class HydrologyPlannerSettingsTest {
base.underground(),
base.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(overlongChannel), List.of()
));
List.of(overlongChannel), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled())
);
}
@Test
@@ -78,8 +80,10 @@ public class HydrologyPlannerSettingsTest {
0D,
0D,
0D,
0D
, 1D, 0);
0D,
1D,
0
);
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
true,
@@ -101,6 +105,7 @@ public class HydrologyPlannerSettingsTest {
0,
0,
64,
1,
1
);
HydrologyPlannerSettings.DeepFluid deepFluid = new HydrologyPlannerSettings.DeepFluid(
@@ -137,25 +142,77 @@ public class HydrologyPlannerSettingsTest {
public void bankSettingsRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Banks banks = HydrologyPlannerSettings.Banks.defaults();
assertEquals(1, banks.inset());
assertEquals(1, banks.freeboard());
assertEquals(0, banks.sink());
assertTrue(banks.erosion().enabled());
assertEquals(12, banks.erosion().smoothingRadius());
assertTrue(banks.ponds().source().enabled());
assertTrue(banks.ponds().terminal().enabled());
assertEquals(3D, banks.blendSlope(), 0D);
assertEquals(4, banks.minimumBlendWidth());
assertEquals(32, banks.maximumBlendWidth());
assertEquals(6, banks.waterfallMinimumDrop());
assertTrue(banks.exposeCutStrata());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(-1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, -1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 0D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 0, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, 2.5D, 24, true));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(-1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 0D, 4, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 4, 32, 0.25D, 16, 2, 0, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Surface(
true,
new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0),
1, 1, 1, 1, 0, 1D,
null));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, -1, 0.45D, 1D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 1D, 1D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 0D, 0.5D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, -0.1D));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 0, 4, 3));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 5, 4, 3));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Pond(true, 4, 6, 0));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Ponds(null, new HydrologyPlannerSettings.Pond(true, 4, 6, 3)));
}
@Test
public void inletSettingsRejectValuesOutsideTheirBounds() {
HydrologyPlannerSettings.Inlet inlet = HydrologyPlannerSettings.defaults().surface().banks().inlet();
assertEquals(64, inlet.length());
assertEquals(3, inlet.depth());
assertEquals(32, inlet.maximumIncision());
assertEquals(new HydrologyPlannerSettings.Inlet(0, 0, 0), HydrologyPlannerSettings.Inlet.none());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(-1, 3, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(1025, 3, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, -1, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 65, 32));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Inlet(64, 3, 513));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, null, 2.5D, 24, true,
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults()));
new HydrologyPlannerSettings.Inlet(1024, 64, 512);
}
@Test
public void publicationRadiusGrowsWithTheMouthFlare() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Banks banks = base.surface().banks();
HydrologyPlannerSettings.Banks flared = new HydrologyPlannerSettings.Banks(
banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(), banks.roughness(),
banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(), 4D, banks.inlet(),
banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), banks.erosion(), banks.ponds());
HydrologyPlannerSettings.Surface surface = base.surface();
HydrologyPlannerSettings wide = new HydrologyPlannerSettings(
base.seaLevel(), base.routing(),
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(),
surface.maximumWidth(), surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(),
surface.shoreWidth(), flared),
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(), 0D, HydrologyPlannerSettings.SeaCaves.disabled());
assertEquals(1.6D, banks.mouthFlareRatio(), 0D);
int widened = (int) StrictMath.ceil(surface.maximumWidth() * (4D - banks.mouthFlareRatio()) / 2D);
assertTrue(wide.publicationRadius() - base.publicationRadius() >= widened - 1);
assertTrue(wide.publicationRadius() > base.publicationRadius());
}
@Test
@@ -178,8 +235,9 @@ public class HydrologyPlannerSettingsTest {
0,
0,
1,
1
));
1,
1)
);
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
true,
accepted,
@@ -188,8 +246,33 @@ public class HydrologyPlannerSettingsTest {
0,
0,
65,
1
));
1,
1)
);
}
@Test
public void coastalOutletBudgetIsBounded() {
HydrologyPlannerSettings.Grotto grotto = new HydrologyPlannerSettings.Grotto(true, 1, 1, 1, 1);
HydrologyPlannerSettings.Outlets separate = new HydrologyPlannerSettings.Outlets(
true,
grotto,
grotto,
true,
0,
0,
1,
3,
0
);
assertEquals(0, separate.maximumCoastalPerTile());
assertEquals(256, new HydrologyPlannerSettings.Outlets(true, grotto, grotto, true, 0, 0, 1, 3, 256)
.maximumCoastalPerTile());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
true, grotto, grotto, true, 0, 0, 1, 3, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Outlets(
true, grotto, grotto, true, 0, 0, 1, 3, 257));
}
@Test
@@ -259,4 +342,78 @@ public class HydrologyPlannerSettingsTest {
0
));
}
@Test
public void seaCaveBoundsAreEnforcedAndTheDefaultsPlanNone() {
HydrologyPlannerSettings.SeaCaves seaCaves = new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 12);
assertEquals(3, seaCaves.maximumPerTile());
assertFalse(HydrologyPlannerSettings.SeaCaves.disabled().enabled());
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, -1, 160, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 65, 160, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 15, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 8193, 8, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 0, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 129, 12));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, -1));
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.SeaCaves(true, 3, 160, 8, 129));
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
assertEquals(HydrologyPlannerSettings.SeaCaves.disabled(), base.seaCaves());
HydrologyPlannerSettings withCaves = new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
base.surface(),
base.hydraulics(),
base.underground(),
base.outlets(),
base.geometry(),
base.deepFluids(),
base.surfacePools(),
base.widestShoreBiomeWidth(),
seaCaves
);
assertEquals(seaCaves, withCaves.seaCaves());
assertTrue(base.fingerprint() != withCaves.fingerprint());
}
@Test
public void seaCavesAloneReachTheChamberAndItsSweepBeyondTheHalo() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
HydrologyPlannerSettings.Source noSources = new HydrologyPlannerSettings.Source(false, 0D, 0, 0, 0, 0);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
false, noSources, 4, 8, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults());
HydrologyPlannerSettings.Underground underground = new HydrologyPlannerSettings.Underground(
false, noSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
HydrologyPlannerSettings quiet = new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
surface,
base.hydraulics(),
underground,
base.outlets(),
base.geometry(),
List.of(),
List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydrologyPlannerSettings withCaves = new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
surface,
base.hydraulics(),
underground,
base.outlets(),
base.geometry(),
List.of(),
List.of(),
surface.shoreWidth(),
new HydrologyPlannerSettings.SeaCaves(true, 1, 64, 8, 12)
);
int alignedHalo = Math.min(base.routing().maximumRouteLength(), base.routing().sampleSpacing() * 2)
/ base.routing().sampleSpacing() * base.routing().sampleSpacing();
assertEquals(0, quiet.publicationRadius());
assertEquals(alignedHalo + base.outlets().coastalGrotto().horizontalRadius() + 12 + 1, withCaves.publicationRadius());
}
}
@@ -31,7 +31,7 @@ public class HydrologyPlannerSurfacePoolTest {
assertEquals(HydrologyFeatureType.STANDING_POOL, segment.type());
assertTrue(segment.width() >= 8 && segment.width() <= 12);
assertTrue(segment.start().x() < 512);
assertEquals(79, segment.upstreamHeadY());
assertEquals(80, segment.upstreamHeadY());
assertEquals(segment.upstreamHeadY(), segment.downstreamHeadY());
}
}
@@ -58,7 +58,7 @@ public class HydrologyPlannerSurfacePoolTest {
if (layer.channel()) {
wet++;
assertEquals("lava_pool", layer.profileKey());
assertEquals(79, layer.fluidHeadY());
assertEquals(80, layer.fluidHeadY());
assertTrue(layer.bedY() < layer.fluidHeadY());
assertTrue(center.distanceSquared2D(new HydrologyPoint(column.x(), 0, column.z())) <= 8 * 8);
} else if (layer.shore()) {
@@ -113,7 +113,9 @@ public class HydrologyPlannerSurfacePoolTest {
base.geometry(),
List.of(),
List.of(new HydrologyPlannerSettings.SurfacePool(
"lava_pool", true, density, spacing, minimumRadius, maximumRadius, depth, 64, null))
"lava_pool", true, density, spacing, minimumRadius, maximumRadius, depth, 64, null)),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
}
@@ -125,8 +125,9 @@ public class HydrologyPlannerTest {
configured.coastalCliffMinimumHeight(),
0,
configured.maximumOceanApron(),
configured.maximumPerTile()
));
configured.maximumPerTile(),
configured.maximumPerTile())
);
HydrologyTile tile = planner(78L, settings, rollingCoast(112)).plan(TILE);
@@ -438,10 +439,10 @@ public class HydrologyPlannerTest {
for (HydrologyPoint point : segment.centerline()) {
assertTrue(point.y() <= previousHead);
assertTrue(previousHead - point.y() <= 1);
assertTrue(rollingTerrain.sample(point.x() - 1, point.z()).naturalHeight() > point.y());
assertTrue(rollingTerrain.sample(point.x() + 1, point.z()).naturalHeight() > point.y());
assertTrue(rollingTerrain.sample(point.x(), point.z() - 1).naturalHeight() > point.y());
assertTrue(rollingTerrain.sample(point.x(), point.z() + 1).naturalHeight() > point.y());
assertTrue(rollingTerrain.sample(point.x() - 1, point.z()).naturalHeight() >= point.y());
assertTrue(rollingTerrain.sample(point.x() + 1, point.z()).naturalHeight() >= point.y());
assertTrue(rollingTerrain.sample(point.x(), point.z() - 1).naturalHeight() >= point.y());
assertTrue(rollingTerrain.sample(point.x(), point.z() + 1).naturalHeight() >= point.y());
previousHead = point.y();
}
assertEquals(segment.downstreamHeadY(), previousHead);
@@ -514,7 +515,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("alpha"), List.of()
List.of("alpha"), List.of(),
Double.NaN,
null
);
};
HydrologyTile tile = new HydrologyPlanner(812L, settings, terrain, solidCaveView()).plan(TILE);
@@ -588,7 +591,9 @@ public class HydrologyPlannerTest {
base.underground(),
base.outlets(),
base.geometry(),
base.deepFluids(), List.of()
base.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydrologyTerrainSampler repeatedRidges = (int x, int z) -> {
if (x >= 112) {
@@ -756,8 +761,9 @@ public class HydrologyPlannerTest {
configured.coastalCliffMinimumHeight(),
configured.mouthLevelingDistance(),
configured.maximumOceanApron(),
1
));
1,
1)
);
HydrologyTerrainSampler coast = (int x, int z) -> {
if (x >= 112) {
return oceanTerrain();
@@ -1122,7 +1128,9 @@ public class HydrologyPlannerTest {
base.underground(),
base.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
base.deepFluids(), List.of()
base.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydrologyPlannerSettings settings = withOutlets(shaped, new HydrologyPlannerSettings.Outlets(
true,
@@ -1132,8 +1140,9 @@ public class HydrologyPlannerTest {
base.outlets().coastalCliffMinimumHeight(),
base.outlets().mouthLevelingDistance(),
base.outlets().maximumOceanApron(),
1
));
1,
1)
);
HydrologyTile tile = new HydrologyPlanner(642L, settings, optionalRollingCoast(112)).plan(TILE);
List<RiverCourse> courses = surfaceCourses(tile);
@@ -1236,8 +1245,9 @@ public class HydrologyPlannerTest {
12,
base.outlets().mouthLevelingDistance(),
base.outlets().maximumOceanApron(),
base.outlets().maximumPerTile()
));
base.outlets().maximumPerTile(),
base.outlets().maximumPerTile())
);
HydrologyTerrainSampler cliffCoast = (int x, int z) -> {
if (x >= 112) {
return oceanTerrain();
@@ -1285,6 +1295,192 @@ public class HydrologyPlannerTest {
}
}
@Test
public void coastalAndInlandOutletsAreBudgetedSeparately() {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, true, List.of());
HydrologyPlannerSettings settings = withOutlets(base, new HydrologyPlannerSettings.Outlets(
true,
base.outlets().coastalGrotto(),
base.outlets().inlandGrotto(),
base.outlets().surfaceSinkholesEnabled(),
base.outlets().coastalCliffMinimumHeight(),
base.outlets().mouthLevelingDistance(),
base.outlets().maximumOceanApron(),
1,
1
));
HydrologyTerrainSampler terrain = (int x, int z) -> {
if (x >= 112) {
return oceanTerrain();
}
if (x == 48) {
return blockedTerrain();
}
boolean source = (x == 0 || x == 80) && z == 0;
return terrain(90 - Math.floorDiv(x, 16), 1D, false, true, source, source, false, false);
};
HydrologyTile tile = new HydrologyPlanner(70143L, settings, terrain, solidCaveView()).plan(TILE);
assertEquals(
"outlets=" + tile.outlets() + " diagnostics=" + tile.diagnosticCandidates(),
1,
tile.outlets().stream().filter(RiverOutlet::directOcean).count()
);
assertEquals(
"outlets=" + tile.outlets() + " diagnostics=" + tile.diagnosticCandidates(),
1,
tile.outlets().stream().filter((RiverOutlet outlet) -> !outlet.directOcean()).count()
);
}
@Test
public void aCliffAndABeachOnTheSameCoastSelectAMouthAndACoastalGrotto() {
HydrologyTerrainSampler coast = (int x, int z) -> {
if (x >= 112) {
return oceanTerrain();
}
if (z < 64) {
return terrain(92, 2D, false, true, x <= 16, true, false, false);
}
return terrain(66 + Math.floorDiv((96 - x) * 3, 16), 1D, false, false, x <= 16, true, false, false);
};
HydrologyTile mixed = new HydrologyPlanner(995L, mixedCoastSettings(2), coast).plan(TILE);
HydrologyTile single = new HydrologyPlanner(995L, mixedCoastSettings(1), coast).plan(TILE);
assertTrue("outlets=" + mixed.outlets(), mixed.outlets().stream().allMatch(RiverOutlet::directOcean));
assertTrue("outlets=" + mixed.outlets(), mixed.outlets().stream()
.anyMatch((RiverOutlet outlet) -> outlet.type() == HydrologyFeatureType.MOUTH));
assertTrue("outlets=" + mixed.outlets(), mixed.outlets().stream()
.anyMatch((RiverOutlet outlet) -> outlet.type() == HydrologyFeatureType.COASTAL_GROTTO));
assertEquals("outlets=" + single.outlets(), 1, single.outlets().size());
assertEquals(HydrologyFeatureType.MOUTH, single.outlets().getFirst().type());
}
@Test
public void ownedCoastOutranksHaloCoast() {
HydrologyPlannerSettings settings = standardSettings(2D, 0D, true, false, List.of());
HydrologyTerrainSampler twoCoasts = (int x, int z) -> {
if (x >= 112 || x <= -32) {
return oceanTerrain();
}
boolean source = x >= 48 && x <= 96;
return terrain(88 - Math.floorDiv(x, 16), 1D, false, true, source, source, false, false);
};
HydrologyTile tile = new HydrologyPlanner(883L, settings, twoCoasts).plan(TILE);
assertFalse("diagnostics=" + tile.diagnosticCandidates(), tile.outlets().isEmpty());
assertFalse(surfaceCourses(tile).isEmpty());
for (RiverOutlet outlet : tile.outlets()) {
assertTrue(
"outlet=" + outlet,
TILE.contains(outlet.landwardPoint().x(), outlet.landwardPoint().z(), tile.tileSize())
);
}
}
@Test
public void undergroundSeaLevelOutletsAreReportedAsOutletLevel() {
HydrologyPlannerSettings base = standardSettings(2D, 1D, true, true, List.of());
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(
base.seaLevel(),
base.routing(),
base.surface(),
base.hydraulics(),
new HydrologyPlannerSettings.Underground(
true,
base.underground().sources(),
40,
50,
4,
12,
2,
4,
5,
9,
true,
0
),
base.outlets(),
base.geometry(),
base.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydrologyTerrainSampler coast = (int x, int z) -> {
if (x >= 112) {
return oceanTerrain();
}
boolean source = x <= 16;
return terrain(88 - Math.floorDiv(x, 16), 1D, false, true, source, source, source, false);
};
HydrologyTile tile = new HydrologyPlanner(884L, settings, coast, solidCaveView()).plan(TILE);
List<HydrologyDiagnosticCandidate> level = tile.diagnosticCandidates().stream()
.filter((HydrologyDiagnosticCandidate candidate) ->
candidate.kind() == HydrologyCandidateKind.OUTLET
&& candidate.rejection() == HydrologyCandidateRejection.OUTLET_LEVEL)
.toList();
assertFalse("diagnostics=" + tile.diagnosticCandidates(), level.isEmpty());
for (HydrologyDiagnosticCandidate candidate : level) {
assertTrue(
candidate.toString(),
candidate.projectedType() == HydrologyFeatureType.MOUTH
|| candidate.projectedType() == HydrologyFeatureType.COASTAL_GROTTO
);
assertEquals(settings.seaLevel(), candidate.point().y());
assertFalse(coast.sample(candidate.point().x(), candidate.point().z()).ocean());
}
assertEquals(level.size(), level.stream().map(HydrologyDiagnosticCandidate::id).distinct().count());
}
@Test
public void fallbackAlternatesCoastalAndInlandTrials() {
// An island whose coast is walled by a berm between the lattice columns: every mouth course
// needs a cut deeper than the channel may make, so the surface routing falls back. The coast
// offers more fallback mouths than the trial budget, so a sinkhole only gets its trial when
// the fallback alternates coastal and inland candidates.
HydrologyPlannerSettings settings = standardSettings(1D, 0D, true, true, List.of());
HydrologyTerrainSampler island = (int x, int z) -> {
if (x < -16 || x > 144 || z < -16 || z > 144) {
return oceanTerrain();
}
boolean berm = (x > -13 && x < -3) || (x > 131 && x < 141)
|| (z > -13 && z < -3) || (z > 131 && z < 141);
boolean source = x >= 32 && x <= 96 && z >= 32 && z <= 96;
return terrain(berm ? 170 : 90, 1D, false, true, source, source, false, false);
};
HydrologyTile tile = new HydrologyPlanner(885L, settings, island, solidCaveView()).plan(TILE);
assertFalse("diagnostics=" + tile.diagnosticCandidates(), surfaceCourses(tile).isEmpty());
assertTrue(tile.diagnosticCandidates().stream().anyMatch((HydrologyDiagnosticCandidate candidate) ->
candidate.rejection() == HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED));
for (RiverCourse course : surfaceCourses(tile)) {
RiverOutlet outlet = tile.outlet(course.outletId().orElseThrow()).orElseThrow();
assertFalse("outlet=" + outlet, outlet.directOcean());
assertTrue(course.surfaceSinkholeContinuation());
}
}
private HydrologyPlannerSettings mixedCoastSettings(int maximumCoastalPerTile) {
HydrologyPlannerSettings base = standardSettings(2D, 0D, true, false, List.of());
return withOutlets(base, new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(true, 4, 3, 3, 4096),
base.outlets().inlandGrotto(),
base.outlets().surfaceSinkholesEnabled(),
12,
base.outlets().mouthLevelingDistance(),
base.outlets().maximumOceanApron(),
base.outlets().maximumPerTile(),
maximumCoastalPerTile
));
}
@Test
public void configuredSurfaceRiverContinuesThroughOneContainedSinkholeCourse() {
HydrologyPlannerSettings settings = standardSettings(0D, 0D, false, true, List.of());
@@ -1355,8 +1551,9 @@ public class HydrologyPlannerTest {
configured.coastalCliffMinimumHeight(),
configured.mouthLevelingDistance(),
configured.maximumOceanApron(),
configured.maximumPerTile()
));
configured.maximumPerTile(),
configured.maximumPerTile())
);
HydrologyTile tile = new HydrologyPlanner(
7013L,
@@ -1408,7 +1605,9 @@ public class HydrologyPlannerTest {
base.underground(),
base.outlets(),
base.geometry(),
base.deepFluids(), List.of()
base.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
HydrologyTerrainSampler terrain = (int x, int z) -> {
if (x >= 112) {
@@ -1756,7 +1955,8 @@ public class HydrologyPlannerTest {
4,
5,
9,
true
true,
0
),
new HydrologyPlannerSettings.Outlets(
oceanEnabled,
@@ -1766,10 +1966,13 @@ public class HydrologyPlannerTest {
12,
32,
2,
4,
4
),
stableGeometry(),
deepFluids, List.of()
deepFluids, List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -1799,8 +2002,10 @@ public class HydrologyPlannerTest {
1.5D,
24D,
2D,
0.2D
, 1D, 0),
0.2D,
1D,
0
),
new HydrologyPlannerSettings.Surface(
true,
new HydrologyPlannerSettings.Source(true, 8D, 80, 0, 8, 128),
@@ -1821,10 +2026,13 @@ public class HydrologyPlannerTest {
outlets.coastalCliffMinimumHeight(),
48,
outlets.maximumOceanApron(),
1,
1
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(), List.of()
List.of(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -1893,7 +2101,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("alpha"), List.of()
List.of("alpha"), List.of(),
Double.NaN,
null
);
};
}
@@ -1958,7 +2168,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("alpha", "beta"), List.of()
List.of("alpha", "beta"), List.of(),
Double.NaN,
null
);
}
@@ -1990,7 +2202,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("alpha"), List.of()
List.of("alpha"), List.of(),
Double.NaN,
null
);
}
@@ -2031,7 +2245,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("beta", "alpha"), List.of()
List.of("beta", "alpha"), List.of(),
Double.NaN,
null
);
}
@@ -2063,7 +2279,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("alpha"), List.of()
List.of("alpha"), List.of(),
Double.NaN,
null
);
}
@@ -2095,7 +2313,9 @@ public class HydrologyPlannerTest {
"shore",
"dry",
"flooded",
List.of("alpha", "beta"), List.of()
List.of("alpha", "beta"), List.of(),
Double.NaN,
null
);
}
@@ -2121,7 +2341,9 @@ public class HydrologyPlannerTest {
settings.underground(),
settings.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
settings.deepFluids(), List.of()
settings.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -2146,11 +2368,14 @@ public class HydrologyPlannerTest {
underground.maximumDepth(),
underground.minimumHeadroom(),
underground.maximumHeadroom(),
underground.connectToExistingCaves()
underground.connectToExistingCaves(),
0
),
settings.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
settings.deepFluids(), List.of()
settings.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -2176,11 +2401,14 @@ public class HydrologyPlannerTest {
underground.maximumDepth(),
underground.minimumHeadroom(),
underground.maximumHeadroom(),
underground.connectToExistingCaves()
underground.connectToExistingCaves(),
0
),
settings.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
settings.deepFluids(), List.of()
settings.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -2205,11 +2433,14 @@ public class HydrologyPlannerTest {
underground.maximumDepth(),
underground.minimumHeadroom(),
underground.maximumHeadroom(),
underground.connectToExistingCaves()
underground.connectToExistingCaves(),
0
),
settings.outlets(),
HydrologyPlannerSettings.Geometry.defaults(),
settings.deepFluids(), List.of()
settings.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -2269,7 +2500,9 @@ public class HydrologyPlannerTest {
settings.underground(),
outlets,
HydrologyPlannerSettings.Geometry.defaults(),
settings.deepFluids(), List.of()
settings.deepFluids(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -6,6 +6,7 @@ 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.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -56,6 +57,110 @@ public class HydrologyPlannerTributaryTest {
}
}
@Test
public void theApproachIntoTheStemNeverRisesWhereTheGroundHoldsAStationUp() {
// A level tributary tail at 120 hangs twenty blocks above its stem. The ground beside the junction
// falls to the stem, but a knoll a few stations back stands too high for the channel to cut it
// down: the stations over the knoll keep their head, and every station upstream of them stays at
// least as high, so the graded reach still descends into the stem instead of rising over the knoll.
HydrologyTerrainSampler terrain = (x, z) -> land(x >= 14 && x <= 17 ? 130 : x >= 18 ? 100 + (20 - x) * 4 : 121);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
HydrologyPlanner planner = new HydrologyPlanner(19L, settings, terrain);
ArrayList<HydrologyPoint> centerline = new ArrayList<>();
for (int x = 0; x <= 20; x++) {
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))));
planner.levelApproach(segments, 100);
List<HydrologyPoint> graded = new ArrayList<>();
int previousHead = Integer.MAX_VALUE;
for (HydraulicSegment segment : segments) {
assertTrue(segment.upstreamHeadY() <= previousHead);
assertTrue(segment.upstreamHeadY() >= segment.downstreamHeadY());
previousHead = segment.downstreamHeadY();
graded.addAll(segment.centerline());
}
assertEquals(21, graded.size());
assertEquals(100, graded.getLast().y());
int previous = Integer.MAX_VALUE;
for (HydrologyPoint point : graded) {
assertTrue("head rises downstream at x=" + point.x(), point.y() <= previous);
assertTrue("cut below the incision floor at x=" + point.x(),
point.y() >= terrain.sample(point.x(), point.z()).naturalHeight() - settings.surface().maximumIncision());
previous = point.y();
}
assertEquals(120, graded.get(14).y());
assertEquals(120, graded.get(17).y());
assertTrue(graded.get(18).y() < 120);
}
@Test
public void aLevelReachWithABumpStaysGradedOnceItsTailIsCutDown() {
// A pool reach may carry stations above its heads. Cutting its tail down to the stem turns it
// into a graded transition, which must descend monotonically: the bump is held at the head.
HydrologyTerrainSampler terrain = (x, z) -> land(121);
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
HydrologyPlanner planner = new HydrologyPlanner(23L, settings, terrain);
ArrayList<HydrologyPoint> centerline = new ArrayList<>();
for (int x = 0; x <= 12; x++) {
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))));
planner.levelApproach(segments, 112);
List<HydrologyPoint> graded = segments.getFirst().centerline();
assertEquals(13, graded.size());
assertEquals(112, graded.getLast().y());
int previous = Integer.MAX_VALUE;
for (HydrologyPoint point : graded) {
assertTrue("head rises downstream at x=" + point.x(), point.y() <= previous);
previous = point.y();
}
assertTrue("bump above the head at x=5", graded.get(5).y() <= 120);
assertTrue(graded.getFirst().y() <= 120);
assertTrue(segments.getFirst().upstreamHeadY() >= segments.getFirst().downstreamHeadY());
}
private static HydrologyTerrainSample land(int height) {
return new HydrologyTerrainSample(
height,
0D,
false,
false,
height - 40,
height - 38,
true,
true,
false,
false,
false,
false,
0D,
0D,
0D,
1D,
1D,
1D,
1D,
1D,
"land",
"land",
"land",
"land",
"land",
"land",
List.of("default"),
List.of(),
Double.NaN,
null
);
}
private static int stations(RiverCourse course) {
int stations = 0;
for (HydraulicSegment segment : course.segments()) {
@@ -114,19 +219,25 @@ public class HydrologyPlannerTributaryTest {
"land",
"land",
List.of("default"),
List.of()
List.of(),
Double.NaN,
null
);
}
private static HydrologyPlannerSettings settings(int tributaries) {
return settings(tributaries, 4D, 4);
}
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);
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 4D, 0, 0, 4, 128);
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());
HydrologyPlannerSettings.Underground underground = new HydrologyPlannerSettings.Underground(
false, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true);
false, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true, 0);
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
true,
base.outlets().coastalGrotto(),
@@ -135,6 +246,7 @@ public class HydrologyPlannerTributaryTest {
base.outlets().coastalCliffMinimumHeight(),
base.outlets().mouthLevelingDistance(),
base.outlets().maximumOceanApron(),
1,
1
);
return new HydrologyPlannerSettings(
@@ -146,7 +258,161 @@ public class HydrologyPlannerTributaryTest {
outlets,
base.geometry(),
List.of(),
List.of()
List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@Test
public void aTributaryOwnsOnlyTheDrainageUpstreamOfItsJunctionAndMeetsTheStemWater() {
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::valley;
HydrologyTile split = new HydrologyPlanner(19L, settings(1), terrain).plan(TILE);
List<RiverCourse> courses = surface(split);
assertEquals(split.diagnosticCandidates().toString(), 2, courses.size());
RiverCourse stem = stations(courses.get(0)) >= stations(courses.get(1)) ? courses.get(0) : courses.get(1);
RiverCourse tributary = stem == courses.get(0) ? courses.get(1) : courses.get(0);
java.util.Set<Long> stemEdges = new java.util.HashSet<>();
for (DrainageEdge edge : stem.drainageEdges()) {
stemEdges.add(edge.id());
}
assertFalse(tributary.drainageEdges().isEmpty());
for (DrainageEdge edge : tributary.drainageEdges()) {
assertFalse("tributary shares stem drainage " + edge.id(), stemEdges.contains(edge.id()));
}
HydrologyPoint confluence = tributary.segments().getLast().end();
HydrologyPoint stemEnd = stem.segments().getLast().end();
assertTrue("junction sits at the stem's mouth", confluence.x() != stemEnd.x() || confluence.z() != stemEnd.z());
Integer stemHead = null;
for (HydraulicSegment segment : stem.segments()) {
for (HydrologyPoint point : segment.centerline()) {
if (point.x() == confluence.x() && point.z() == confluence.z()) {
stemHead = point.y();
}
}
}
assertNotNull("tributary ends on the stem", stemHead);
assertEquals(stemHead.intValue(), tributary.segments().getLast().downstreamHeadY());
for (HydrologyDiagnosticCandidate candidate : split.diagnosticCandidates()) {
assertTrue(candidate.toString(), candidate.kind() != HydrologyCandidateKind.TRIBUTARY);
}
}
@Test
public void tributariesAreBudgetedOnTopOfTheSourceDensity() {
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::valley;
HydrologyTile single = new HydrologyPlanner(19L, settings(0, 1D, 1), terrain).plan(TILE);
HydrologyTile joined = new HydrologyPlanner(19L, settings(1, 1D, 1), terrain).plan(TILE);
assertEquals(single.diagnosticCandidates().toString(), 1, surface(single).size());
assertEquals(joined.diagnosticCandidates().toString(), 2, surface(joined).size());
}
@Test
public void undergroundSourcesJoinTheirStemWhereTheirPassagesMeet() {
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::cavernousCoast;
HydrologyTile single = new HydrologyPlanner(23L, undergroundSettings(0), terrain).plan(TILE);
HydrologyTile joined = new HydrologyPlanner(23L, undergroundSettings(1), terrain).plan(TILE);
assertEquals(single.diagnosticCandidates().toString(), 1, underground(single).size());
List<RiverCourse> courses = underground(joined);
assertEquals(joined.diagnosticCandidates().toString(), 2, courses.size());
RiverCourse stem = stations(courses.get(0)) >= stations(courses.get(1)) ? courses.get(0) : courses.get(1);
RiverCourse tributary = stem == courses.get(0) ? courses.get(1) : courses.get(0);
assertEquals(stem.outletId(), tributary.outletId());
HydrologyPoint junction = tributary.segments().getLast().end();
Integer stemHead = null;
for (HydraulicSegment segment : stem.segments()) {
for (HydrologyPoint point : segment.centerline()) {
if (point.x() == junction.x() && point.z() == junction.z()) {
stemHead = point.y();
}
}
}
assertNotNull("underground tributary ends on its stem", stemHead);
assertTrue(tributary.segments().getLast().downstreamHeadY() >= stemHead);
java.util.Set<Long> stemEdges = new java.util.HashSet<>();
for (DrainageEdge edge : stem.drainageEdges()) {
stemEdges.add(edge.id());
}
for (DrainageEdge edge : tributary.drainageEdges()) {
assertFalse(stemEdges.contains(edge.id()));
}
for (HydraulicSegment segment : tributary.segments()) {
assertTrue(segment.upstreamHeadY() >= segment.downstreamHeadY());
}
}
@Test
public void undergroundTributariesAreBounded() {
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
org.junit.Assert.assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Underground(
true, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true, 5));
}
private static List<RiverCourse> underground(HydrologyTile tile) {
List<RiverCourse> courses = new java.util.ArrayList<>();
for (RiverCourse course : tile.courses()) {
if (course.type() == RiverCourseType.UNDERGROUND) {
courses.add(course);
}
}
return courses;
}
/** A small coast with caves: two underground source zones at the west edge drain east into one sea outlet. */
private static HydrologyTerrainSample cavernousCoast(int x, int z) {
if (x >= 112) {
return new HydrologyTerrainSample(
54, 0D, true, false, 30, 32, false, false, false, false, false, false,
0D, 0D, 0D, 1D, 1D, 1D, 1D, 1D,
"ocean_parent", "surface", "mouth", "shore", "dry", "flooded",
List.of("default"), List.of(), Double.NaN, null);
}
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
boolean source = x >= 0 && x <= 24 && (z >= 8 && z <= 40 || z >= 72 && z <= 104);
return new HydrologyTerrainSample(
height, 1D, false, true, 70, 74, true, true, false, false, source, false,
0D, 0D, source ? 1D : 0D, 1D, 1D, 1D, 1D, 1D,
"parent", "surface", "mouth", "shore", "dry", "flooded",
List.of("default"), List.of(), Double.NaN, null);
}
private static HydrologyPlannerSettings undergroundSettings(int tributaries) {
HydrologyPlannerSettings.Source none = new HydrologyPlannerSettings.Source(false, 0D, 80, 0, 0, 24);
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, Integer.MIN_VALUE, 0, 1, 32);
HydrologyPlannerSettings.ChannelShape stableChannel = new HydrologyPlannerSettings.ChannelShape(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.Surface(false, none, 4, 18, 2, 4, 10, 1.5D, HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(4),
new HydrologyPlannerSettings.Underground(true, sources, 68, 82, 4, 12, 2, 4, 5, 9, true, tributaries),
new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
false,
12,
32,
2,
1,
1
),
new HydrologyPlannerSettings.Geometry(
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
stableChannel,
stableChannel,
stableChannel,
HydrologyPlannerSettings.Geometry.defaults().drops()
),
List.of(),
List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
}
@@ -89,7 +89,7 @@ public class HydrologyStyledGeometryTest {
+ terrain(surfaceSource.naturalPoint().x(), surfaceSource.naturalPoint().z()).naturalHeight(),
surfaceStart.upstreamHeadY() <=
terrain(surfaceSource.naturalPoint().x(), surfaceSource.naturalPoint().z()).naturalHeight()
- surfaceSettings.surface().banks().inset()
- surfaceSettings.surface().banks().sink()
);
int widestSurfaceWidth = 0;
for (HydraulicSegment segment : surface.segments()) {
@@ -218,7 +218,8 @@ public class HydrologyStyledGeometryTest {
4,
5,
9,
true
true,
0
),
new HydrologyPlannerSettings.Outlets(
true,
@@ -228,10 +229,13 @@ public class HydrologyStyledGeometryTest {
12,
32,
2,
4,
4
),
HydrologyPlannerSettings.Geometry.defaults(),
deepFluids, List.of()
deepFluids, List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -264,7 +268,9 @@ public class HydrologyStyledGeometryTest {
"shore",
"dry",
"flooded",
List.of("water"), List.of()
List.of("water"), List.of(),
Double.NaN,
null
);
}
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
@@ -296,7 +302,9 @@ public class HydrologyStyledGeometryTest {
"shore",
"dry",
"flooded",
List.of("water"), List.of()
List.of("water"), List.of(),
Double.NaN,
null
);
}
@@ -6,6 +6,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
@@ -77,7 +80,42 @@ public final class HydrologyTerrainSampleTest {
"shore",
"dry",
"flooded",
profiles, List.of()
profiles, List.of(),
Double.NaN,
null
);
}
@Test
public void shoreBiomeWidthInheritsWhenUnsetAndRejectsNegatives() {
HydrologyTerrainSample unset = HydrologyTerrainSample.openLand(80, 0D, "parent");
assertTrue(Double.isNaN(unset.shoreBiomeWidth()));
assertEquals(1.5D, unset.shoreBiomeWidth(1.5D), 0D);
HydrologyTerrainSample wide = sample(List.of("default"), 12D, null);
assertEquals(12D, wide.shoreBiomeWidth(1.5D), 0D);
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), -1D, null));
assertThrows(IllegalArgumentException.class, () -> sample(List.of("default"), Double.POSITIVE_INFINITY, null));
}
@Test
public void confinesKeyIsNormalizedAndDecidesWhereWaterMayDrain() {
HydrologyTerrainSample open = sample(List.of("default"), Double.NaN, " ");
HydrologyTerrainSample west = sample(List.of("default"), Double.NaN, "region:west");
HydrologyTerrainSample east = sample(List.of("default"), Double.NaN, "region:east");
assertNull(open.confinesKey());
assertTrue(open.drainsInto(west));
assertTrue(west.drainsInto(west));
assertFalse(west.drainsInto(east));
assertFalse(west.drainsInto(open));
}
private static HydrologyTerrainSample sample(List<String> profiles, double shoreBiomeWidth, String confinesKey) {
return new HydrologyTerrainSample(
80, 0D, false, false, 48, 50, true, true, true, false, false, false,
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
"parent", "parent", "parent", "parent", "parent", "parent",
profiles, List.of(), shoreBiomeWidth, confinesKey
);
}
}
@@ -8,6 +8,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -247,7 +248,7 @@ public class HydrologyTileCacheTest {
new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(3),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
new HydrologyPlannerSettings.Outlets(
false,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -256,10 +257,13 @@ public class HydrologyTileCacheTest {
8,
16,
1,
2,
2
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(), List.of()
List.of(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -285,7 +289,7 @@ public class HydrologyTileCacheTest {
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()),
new HydrologyPlannerSettings.Hydraulics(3),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false, 0),
new HydrologyPlannerSettings.Outlets(
true,
new HydrologyPlannerSettings.Grotto(false, 2, 2, 2, 512),
@@ -294,16 +298,20 @@ public class HydrologyTileCacheTest {
8,
16,
1,
2,
2
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(), List.of()
List.of(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
// Small-tile settings need a blend envelope that fits inside the bounded cross-tile admission period.
private static HydrologyPlannerSettings.Banks tileBoundedBanks() {
return new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true);
return new HydrologyPlannerSettings.Banks(0, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults());
}
private HydrologyTerrainSample featureTerrain(int x, int z) {
@@ -338,7 +346,9 @@ public class HydrologyTileCacheTest {
"shore",
"dry",
"flooded",
List.of("default"), List.of()
List.of("default"), List.of(),
Double.NaN,
null
);
}
@@ -419,4 +429,234 @@ public class HydrologyTileCacheTest {
assertEquals(new HydrologyTileKey(1, 1), order.getFirst());
assertTrue(order.subList(0, 4).containsAll(List.of(new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(0, 1), new HydrologyTileKey(1, 1))));
}
@Test
public void tileBatchesPlanMissingTilesConcurrentlyOnThePrefetchExecutor() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
AtomicInteger inFlight = new AtomicInteger();
AtomicInteger peak = new AtomicInteger();
CountDownLatch allStarted = new CountDownLatch(4);
doAnswer(invocation -> {
peak.accumulateAndGet(inFlight.incrementAndGet(), Math::max);
allStarted.countDown();
assertTrue("every batch member should plan at the same time", allStarted.await(5, TimeUnit.SECONDS));
inFlight.decrementAndGet();
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, executor);
List<HydrologyTileKey> keys = List.of(
new HydrologyTileKey(0, 0),
new HydrologyTileKey(1, 0),
new HydrologyTileKey(2, 0),
new HydrologyTileKey(3, 0)
);
List<HydrologyTile> tiles = cache.tiles(keys);
assertEquals(4, tiles.size());
assertEquals(4, peak.get());
verify(planner, times(4)).plan(any(HydrologyTileKey.class));
assertEquals(4, cache.size());
} finally {
executor.shutdownNow();
}
}
@Test
public void tileBatchesNeverKeepMoreThanHalfTheCacheBoundInFlight() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
AtomicInteger inFlight = new AtomicInteger();
AtomicInteger peak = new AtomicInteger();
List<HydrologyTileKey> planned = java.util.Collections.synchronizedList(new ArrayList<>());
doAnswer(invocation -> {
peak.accumulateAndGet(inFlight.incrementAndGet(), Math::max);
Thread.sleep(20L);
planned.add(invocation.getArgument(0));
inFlight.decrementAndGet();
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService executor = Executors.newFixedThreadPool(8);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, executor);
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
for (int tileX = 0; tileX < 6; tileX++) {
keys.add(new HydrologyTileKey(tileX, 0));
}
List<HydrologyTile> tiles = cache.tiles(keys);
assertEquals(6, tiles.size());
assertEquals(6, planned.size());
assertTrue("in flight " + peak.get(), peak.get() <= 2);
} finally {
executor.shutdownNow();
}
}
@Test
public void tileBatchesWithoutAPrefetchExecutorPlanInlineInKeyOrder() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
List<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return mock(HydrologyTile.class);
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
List<HydrologyTileKey> keys = List.of(new HydrologyTileKey(2, 1), new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 1));
List<HydrologyTile> tiles = cache.tiles(keys);
assertEquals(keys, planned);
assertEquals(3, tiles.size());
assertSame(cache.get(keys.get(1)), tiles.get(1));
}
@Test
public void tileBatchesJoinAPlanAlreadyInFlightInsteadOfQueueingAnother() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
CountDownLatch release = new CountDownLatch(1);
AtomicInteger plans = new AtomicInteger();
doAnswer(invocation -> {
plans.incrementAndGet();
assertTrue(release.await(5, TimeUnit.SECONDS));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, executor);
HydrologyTileKey key = new HydrologyTileKey(5, 5);
ExecutorService callers = Executors.newFixedThreadPool(3);
try {
List<Future<List<HydrologyTile>>> results = new ArrayList<>();
for (int caller = 0; caller < 3; caller++) {
results.add(callers.submit(() -> cache.tiles(List.of(key))));
}
Thread.sleep(100L);
release.countDown();
for (Future<List<HydrologyTile>> result : results) {
assertSame(tile, result.get(5, TimeUnit.SECONDS).getFirst());
}
} finally {
callers.shutdownNow();
}
assertEquals(1, plans.get());
} finally {
executor.shutdownNow();
}
}
@Test
public void interruptedPlanningIsNotPublishedAsAnEmptyTile() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
AtomicInteger plans = new AtomicInteger();
doAnswer(invocation -> {
if (plans.incrementAndGet() == 1) {
throw new IllegalStateException("Interrupted", new InterruptedException());
}
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
HydrologyTileKey key = new HydrologyTileKey(3, 4);
IllegalStateException interrupted = org.junit.Assert.assertThrows(IllegalStateException.class, () -> cache.get(key));
assertEquals("Interrupted", interrupted.getMessage());
assertEquals(0, cache.size());
assertSame(tile, cache.get(key));
assertEquals(2, plans.get());
verify(planner, org.mockito.Mockito.never()).emptyTile(any(HydrologyTileKey.class));
}
@Test
public void directTileRequestsPlanOnThePrefetchExecutorInsteadOfTheCaller() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
List<String> planningThreads = java.util.Collections.synchronizedList(new ArrayList<>());
doAnswer(invocation -> {
planningThreads.add(Thread.currentThread().getName());
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "hydrology-planning-executor"));
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, executor);
assertSame(tile, cache.get(new HydrologyTileKey(7, 7)));
assertEquals(List.of("hydrology-planning-executor"), planningThreads);
} finally {
executor.shutdownNow();
}
}
@Test
public void forkJoinWorkersPlanOnTheirOwnThreadInsteadOfTheExecutor() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
List<String> planningThreads = java.util.Collections.synchronizedList(new ArrayList<>());
doAnswer(invocation -> {
planningThreads.add(Thread.currentThread().getName());
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "hydrology-planning-executor"));
java.util.concurrent.ForkJoinPool pool = new java.util.concurrent.ForkJoinPool(1);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, executor);
HydrologyTile planned = pool.submit(() -> cache.get(new HydrologyTileKey(9, 9))).get(5, TimeUnit.SECONDS);
List<HydrologyTile> batch = pool.submit(() -> cache.tiles(List.of(new HydrologyTileKey(10, 9), new HydrologyTileKey(11, 9)))).get(5, TimeUnit.SECONDS);
assertSame(tile, planned);
assertEquals(2, batch.size());
assertEquals(3, planningThreads.size());
assertTrue(planningThreads.toString(), planningThreads.stream().allMatch(name -> name.startsWith("ForkJoinPool-")));
} finally {
pool.shutdownNow();
executor.shutdownNow();
}
}
@Test
public void plannedCheckNeverPlansItselfButAsksThePrefetchExecutorForMissingTiles() {
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);
assertFalse(cache.isPlanned(40, 40));
assertFalse(queued.isEmpty());
verify(planner, org.mockito.Mockito.never()).plan(any(HydrologyTileKey.class));
for (Runnable task : List.copyOf(queued)) {
task.run();
}
assertTrue(cache.isPlanned(40, 40));
assertTrue(cache.columnAt(40, 40).isEmpty());
}
@Test
public void plannedCheckWithoutAnExecutorReportsTheCacheOnly() {
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());
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
assertFalse(cache.isPlanned(40, 40));
cache.columnAt(40, 40);
assertTrue(cache.isPlanned(40, 40));
}
}
@@ -13,6 +13,7 @@ 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.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -150,4 +151,29 @@ public class RiverPolicyResolverTest {
private EffectiveRiverPolicy resolvePlacement(IrisRiverPlacementMode placement) {
return RiverPolicyResolver.resolve(new IrisRiverPolicy().setPlacement(placement), null, null);
}
@Test
public void shoreBiomeWidthInheritsFromDimensionThroughRegionToBiome() {
IrisRiverPolicy region = new IrisRiverPolicy().setShoreBiomeWidth(6D);
IrisRiverPolicy biome = new IrisRiverPolicy().setShoreBiomeWidth(0D);
assertNull(RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null).shoreBiomeWidth());
assertEquals(6D, RiverPolicyResolver.resolve(null, region, null).shoreBiomeWidth(), 0D);
assertEquals(6D, RiverPolicyResolver.resolve(null, region, new IrisRiverPolicy()).shoreBiomeWidth(), 0D);
assertEquals(0D, RiverPolicyResolver.resolve(null, region, biome).shoreBiomeWidth(), 0D);
}
@Test
public void confinementScopeIsTheLevelThatConfinedTheRiver() {
IrisRiverPolicy confined = new IrisRiverPolicy().setConfined(true);
IrisRiverPolicy released = new IrisRiverPolicy().setConfined(false);
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve((IrisRiverPolicy) null, null, null).confinement());
assertEquals(RiverConfinement.REGION, RiverPolicyResolver.resolve(confined, null, null).confinement());
assertEquals(RiverConfinement.REGION, RiverPolicyResolver.resolve(null, confined, null).confinement());
assertEquals(RiverConfinement.REGION, RiverPolicyResolver.resolve(null, confined, new IrisRiverPolicy()).confinement());
assertEquals(RiverConfinement.BIOME, RiverPolicyResolver.resolve(null, null, confined).confinement());
assertEquals(RiverConfinement.BIOME, RiverPolicyResolver.resolve(null, confined, confined).confinement());
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(null, confined, released).confinement());
assertEquals(RiverConfinement.NONE, RiverPolicyResolver.resolve(confined, released, null).confinement());
}
}
@@ -440,7 +440,7 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
}
@Test
public void burstWorkerUsesAvailableParallelRows() throws Exception {
public void burstWorkerForksRowsIntoItsOwnPoolInsteadOfTheExecutor() throws Exception {
AtomicInteger submissions = new AtomicInteger();
AtomicInteger heightCalls = new AtomicInteger();
MultiBurst burst = new MultiBurst("Iris Hydrology Test", Thread.NORM_PRIORITY, () -> 2);
@@ -471,7 +471,8 @@ public class IrisHydrologyRoutingTerrainSamplerTest {
);
completion.get(5L, TimeUnit.SECONDS);
assertTrue(submissions.get() > 1);
// Handing rows to another pool from a pool worker is how saturated pools deadlock each other.
assertEquals(0, submissions.get());
assertEquals((width + 1) * (width + 1), heightCalls.get());
} finally {
burst.close();
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig;
import art.arcane.iris.engine.object.IrisDeepFluidConfig;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRiverHydrology;
@@ -43,12 +44,12 @@ public class IrisHydrologyRuntimeSettingsTest {
rivers.getGrottos().getInland().setEnabled(true).setConnectSurfaceRivers(true);
rivers.getRouting().getInlandOutlets().add(IrisRiverInlandOutlet.SINKHOLE_GROTTO);
assertTrue(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
assertTrue(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.outlets().surfaceSinkholesEnabled());
rivers.getRouting().getInlandOutlets().clear();
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.outlets().surfaceSinkholesEnabled());
}
@@ -63,13 +64,13 @@ public class IrisHydrologyRuntimeSettingsTest {
rivers.getUnderground().getSources().setDensity(4.5D);
rivers.getRouting().setSampleSpacing(64);
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.surface().sources().maximumPerTile());
assertEquals(384, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
assertEquals(384, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.surface().sources().minimumSpacing());
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
assertEquals(5, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.underground().sources().maximumPerTile());
assertEquals(512, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
assertEquals(512, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.underground().sources().minimumSpacing());
}
@@ -77,16 +78,23 @@ public class IrisHydrologyRuntimeSettingsTest {
public void surfaceBankAndFlowConfigurationMapsIntoPlannerSettings() {
IrisDimension dimension = new IrisDimension();
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
rivers.getSurface().getChannel().setInset(2).setMaximumIncision(12);
rivers.getSurface().getChannel().setSink(2).setMaximumIncision(12);
rivers.getSurface().getErosion()
.setEnabled(false)
.setSmoothingRadius(6)
.setThalwegFraction(0.6D)
.setBlendCurve(1.5D)
.setBedNoise(0.25D);
rivers.getSurface().getPonds().getSource().setEnabled(false).setMinimumRadius(3).setMaximumRadius(4).setDepth(2);
rivers.getSurface().getPonds().getTerminal().setMinimumRadius(7).setMaximumRadius(11).setDepth(5);
rivers.getSurface().getBanks()
.setFreeboard(2)
.setShoreWidth(2.5D)
.setBlendSlope(4D)
.setMinimumBlendWidth(6)
.setMaximumBlendWidth(48)
.setExposeCutStrata(false);
rivers.getSurface().getFlow().setCascadeRun(3).setWaterfallMinimumDrop(8);
rivers.getSurface().getMouths().setFlareRatio(2.2D);
rivers.getSurface().getMouths().setFlareRatio(2.2D).setInletLength(96).setInletDepth(5).setMaximumIncision(40);
rivers.getUnderground().setMouthLevelingDistance(96);
rivers.getRouting()
.setSampleSpacing(32)
@@ -97,17 +105,30 @@ public class IrisHydrologyRuntimeSettingsTest {
.setSlopePenalty(3D)
.setConfluenceAttraction(0.4D);
HydrologyPlannerSettings settings = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology());
HydrologyPlannerSettings settings = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null);
HydrologyPlannerSettings.Banks banks = settings.surface().banks();
assertEquals(2, banks.inset());
assertEquals(2, banks.freeboard());
assertEquals(2, banks.sink());
assertFalse(banks.erosion().enabled());
assertEquals(6, banks.erosion().smoothingRadius());
assertEquals(0.6D, banks.erosion().thalwegFraction(), 0D);
assertEquals(1.5D, banks.erosion().blendCurve(), 0D);
assertEquals(0.25D, banks.erosion().bedNoise(), 0D);
assertFalse(banks.ponds().source().enabled());
assertEquals(3, banks.ponds().source().minimumRadius());
assertEquals(4, banks.ponds().source().maximumRadius());
assertEquals(2, banks.ponds().source().depth());
assertTrue(banks.ponds().terminal().enabled());
assertEquals(7, banks.ponds().terminal().minimumRadius());
assertEquals(11, banks.ponds().terminal().maximumRadius());
assertEquals(5, banks.ponds().terminal().depth());
assertEquals(4D, banks.blendSlope(), 0D);
assertEquals(6, banks.minimumBlendWidth());
assertEquals(48, banks.maximumBlendWidth());
assertEquals(3, banks.cascadeRun());
assertEquals(8, banks.waterfallMinimumDrop());
assertEquals(2.2D, banks.mouthFlareRatio(), 0D);
assertEquals(new HydrologyPlannerSettings.Inlet(96, 5, 40), banks.inlet());
assertFalse(banks.exposeCutStrata());
assertEquals(12, settings.surface().maximumIncision());
assertEquals(2.5D, settings.surface().shoreWidth(), 0D);
@@ -127,10 +148,16 @@ public class IrisHydrologyRuntimeSettingsTest {
@Test
public void routingOutletLimitControlsDrainageRootCount() {
IrisDimension dimension = new IrisDimension();
dimension.getHydrology().getRivers().getRouting().setMaximumOutletsPerTile(2);
dimension.getHydrology().getRivers().getRouting()
.setMaximumOutletsPerTile(2)
.setMaximumCoastalOutletsPerTile(5);
assertEquals(2, IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology())
.outlets().maximumPerTile());
HydrologyPlannerSettings.Outlets outlets = IrisHydrologyRuntime
.createSettings(dimension, dimension.getHydrology(), () -> null)
.outlets();
assertEquals(2, outlets.maximumPerTile());
assertEquals(5, outlets.maximumCoastalPerTile());
}
@Test
@@ -158,7 +185,7 @@ public class IrisHydrologyRuntimeSettingsTest {
.setMaximumBasinDepth(9);
HydrologyPlannerSettings.Geometry geometry = IrisHydrologyRuntime
.createSettings(dimension, dimension.getHydrology())
.createSettings(dimension, dimension.getHydrology(), () -> null)
.geometry();
assertEquals(47, geometry.meanders().primaryWavelength());
@@ -179,4 +206,29 @@ public class IrisHydrologyRuntimeSettingsTest {
assertEquals(2.1D, geometry.drops().basinWidthRatio(), 0D);
assertEquals(9, geometry.drops().maximumBasinDepth());
}
@Test
public void seaCaveConfigurationMapsIntoPlannerSettings() {
IrisDimension dimension = new IrisDimension();
IrisRiverHydrology rivers = dimension.getHydrology().getRivers();
rivers.setEnabled(true);
IrisCoastalRiverGrottoConfig coastal = rivers.getGrottos().getCoastal();
coastal.setEnabled(true);
coastal.getSeaCaves().setEnabled(true).setMaximumPerTile(5).setMinimumSpacing(200).setMinimumCoastHeight(9).setDepth(20);
HydrologyPlannerSettings.SeaCaves mapped = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.seaCaves();
assertEquals(new HydrologyPlannerSettings.SeaCaves(true, 5, 200, 9, 20), mapped);
coastal.setEnabled(false);
HydrologyPlannerSettings.SeaCaves withoutCoastalGrottos = IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null)
.seaCaves();
assertFalse(withoutCoastalGrottos.enabled());
assertEquals(5, withoutCoastalGrottos.maximumPerTile());
assertEquals(20, withoutCoastalGrottos.depth());
coastal.setEnabled(true);
coastal.getSeaCaves().setEnabled(false);
assertFalse(IrisHydrologyRuntime.createSettings(dimension, dimension.getHydrology(), () -> null).seaCaves().enabled());
}
}
@@ -40,11 +40,47 @@ public class ChannelProfileBuilderTest {
}
@Test
public void directOceanCoursesFlareAtTheMouth() {
public void theInletWidensAndDeepensTowardTheCoastOverItsLength() {
SurfaceCenterline centerline = straight(400);
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", true);
ChannelProfile plain = builder(defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()))
.build(centerline, "water", true);
assertEquals(3D * 1.6D, profile.halfWidth()[399], 0.05D);
assertEquals(64, inlet.length());
int flareStart = 400 - inlet.length();
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
assertEquals(3D + inlet.depth(), profile.depth()[399], 1.0E-9D);
assertEquals(3D, profile.halfWidth()[flareStart], 1.0E-9D);
assertEquals(3D, profile.depth()[flareStart], 1.0E-9D);
assertTrue(profile.halfWidth()[flareStart + 1] > 3D);
assertTrue(profile.depth()[flareStart + 1] > 3D);
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
for (int station = flareStart + 1; station < 400; station++) {
assertTrue(profile.halfWidth()[station] >= profile.halfWidth()[station - 1]);
assertTrue(profile.depth()[station] >= profile.depth()[station - 1]);
}
assertEquals(3D, plain.halfWidth()[399], 1.0E-9D);
assertEquals(3D, plain.depth()[399], 1.0E-9D);
}
@Test
public void theFlareCompletesAtTheShorelineWhenTheCenterlineRunsIntoTheSea() {
SurfaceCenterline centerline = straight(400);
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
HydrologyTerrainSampler coast = (int x, int z) -> x >= 380
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(80, 0D, "land");
ChannelProfile profile = new ChannelProfileBuilder(defaults, coast, CONSTANT_GEOMETRY).build(centerline, "water", true);
int flareStart = 380 - inlet.length();
assertEquals(3D, profile.halfWidth()[flareStart], 1.0E-9D);
assertTrue(profile.halfWidth()[flareStart + 1] > 3D);
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[379], 1.0E-9D);
assertEquals(3D + inlet.depth(), profile.depth()[379], 1.0E-9D);
assertEquals(3D * defaults.banks().mouthFlareRatio(), profile.halfWidth()[399], 1.0E-9D);
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
}
@@ -76,6 +112,14 @@ public class ChannelProfileBuilderTest {
return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80);
}
private static ChannelProfileBuilder builder(HydrologyPlannerSettings.Banks banks) {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), banks);
return new ChannelProfileBuilder(surface, (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land"), CONSTANT_GEOMETRY);
}
private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier, IntBinaryOperator height) {
HydrologyTerrainSampler sampler = (int x, int z) -> new HydrologyTerrainSample(
height.applyAsInt(x, z),
@@ -104,7 +148,9 @@ public class ChannelProfileBuilderTest {
"parent",
"parent",
"parent",
List.of("water"), List.of()
List.of("water"), List.of(),
Double.NaN,
null
);
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY);
}
@@ -25,20 +25,20 @@ public class ErosionFieldCompilerTest {
};
@Test
public void flatTerrainProducesAContainedChannelWithLipAndNoWriteAboveNatural() {
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, compiled.field().uncontainedWetCells());
SurfaceColumn center = compiled.field().column(150, 0);
assertNotNull(center);
assertEquals(SurfaceRole.CHANNEL, center.role());
assertEquals(79, center.headY());
assertTrue(center.height() <= 76);
assertEquals(80, center.headY());
assertTrue(center.height() <= 77);
SurfaceColumn edge = compiled.field().column(150, 3);
assertNotNull(edge);
assertEquals(SurfaceRole.CHANNEL, edge.role());
assertTrue(edge.height() > center.height());
assertTrue(edge.height() <= 78);
assertTrue(edge.height() <= 79);
SurfaceColumn shore = compiled.field().column(150, 4);
assertNotNull(shore);
assertEquals(SurfaceRole.SHORE, shore.role());
@@ -47,6 +47,71 @@ public class ErosionFieldCompilerTest {
assertChannelContained(compiled);
}
@Test
public void aConfiguredSinkLowersTheWaterBelowTheBankByThatMuch() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
Compiled sunk = compile(zeroRoughnessSurface(1, HydrologyPlannerSettings.Erosion.defaults()), flat, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, sunk.field().uncontainedWetCells());
assertEquals(79, sunk.field().column(150, 0).headY());
SurfaceColumn shore = sunk.field().column(150, 4);
assertEquals(SurfaceRole.SHORE, shore.role());
assertEquals(80, shore.height());
assertChannelContained(sunk);
}
@Test
public void disabledErosionKeepsTheChannelShoreAndLipButNoValley() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
HydrologyPlannerSettings.Erosion off = new HydrologyPlannerSettings.Erosion(false, 12, 0.45D, 1D, 0.5D);
Compiled compiled = compile(zeroRoughnessSurface(0, off), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, compiled.field().uncontainedWetCells());
assertChannelContained(compiled);
assertNoWriteAboveNatural(compiled);
for (SurfaceColumn column : compiled.field().columns().values()) {
assertTrue(column.role() != SurfaceRole.BANK);
}
assertNotNull(compiled.field().column(150, 0));
assertEquals(SurfaceRole.SHORE, compiled.field().column(150, 4).role());
}
@Test
public void erosionDefaultsReproduceTheValleyBitForBitAndACurveReshapesIt() {
HydrologyTerrainSampler hillside = (int x, int z) -> HydrologyTerrainSample.openLand(90 + Math.max(0, z) / 2, 0D, "land");
Compiled defaults = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults()), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled explicit = compile(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D)), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled steep = compile(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 2D, 0.5D)), hillside, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(defaults.field().columns().size(), explicit.field().columns().size());
for (SurfaceColumn column : defaults.field().columns().values()) {
SurfaceColumn same = explicit.field().column(column.x(), column.z());
assertNotNull(same);
assertEquals(column.height(), same.height());
assertEquals(column.headY(), same.headY());
assertEquals(column.role(), same.role());
}
boolean differs = false;
for (SurfaceColumn column : defaults.field().columns().values()) {
SurfaceColumn other = steep.field().column(column.x(), column.z());
if (column.role() == SurfaceRole.BANK && other != null && other.height() != column.height()) {
differs = true;
break;
}
}
assertTrue(differs);
assertChannelContained(steep);
assertNoWriteAboveNatural(steep);
}
@Test
public void blendCurveOneIsTheEasedBlendAndHigherCurvesHoldTheBankTopLonger() {
assertEquals(SurfaceNoise.smoothStep(0.3D), ErosionFieldCompiler.blend(0.3D, 1D), 0D);
assertTrue(ErosionFieldCompiler.blend(0.3D, 2D) < ErosionFieldCompiler.blend(0.3D, 1D));
assertTrue(ErosionFieldCompiler.blend(0.3D, 0.5D) > ErosionFieldCompiler.blend(0.3D, 1D));
assertEquals(1D, ErosionFieldCompiler.blend(1D, 3D), 0D);
}
@Test
public void hillsideCutGetsABankWhoseWidthGrowsWithTheCut() {
Compiled compiled = compile(300, (x, z) -> 90 + Math.max(0, z) / 2, SurfaceTerminal.SINKHOLE, 40);
@@ -115,6 +180,77 @@ public class ErosionFieldCompilerTest {
assertNoWriteAboveNatural(compiled);
}
@Test
public void inletBanksAreCutToSeaLevelAndBlendBackToTheNaturalValley() {
HydrologyTerrainSampler coast = (int x, int z) -> {
if (x >= 240) {
return HydrologyTerrainSample.ocean(50, "ocean");
}
int height = x < 200 ? 100 - x / 6 : 66 + (x - 200) * 18 / 40;
return HydrologyTerrainSample.openLand(height, 0D, "land");
};
HydrologyPlannerSettings.Inlet inlet = new HydrologyPlannerSettings.Inlet(32, 3, 32);
HydrologyPlannerSettings.Surface surface = withBanks(zeroRoughnessSurface(), zeroRoughnessSurface().banks().withInlet(inlet));
Compiled compiled = compile(surface, coast, 244, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertEquals(0, compiled.field().uncontainedWetCells());
assertNoWriteAboveNatural(compiled);
assertChannelContained(compiled);
int oceanWrites = 0;
for (SurfaceColumn column : compiled.field().columns().values()) {
HydrologyTerrainSample terrain = coast.sample(column.x(), column.z());
if ((terrain.ocean() || terrain.naturalHeight() <= SEA_LEVEL) && !(column.apron() && column.height() == terrain.naturalHeight())) {
oceanWrites++;
}
}
assertEquals(0, oceanWrites);
for (int x = 240 - inlet.length(); x < 240; x++) {
SurfaceColumn center = compiled.field().column(x, 0);
assertNotNull(center);
assertEquals(SurfaceRole.CHANNEL, center.role());
assertEquals(SEA_LEVEL, center.headY());
assertTrue(center.height() < SEA_LEVEL);
}
int pinned = 220;
int shoreZ = -1;
for (int z = 1; z < 12; z++) {
SurfaceColumn column = compiled.field().column(pinned, z);
assertNotNull(column);
if (column.role() != SurfaceRole.CHANNEL) {
shoreZ = z;
break;
}
}
assertTrue(shoreZ > 0);
SurfaceColumn shore = compiled.field().column(pinned, shoreZ);
assertEquals(SurfaceRole.SHORE, shore.role());
assertEquals(SEA_LEVEL, shore.height());
assertTrue(coast.sample(pinned, shoreZ).naturalHeight() > SEA_LEVEL + 10);
int previous = SEA_LEVEL;
int banks = 0;
int lastHeight = SEA_LEVEL;
for (int z = shoreZ + 1; z < 60; z++) {
SurfaceColumn column = compiled.field().column(pinned, z);
if (column == null) {
break;
}
assertTrue(column.role() == SurfaceRole.SHORE || column.role() == SurfaceRole.BANK);
assertTrue(column.height() >= previous);
assertTrue(column.height() - previous <= 1);
previous = column.height();
lastHeight = column.height();
banks++;
}
assertTrue(banks > 8);
assertTrue(lastHeight >= coast.sample(pinned, 0).naturalHeight() - 1);
}
private static HydrologyPlannerSettings.Surface withBanks(HydrologyPlannerSettings.Surface surface, HydrologyPlannerSettings.Banks banks) {
return new HydrologyPlannerSettings.Surface(
surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), banks);
}
@Test
public void bankColumnsBeyondTheBlendAreNotWritten() {
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
@@ -154,7 +290,16 @@ public class ErosionFieldCompilerTest {
}
private static Compiled compile(HydrologyTerrainSampler sampler, int stations, SurfaceTerminal terminal, int terminalHead) {
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
return compile(zeroRoughnessSurface(), sampler, stations, terminal, terminalHead);
}
private static Compiled compile(
HydrologyPlannerSettings.Surface surface,
HydrologyTerrainSampler sampler,
int stations,
SurfaceTerminal terminal,
int terminalHead
) {
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, CONSTANT_GEOMETRY)
@@ -168,12 +313,26 @@ public class ErosionFieldCompilerTest {
}
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface() {
return zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults());
}
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface(int sink, HydrologyPlannerSettings.Erosion erosion) {
return zeroRoughnessSurface(sink, erosion, new HydrologyPlannerSettings.Ponds(
new HydrologyPlannerSettings.Pond(false, 5, 9, 3),
new HydrologyPlannerSettings.Pond(false, 4, 7, 3)));
}
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface(
int sink,
HydrologyPlannerSettings.Erosion erosion,
HydrologyPlannerSettings.Ponds ponds
) {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
return new HydrologyPlannerSettings.Surface(
true,
defaults.sources(),
4, 8, 2, 4, 10, 1.5D,
new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, 2.5D, 24, true)
new HydrologyPlannerSettings.Banks(sink, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true, erosion, ponds)
);
}
@@ -210,14 +369,14 @@ public class ErosionFieldCompilerTest {
@Test
public void bedBowlIsLevelAcrossTheThalwegAndOneBlockAtTheEdge() {
assertEquals(1D, ErosionFieldCompiler.bowl(0D), 1e-9);
assertEquals(1D, ErosionFieldCompiler.bowl(0.45D), 1e-9);
assertTrue(ErosionFieldCompiler.bowl(0.6D) > 0.8D);
assertTrue(ErosionFieldCompiler.bowl(0.8D) > 0.3D && ErosionFieldCompiler.bowl(0.8D) < 0.7D);
assertEquals(0D, ErosionFieldCompiler.bowl(1D), 1e-9);
assertEquals(1D, ErosionFieldCompiler.bowl(0D, 0.45D), 1e-9);
assertEquals(1D, ErosionFieldCompiler.bowl(0.45D, 0.45D), 1e-9);
assertTrue(ErosionFieldCompiler.bowl(0.6D, 0.45D) > 0.8D);
assertTrue(ErosionFieldCompiler.bowl(0.8D, 0.45D) > 0.3D && ErosionFieldCompiler.bowl(0.8D, 0.45D) < 0.7D);
assertEquals(0D, ErosionFieldCompiler.bowl(1D, 0.45D), 1e-9);
double previous = 1D;
for (double normalized = 0D; normalized <= 1D; normalized += 0.05D) {
double value = ErosionFieldCompiler.bowl(normalized);
double value = ErosionFieldCompiler.bowl(normalized, 0.45D);
assertTrue(value <= previous + 1e-9);
previous = value;
}
@@ -226,7 +385,7 @@ public class ErosionFieldCompilerTest {
@Test
public void everyCellTouchingWaterKeepsTheLipAcrossHeadSteps() {
Compiled compiled = compile(400, (x, z) -> 120 - x / 8, SurfaceTerminal.SINKHOLE, 40);
int freeboard = zeroRoughnessSurface().banks().freeboard();
int sink = zeroRoughnessSurface().banks().sink();
int checked = 0;
for (SurfaceColumn column : compiled.field().columns().values()) {
@@ -239,7 +398,7 @@ public class ErosionFieldCompilerTest {
if (neighbour == null || neighbour.role() == SurfaceRole.CHANNEL) {
continue;
}
int required = Math.min(neighbour.terrain().naturalHeight(), column.headY() + freeboard);
int required = Math.min(neighbour.terrain().naturalHeight(), column.headY() + sink);
assertTrue(neighbour.x() + "," + neighbour.z() + " height " + neighbour.height() + " < " + required,
neighbour.height() >= required);
checked++;
@@ -250,16 +409,34 @@ public class ErosionFieldCompilerTest {
}
@Test
public void aLowCellBesideTheChannelLowersTheHeadSoTheLipSurvives() {
public void aDipOneBlockBelowTheWaterAnywhereBesideTheChannelIsHeldByALip() {
// Whatever the solver sampled, no water cell may border a lower dry cell: a one-block dip
// beside the channel is either answered by a lower head or by a one-block lip on the dip.
for (int dipZ = 2; dipZ <= 6; dipZ++) {
for (int dipX = 148; dipX <= 152; dipX++) {
int finalZ = dipZ;
int finalX = dipX;
Compiled compiled = compile(300, (x, z) -> x == finalX && z == finalZ ? 79 : 80, SurfaceTerminal.SINKHOLE, 40);
assertEquals("dip at " + finalX + "," + finalZ, 0, compiled.field().uncontainedWetCells());
SurfaceColumn dip = compiled.field().column(finalX, finalZ);
if (dip != null) {
assertTrue("lip taller than one block at " + finalX + "," + finalZ, dip.height() <= 80);
}
}
}
}
@Test
public void aLowCellBesideTheChannelLowersTheHeadSoTheBankStillHoldsIt() {
Compiled compiled = compile(300, (x, z) -> x == 150 && z == 4 ? 79 : 80, SurfaceTerminal.SINKHOLE, 40);
SurfaceColumn center = compiled.field().column(150, 0);
assertNotNull(center);
assertTrue("head " + center.headY(), center.headY() <= 78);
assertTrue("head " + center.headY(), center.headY() <= 79);
SurfaceColumn low = compiled.field().column(150, 4);
assertEquals(SurfaceRole.SHORE, low.role());
assertNotNull(low);
assertTrue(low.height() >= center.headY() + 1);
assertTrue(low.height() >= center.headY());
assertEquals(0, compiled.field().uncontainedWetCells());
}
@@ -292,4 +469,145 @@ public class ErosionFieldCompilerTest {
assertEquals(200, firstApron);
assertEquals(202, lastApron);
}
@Test
public void policyShoreBiomeWidthWidensTheShoreRoleOverUntouchedGround() {
Compiled compiled = compile((int x, int z) -> land(80, 12D), 300, SurfaceTerminal.SINKHOLE, 40);
SurfaceColumn geometricShore = compiled.field().column(150, 4);
assertNotNull(geometricShore);
assertEquals(SurfaceRole.SHORE, geometricShore.role());
SurfaceColumn band = compiled.field().column(150, 12);
assertNotNull(band);
assertEquals(SurfaceRole.SHORE, band.role());
assertEquals(80, band.height());
assertNull(compiled.field().column(150, 17));
assertNoWriteAboveNatural(compiled);
assertChannelContained(compiled);
}
@Test
public void zeroShoreBiomeWidthLeavesTheGeometricShoreWithTheBankRole() {
Compiled compiled = compile((int x, int z) -> land(80, 0D), 300, SurfaceTerminal.SINKHOLE, 40);
SurfaceColumn geometricShore = compiled.field().column(150, 4);
assertNotNull(geometricShore);
assertEquals(SurfaceRole.BANK, geometricShore.role());
assertEquals(80, geometricShore.height());
assertNull(compiled.field().column(150, 6));
assertChannelContained(compiled);
}
private static HydrologyTerrainSample land(int height, double shoreBiomeWidth) {
HydrologyTerrainSample open = HydrologyTerrainSample.openLand(height, 0D, "land");
return new HydrologyTerrainSample(
open.naturalHeight(), open.slope(), open.ocean(), open.caveAvailable(), open.caveFloorY(), open.caveFluidY(),
open.transitAllowed(), open.outletAllowed(), open.surfaceSourceAllowed(), open.surfaceSourceRequired(),
open.undergroundSourceAllowed(), open.undergroundSourceRequired(), open.routingCost(),
open.surfaceSourceWeight(), open.undergroundSourceWeight(), open.widthMultiplier(), open.depthMultiplier(),
open.incisionMultiplier(), open.routingMultiplier(), open.bankMultiplier(), open.parentBiomeKey(),
open.surfaceBiomeKey(), open.mouthBiomeKey(), open.shoreBiomeKey(), open.bankBiomeKey(),
open.floodedCaveBiomeKey(), open.preferredProfileKeys(), open.surfacePoolKeys(), shoreBiomeWidth, null
);
}
@Test
public void aSourcePondOpensAroundTheHeadwaterAtTheSourceHead() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
new HydrologyPlannerSettings.Pond(true, 10, 10, 3),
new HydrologyPlannerSettings.Pond(false, 4, 7, 3));
Compiled compiled = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), flat, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled plain = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, compiled.field().uncontainedWetCells());
assertChannelContained(compiled);
assertNoWriteAboveNatural(compiled);
SurfaceColumn centre = compiled.field().column(0, 0);
assertEquals(SurfaceRole.CHANNEL, centre.role());
assertEquals(80, centre.headY());
assertTrue(centre.height() <= 77);
SurfaceColumn behind = compiled.field().column(-9, 0);
assertNotNull(behind);
assertEquals(SurfaceRole.CHANNEL, behind.role());
assertEquals(80, behind.headY());
SurfaceColumn beside = compiled.field().column(0, 9);
assertEquals(SurfaceRole.CHANNEL, beside.role());
assertEquals(0, behind.station());
SurfaceColumn plainBehind = plain.field().column(-9, 0);
assertTrue(plainBehind == null || plainBehind.role() != SurfaceRole.CHANNEL);
assertEquals(SurfaceRole.SHORE, compiled.field().column(-11, 0).role());
assertNotNull(compiled.field().column(150, 0));
assertEquals(SurfaceRole.CHANNEL, compiled.field().column(150, 0).role());
assertTrue(compiled.field().column(150, 4).role() == SurfaceRole.SHORE);
}
@Test
public void aTerminalPondOpensAtAnInlandEndButNotAtAnOceanMouth() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
new HydrologyPlannerSettings.Pond(false, 5, 9, 3),
new HydrologyPlannerSettings.Pond(true, 5, 5, 2));
Compiled inland = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), flat, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, inland.field().uncontainedWetCells());
assertChannelContained(inland);
SurfaceColumn end = inland.field().column(299, 0);
assertEquals(SurfaceRole.CHANNEL, end.role());
assertTrue(end.height() <= 78);
SurfaceColumn beyond = inland.field().column(303, 0);
assertNotNull(beyond);
assertEquals(SurfaceRole.CHANNEL, beyond.role());
assertEquals(299, beyond.station());
SurfaceColumn plainBeyond = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40).field().column(303, 0);
assertTrue(plainBeyond == null || plainBeyond.role() != SurfaceRole.CHANNEL);
HydrologyTerrainSampler coast = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
Compiled mouth = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), coast, 300, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
int wetBeyondTheShore = 0;
for (SurfaceColumn column : mouth.field().columns().values()) {
if (column.role() == SurfaceRole.CHANNEL && !column.apron() && column.x() >= 236 && Math.abs(column.z()) > 4) {
wetBeyondTheShore++;
}
}
assertEquals(0, wetBeyondTheShore);
}
@Test
public void aPondShrinksWhereTheGroundFallsAwayAroundItsRim() {
HydrologyTerrainSampler ridge = (int x, int z) -> HydrologyTerrainSample.openLand(z > 7 || z < -7 ? 70 : 80, 0D, "land");
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
new HydrologyPlannerSettings.Pond(true, 4, 12, 3),
new HydrologyPlannerSettings.Pond(false, 4, 7, 3));
Compiled compiled = compile(zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), ponds), ridge, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(0, compiled.field().uncontainedWetCells());
assertChannelContained(compiled);
assertNoWriteAboveNatural(compiled);
assertEquals(80, compiled.field().column(0, 0).headY());
SurfaceColumn nearRim = compiled.field().column(0, 4);
assertEquals(SurfaceRole.CHANNEL, nearRim.role());
for (SurfaceColumn column : compiled.field().columns().values()) {
if (column.role() == SurfaceRole.CHANNEL) {
assertTrue(Math.abs(column.z()) <= 7);
}
}
}
@Test
public void pondsAreDeterministicForACourseSeed() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface(0, HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.defaults());
Compiled first = compile(surface, flat, 300, SurfaceTerminal.SINKHOLE, 40);
Compiled second = compile(surface, flat, 300, SurfaceTerminal.SINKHOLE, 40);
assertEquals(first.field().columns().size(), second.field().columns().size());
for (SurfaceColumn column : first.field().columns().values()) {
SurfaceColumn same = second.field().column(column.x(), column.z());
assertNotNull(same);
assertEquals(column.height(), same.height());
assertEquals(column.role(), same.role());
}
assertEquals(0, first.field().uncontainedWetCells());
}
}
@@ -79,7 +79,7 @@ public class SurfaceCourseBuilderTest {
assertNull(result.rejection());
assertEquals(1, result.segments().size());
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().getFirst().type());
assertEquals(79, result.lastHead());
assertEquals(80, result.lastHead());
assertEquals(201, result.segments().getFirst().centerline().size());
}
@@ -135,7 +135,42 @@ public class SurfaceCourseBuilderTest {
assertTrue(result.segments().isEmpty());
}
@Test
public void aCourseWithAnInletEndsAtSeaLevelWithoutACoastalDropStation() {
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(241, 0, 0));
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
SurfaceCourseResult plain = builder(defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()), sampler)
.build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(result.rejection());
assertEquals(SEA_LEVEL, result.lastHead());
assertEquals(239, result.pathEnd().x());
HydraulicSegment last = result.segments().getLast();
assertEquals(HydrologyFeatureType.SURFACE_POOL, last.type());
assertEquals(SEA_LEVEL, last.upstreamHeadY());
assertEquals(SEA_LEVEL, last.downstreamHeadY());
assertEquals(inlet.length(), last.centerline().size());
assertEquals(240 - inlet.length(), last.start().x());
assertNull(plain.rejection());
assertEquals(SEA_LEVEL, plain.lastHead());
assertEquals(240, plain.pathEnd().x());
assertTrue(plain.segments().getLast().upstreamHeadY() > SEA_LEVEL);
}
private static SurfaceCourseBuilder builder(HydrologyTerrainSampler sampler) {
return new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL, 64);
}
private static SurfaceCourseBuilder builder(HydrologyPlannerSettings.Banks banks, HydrologyTerrainSampler sampler) {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(), banks);
return new SurfaceCourseBuilder(surface, sampler, CONSTANT_GEOMETRY, SEA_LEVEL, 64);
}
}
@@ -114,6 +114,58 @@ public class SurfaceFootprintCompilerTest {
assertNull(column(footprint, 111, 0));
}
@Test
public void inletColumnsPublishSeaLevelFluidOverALoweredBed() {
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 100
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
HydrologyPlannerSettings.Inlet inlet = HydrologyPlannerSettings.defaults().surface().banks().inlet();
ArrayList<HydrologyPoint> ramp = new ArrayList<>();
for (int x = 30; x <= 39; x++) {
ramp.add(new HydrologyPoint(x, 69 - (x - 30), 0));
}
RiverCourse course = course(List.of(
segment(1L, HydrologyFeatureType.SURFACE_POOL, 69, 69, points(0, 30, 69)),
segment(2L, HydrologyFeatureType.CASCADE, 69, SEA_LEVEL, List.copyOf(ramp)),
segment(3L, HydrologyFeatureType.SURFACE_POOL, SEA_LEVEL, SEA_LEVEL, points(39, 99, SEA_LEVEL)),
segment(4L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL, points(99, 101, SEA_LEVEL))
));
SurfaceFootprint footprint = compiler(sampler).compile(course);
assertEquals(0, footprint.uncontainedWetCells());
assertEquals(3, inlet.depth());
for (int x = 40; x < 100; x++) {
SurfaceLayerColumn center = column(footprint, x, 0);
assertNotNull("x=" + x, center);
HydrologyColumnLayer layer = center.layer();
assertTrue(layer.channel());
assertTrue(layer.fluidOwned());
assertEquals(SEA_LEVEL, layer.fluidHeadY());
assertTrue("x=" + x + " bed " + layer.bedY(), layer.bedY() <= SEA_LEVEL - inlet.depth());
assertEquals(sampler.sample(x, 0).mouthBiomeKey(), layer.mouthBiomeKey());
}
int shoreZ = -1;
for (int z = 1; z < 16; z++) {
SurfaceLayerColumn column = column(footprint, 90, z);
assertNotNull(column);
if (!column.layer().channel()) {
shoreZ = z;
break;
}
}
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());
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.terrain().ocean()) {
assertTrue(column.layer().oceanApron());
assertFalse(column.layer().terrainOwned());
}
}
}
private static SurfaceLayerColumn column(SurfaceFootprint footprint, int x, int z) {
for (SurfaceLayerColumn column : footprint.columns()) {
if (column.x() == x && column.z() == z) {
@@ -24,18 +24,29 @@ public class ValleyProfileSolverTest {
};
@Test
public void flatTerrainHoldsTheHeadOneInsetBelowTheBanks() {
public void flatTerrainHoldsTheHeadAtTheLowestBankByDefault() {
ValleyProfile valley = solve(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
assertNull(valley.rejection());
assertEquals(300, valley.exposedStations());
for (int station = 0; station < valley.exposedStations(); station++) {
assertEquals(79, valley.head()[station]);
assertEquals(80, valley.head()[station]);
assertEquals(80, valley.crossMin()[station]);
assertEquals(80, valley.centerNatural()[station]);
}
}
@Test
public void aConfiguredSinkHoldsTheHeadThatFarBelowTheLowestBank() {
HydrologyTerrainSampler flat = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
ValleyProfile valley = solver(flat, 2).solve(straight(300), channel(300, flat), SurfaceTerminal.SINKHOLE, 40);
assertNull(valley.rejection());
for (int station = 0; station < valley.exposedStations(); station++) {
assertEquals(78, valley.head()[station]);
}
}
@Test
public void gentleSlopeProducesOneBlockNonRisingSteps() {
ValleyProfile valley = solve(400, (x, z) -> 120 - x / 8, SurfaceTerminal.SINKHOLE, 40);
@@ -66,9 +77,9 @@ public class ValleyProfileSolverTest {
ValleyProfile valley = solve(300, (x, z) -> x == 150 && z == 6 ? 74 : 80, SurfaceTerminal.SINKHOLE, 40);
assertNull(valley.rejection());
assertEquals(79, valley.head()[100]);
assertTrue(valley.head()[150] <= 73);
assertTrue(valley.head()[299] <= 73);
assertEquals(80, valley.head()[100]);
assertTrue(valley.head()[150] <= 74);
assertTrue(valley.head()[299] <= 74);
assertContained(valley);
}
@@ -87,7 +98,8 @@ public class ValleyProfileSolverTest {
assertNull(valley.rejection());
assertTrue(valley.exposedStations() > 200);
assertTrue(valley.exposedStations() <= 240);
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations() - 1]);
assertTrue(valley.head()[valley.exposedStations() - 1] >= SEA_LEVEL);
assertTrue(valley.head()[valley.exposedStations() - 1] <= SEA_LEVEL + 1);
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations()]);
assertEquals(SEA_LEVEL, valley.head()[299]);
for (int station = 0; station < valley.exposedStations(); station++) {
@@ -103,7 +115,8 @@ public class ValleyProfileSolverTest {
assertNull(valley.rejection());
assertTrue(valley.exposedStations() <= 200);
assertTrue(valley.exposedStations() >= 190);
assertEquals(89, valley.head()[valley.exposedStations() - 1]);
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()]);
}
@@ -117,8 +130,8 @@ public class ValleyProfileSolverTest {
largest = Math.max(largest, valley.head()[station - 1] - valley.head()[station]);
}
assertEquals(10, largest);
assertEquals(99, valley.head()[100]);
assertEquals(89, valley.head()[200]);
assertEquals(100, valley.head()[100]);
assertEquals(90, valley.head()[200]);
}
@Test
@@ -142,9 +155,140 @@ public class ValleyProfileSolverTest {
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
}
@Test
public void anInletHoldsTheLastStationsAtSeaLevelAndMayCutDeeperThanTheChannelCap() {
HydrologyTerrainSampler coast = risingCoast();
HydrologyPlannerSettings.Inlet inlet = new HydrologyPlannerSettings.Inlet(32, 3, 32);
ValleyProfile valley = solver(coast, inlet).solve(straight(300), channel(300, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
ValleyProfile plain = solver(coast, HydrologyPlannerSettings.Inlet.none())
.solve(straight(300), channel(300, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
assertEquals(240, valley.exposedStations());
for (int station = 240 - inlet.length(); station < 240; station++) {
assertEquals(SEA_LEVEL, valley.head()[station]);
}
int deepestCut = 0;
for (int station = 0; station < valley.exposedStations(); station++) {
deepestCut = Math.max(deepestCut, valley.centerNatural()[station] - valley.head()[station]);
}
assertTrue("deepest " + deepestCut, deepestCut > 10);
assertContained(valley);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, plain.rejection());
assertTrue(plain.rejectionDetail() > 10);
}
@Test
public void theInletApproachGradesDownOneBlockPerStation() {
HydrologyTerrainSampler plateau = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(88, 0D, "land");
ValleyProfile valley = solver(plateau, new HydrologyPlannerSettings.Inlet(32, 3, 32))
.solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
ValleyProfile shallow = solver(plateau, new HydrologyPlannerSettings.Inlet(32, 3, 24))
.solve(straight(300), channel(300, plateau), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
int inletStart = 240 - 32;
assertEquals(88, valley.head()[inletStart - 17]);
for (int station = inletStart - 16; station < inletStart; station++) {
assertEquals(SEA_LEVEL + (inletStart - station), valley.head()[station]);
}
assertEquals(SEA_LEVEL, valley.head()[inletStart]);
assertContained(valley);
// A cap the plateau cannot be cut through keeps the plain crossing: no inlet, no ramp, the
// river still reaches the sea and drops into it at the coast.
assertNull(shallow.rejection());
assertEquals(88, valley.head()[0]);
for (int station = 0; station < shallow.exposedStations(); station++) {
assertEquals(88, shallow.head()[station]);
}
assertEquals(SEA_LEVEL, shallow.head()[shallow.exposedStations()]);
}
@Test
public void theInletStopsWhereTheCoastRisesBeyondItsCap() {
HydrologyTerrainSampler bluff = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 220 ? 66 + (x - 220) / 5 : 110, 0D, "land");
ValleyProfile valley = solver(bluff, new HydrologyPlannerSettings.Inlet(64, 3, 32))
.solve(straight(300), channel(300, bluff), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
assertEquals(240, valley.exposedStations());
for (int station = 220; station < 240; station++) {
assertEquals(SEA_LEVEL, valley.head()[station]);
}
assertEquals(110, valley.head()[219]);
assertEquals(110, valley.head()[100]);
assertContained(valley);
}
@Test
public void upstreamStationsKeepTheChannelCap() {
HydrologyTerrainSampler ridge = (int x, int z) -> x >= 240
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(x >= 30 && x < 50 ? 94 : 80, 0D, "land");
ValleyProfile valley = solver(ridge, new HydrologyPlannerSettings.Inlet(32, 3, 32))
.solve(straight(300), channel(300, ridge), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
assertTrue(valley.rejectionDetail() > 10);
}
@Test
public void theInletNeverTakesMoreThanHalfOfAShortCourse() {
HydrologyTerrainSampler coast = (int x, int z) -> x >= 80
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(70, 0D, "land");
ValleyProfile valley = solver(coast, new HydrologyPlannerSettings.Inlet(64, 3, 32))
.solve(straight(100), channel(100, coast), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
assertNull(valley.rejection());
assertEquals(80, valley.exposedStations());
for (int station = 40; station < 80; station++) {
assertEquals(SEA_LEVEL, valley.head()[station]);
}
assertEquals(SEA_LEVEL + 1, valley.head()[39]);
assertEquals(SEA_LEVEL + 10, valley.head()[30]);
for (int station = 0; station < 20; station++) {
assertEquals(70, valley.head()[station]);
}
assertContained(valley);
}
/** Land falling gently to 66 at x=200, then a coast rising to 83 over the last forty blocks before the sea at x=240. */
private static HydrologyTerrainSampler risingCoast() {
return (int x, int z) -> {
if (x >= 240) {
return HydrologyTerrainSample.ocean(50, "ocean");
}
int height = x < 200 ? 100 - x / 6 : 66 + (x - 200) * 18 / 40;
return HydrologyTerrainSample.openLand(height, 0D, "land");
};
}
private static ValleyProfileSolver solver(HydrologyTerrainSampler sampler, HydrologyPlannerSettings.Inlet inlet) {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Banks banks = defaults.banks();
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(),
new HydrologyPlannerSettings.Banks(banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
banks.mouthFlareRatio(), inlet, banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(),
banks.erosion(), banks.ponds()));
return new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64);
}
private static void assertContained(ValleyProfile valley) {
for (int station = 0; station < valley.exposedStations(); station++) {
assertTrue(valley.head()[station] <= valley.crossMin()[station] - 1);
assertTrue(valley.head()[station] <= valley.crossMin()[station]);
if (station > 0) {
assertTrue(valley.head()[station] <= valley.head()[station - 1]);
}
@@ -160,6 +304,19 @@ public class ValleyProfileSolverTest {
return new ValleyProfileSolver(HydrologyPlannerSettings.defaults().surface(), sampler, SEA_LEVEL, 64);
}
private static ValleyProfileSolver solver(HydrologyTerrainSampler sampler, int sink) {
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
HydrologyPlannerSettings.Banks banks = defaults.banks();
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(),
new HydrologyPlannerSettings.Banks(sink, banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(),
banks.roughness(), banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(),
banks.mouthFlareRatio(), HydrologyPlannerSettings.Inlet.none(), banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(),
banks.erosion(), banks.ponds()));
return new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64);
}
private static ChannelProfile channel(int stations, HydrologyTerrainSampler sampler) {
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY)
.build(straight(stations), "water", false);
@@ -1553,7 +1553,8 @@ public class MantleHydrologyComponentTest {
1,
2,
2,
false
false,
0
),
new HydrologyPlannerSettings.Outlets(
true,
@@ -1563,10 +1564,13 @@ public class MantleHydrologyComponentTest {
32,
16,
1,
8,
8
),
HydrologyPlannerSettings.Geometry.defaults(),
List.of(), List.of()
List.of(), List.of(),
0D,
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
@@ -1603,7 +1607,9 @@ public class MantleHydrologyComponentTest {
"shore",
"dry",
"flooded",
List.of("default"), List.of()
List.of("default"), List.of(),
Double.NaN,
null
);
}
@@ -50,11 +50,10 @@ public class IrisRiverConfigurationTest {
assertEquals(8D, surface.getChannel().getWidth().getMax(), 0D);
assertEquals(2D, surface.getChannel().getDepth().getMin(), 0D);
assertEquals(4D, surface.getChannel().getDepth().getMax(), 0D);
assertEquals(1, surface.getChannel().getInset());
assertEquals(0, surface.getChannel().getSink());
assertEquals(10, surface.getChannel().getMaximumIncision());
assertEquals(0.25D, surface.getChannel().getRoughness(), 0D);
assertEquals(16, surface.getChannel().getRoughnessWavelength());
assertEquals(1, surface.getBanks().getFreeboard());
assertEquals(1.5D, surface.getBanks().getShoreWidth(), 0D);
assertEquals(3D, surface.getBanks().getBlendSlope(), 0D);
assertEquals(4, surface.getBanks().getMinimumBlendWidth());
@@ -154,13 +153,12 @@ public class IrisRiverConfigurationTest {
"channel": {
"width": {"min": 5, "max": 24},
"depth": {"min": 2, "max": 6},
"inset": 2,
"sink": 2,
"maximumIncision": 14,
"roughness": 0.4,
"roughnessWavelength": 24
},
"banks": {
"freeboard": 2,
"shoreWidth": 2.5,
"blendSlope": 4,
"minimumBlendWidth": 6,
@@ -262,11 +260,10 @@ public class IrisRiverConfigurationTest {
assertEquals(0.4D, rivers.getRouting().getConfluenceAttraction(), 0D);
assertEquals(new KList<>(IrisRiverInlandOutlet.SINKHOLE_GROTTO), rivers.getRouting().getInlandOutlets());
assertEquals(24D, rivers.getSurface().getChannel().getWidth().getMax(), 0D);
assertEquals(2, rivers.getSurface().getChannel().getInset());
assertEquals(2, rivers.getSurface().getChannel().getSink());
assertEquals(14, rivers.getSurface().getChannel().getMaximumIncision());
assertEquals(0.4D, rivers.getSurface().getChannel().getRoughness(), 0D);
assertEquals(24, rivers.getSurface().getChannel().getRoughnessWavelength());
assertEquals(2, rivers.getSurface().getBanks().getFreeboard());
assertEquals(2.5D, rivers.getSurface().getBanks().getShoreWidth(), 0D);
assertEquals(4D, rivers.getSurface().getBanks().getBlendSlope(), 0D);
assertEquals(6, rivers.getSurface().getBanks().getMinimumBlendWidth());