Legacy hydrology tests on the v2 contract; water connects across steps

- Geometry pins became relational properties; tests for deleted mechanisms removed
- Every owned channel water column is published to the mantle so steps get fluid updates
- Upper beds at head steps reach the lower head so the water stays one body
- Ocean aprons are measured from the first ocean station and bounded laterally
- Dry cave headroom uses the flooded cave biome
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-01 21:54:34 -04:00
1 parent 3d96a8616a
commit 507e241b2c
12 files changed
+336 -692

No files matched your search

@@ -70,13 +70,13 @@ public record HydrologyColumnLayer(
return surfaceBiomeKey;
}
/** Every owned channel water column is published so steps between stations receive fluid updates. */
public boolean publishesSurfaceFluid() {
return feature.type().isSurface()
&& !oceanApron
&& channel
&& fluidOwned
&& (terrainOwned || fallingFluid)
&& (feature.type().isDrop() || fallingFluid || receivingPool)
&& fluidHeadY > bedY;
}
@@ -78,6 +78,7 @@ public final class ErosionFieldCompiler {
}
}
}
int oceanStart = oceanStart(centerline, valley, terminal);
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(nearest.size());
LongArrayList wetKeys = new LongArrayList();
for (long key : nearest.keySet()) {
@@ -95,7 +96,8 @@ public final class ErosionFieldCompiler {
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
if (terrain != null && wet && terminal == SurfaceTerminal.OCEAN_MOUTH
&& station >= valley.exposedStations() - 1
&& station - valley.exposedStations() < apronLimit) {
&& station - oceanStart < apronLimit
&& cellDistance <= apronLimit + 0.25D) {
columns.put(key, new SurfaceColumn(
cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, terrain.naturalHeight(), seaLevel, true));
}
@@ -133,9 +135,34 @@ public final class ErosionFieldCompiler {
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.BANK, height, height, false));
}
int uncontained = contain(columns, wetKeys, freeboard);
connectSteps(columns, wetKeys);
return new ErosionField(columns, uncontained);
}
/**
* Where the head steps down between neighbouring wet cells, the upper bed is lowered so its water
* column reaches the lower head; the water stays one connected body across every step.
*/
private static void connectSteps(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys) {
for (int index = 0; index < wetKeys.size(); index++) {
long key = wetKeys.getLong(index);
SurfaceColumn wet = columns.get(key);
int bed = wet.height();
for (int[] offset : CARDINALS) {
SurfaceColumn neighbour = columns.get(RiverFootprint.pack(wet.x() + offset[0], wet.z() + offset[1]));
if (neighbour == null || neighbour.role() != SurfaceRole.CHANNEL || neighbour.apron()) {
continue;
}
if (neighbour.headY() < wet.headY()) {
bed = Math.min(bed, neighbour.headY() - 1);
}
}
if (bed < wet.height()) {
columns.put(key, wet.withBed(bed));
}
}
}
/**
* Every dry cell touching water, diagonals included, keeps the lip; only cardinal neighbours can
* spill, so only they count as uncontained.
@@ -183,6 +210,21 @@ public final class ErosionFieldCompiler {
return uncontained;
}
/** First mouth station whose center is ocean; the apron is measured from there, not from the mouth segment start. */
private int oceanStart(SurfaceCenterline centerline, ValleyProfile valley, SurfaceTerminal terminal) {
int count = centerline.size();
if (terminal != SurfaceTerminal.OCEAN_MOUTH) {
return count;
}
for (int station = Math.max(0, valley.exposedStations() - 1); station < count; station++) {
HydrologyTerrainSample center = sampler.sample(centerline.x()[station], centerline.z()[station]);
if (!SurfaceCellAdmission.writable(center, seaLevel)) {
return station;
}
}
return count;
}
private boolean[] basins(ValleyProfile valley, ChannelProfile channel, int count) {
boolean[] basin = new boolean[count];
for (int station = 1; station < count; station++) {
@@ -19,7 +19,13 @@ public record SurfaceColumn(
Objects.requireNonNull(role, "role");
}
/** Dry columns sit at their own height; the head follows it. */
public SurfaceColumn withHeight(int replacementHeight) {
return new SurfaceColumn(x, z, terrain, station, role, replacementHeight, replacementHeight, apron);
}
/** Wet columns keep their head; only the bed moves. */
public SurfaceColumn withBed(int replacementBed) {
return new SurfaceColumn(x, z, terrain, station, role, replacementBed, headY, apron);
}
}
@@ -325,11 +325,8 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
throw new IllegalStateException("Accepted cave layer is absent from its containment plan at "
+ position + ".");
}
String biomeKey = action == HydrologyCaveAction.DRY_AIR
? layer.floodedCaveBiomeKey()
: layer.floodedCaveBiomeKey();
PlannedCell candidate = new PlannedCell(
new HydrologyCaveCell(action, layer.profileKey(), biomeKey),
new HydrologyCaveCell(action, layer.profileKey(), layer.floodedCaveBiomeKey()),
layer.feature().courseId()
);
mergeCaveCell(caveCells, position, candidate);