Validator for rivers schema v2 and dead planner code removal

- PackHydrologyValidator rewritten for surface.channel/banks/flow/mouths, routing weights and bankBiomes
- 76 unreferenced private members deleted from HydrologyPlanner and HydrologyFootprintCompiler
This commit is contained in:
Brian Neumann-Fopiano
2026-09-01 19:30:29 -04:00
parent b57a2e2fc8
commit 00f5a16c20
5 changed files with 510 additions and 2284 deletions
@@ -28,6 +28,8 @@ final class PackHydrologyValidator {
private static final long MAX_LATTICE_NODES = 65_536L;
private static final long MAX_ROUTE_SAMPLES = 262_144L;
private static final int MAX_REACHABLE_BIOMES = 65_536;
private static final int CORRIDOR_SAMPLE_SPACING = 4;
private static final List<Integer> SAMPLE_SPACINGS = List.of(8, 16, 32, 64);
private static final IrisCoastalRiverGrottoConfig DEFAULT_COASTAL_GROTTO =
new IrisCoastalRiverGrottoConfig();
private static final IrisInlandRiverGrottoConfig DEFAULT_INLAND_GROTTO =
@@ -180,7 +182,7 @@ final class PackHydrologyValidator {
}
SourceBudget surfaceBudget = surface == null
? SourceBudget.defaults()
: validateSurface(path + ".surface", surface, dimension, routingValues, errors, warnings);
: validateSurface(path + ".surface", surface, dimension, errors, warnings);
SourceBudget undergroundBudget = underground == null
? SourceBudget.defaults()
: validateUnderground(path + ".underground", underground, dimension, errors, warnings);
@@ -198,16 +200,12 @@ final class PackHydrologyValidator {
private static void validateGeometry(String path, JSONObject geometry, List<String> errors) {
JSONObject meanders = nestedObject(geometry, "meanders", path, errors);
JSONObject surface = nestedObject(geometry, "surface", path, errors);
JSONObject underground = nestedObject(geometry, "underground", path, errors);
JSONObject grottos = nestedObject(geometry, "grottos", path, errors);
JSONObject drops = nestedObject(geometry, "drops", path, errors);
if (meanders != null) {
validateMeanders(path + ".meanders", meanders, errors);
}
if (surface != null) {
validateChannelShape(path + ".surface", surface, errors);
}
if (underground != null) {
validateChannelShape(path + ".underground", underground, errors);
}
@@ -252,53 +250,47 @@ final class PackHydrologyValidator {
private static RoutingValues validateRouting(String path, JSONObject routing, List<String> errors) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "tileSize", 256, 8192, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "sampleSpacing", 8, 512, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "refinementSpacing", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "sampleSpacing", 8, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "maximumRouteLength", 256, 32768, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path,
routing,
"minimumSurfaceCourseLength",
0,
32768,
errors
);
PackJsonFieldChecks.validateOptionalIntegerRange(
path,
routing,
"minimumUndergroundCourseLength",
0,
32768,
errors
);
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "maximumOutletsPerTile", 1, 256, errors);
PackJsonFieldChecks.validateOptionalBoolean(path, routing, "oceanOutlets", errors);
JSONObject branching = nestedObject(routing, "branching", path, errors);
if (branching != null) {
PackJsonFieldChecks.validateOptionalIntegerRange(
path + ".branching",
branching,
"minimumSurfaceCourseLength",
0,
32768,
errors
);
PackJsonFieldChecks.validateOptionalIntegerRange(
path + ".branching",
branching,
"minimumUndergroundCourseLength",
0,
32768,
errors
);
}
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "valleyPreference", 0D, 8D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "uphillPenalty", 0D, 128D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "slopePenalty", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors);
int tileSize = integerValue(routing, "tileSize", 2048);
int sampleSpacing = integerValue(routing, "sampleSpacing", 64);
int refinementSpacing = integerValue(routing, "refinementSpacing", 8);
int maximumRouteLength = integerValue(routing, "maximumRouteLength", 16384);
int minimumSurfaceCourseLength = branching == null
? 384
: integerValue(branching, "minimumSurfaceCourseLength", 384);
int minimumUndergroundCourseLength = branching == null
? 192
: integerValue(branching, "minimumUndergroundCourseLength", 192);
int minimumSurfaceCourseLength = integerValue(routing, "minimumSurfaceCourseLength", 384);
int minimumUndergroundCourseLength = integerValue(routing, "minimumUndergroundCourseLength", 192);
if (!SAMPLE_SPACINGS.contains(sampleSpacing)) {
errors.add(path + ".sampleSpacing must be one of " + SAMPLE_SPACINGS + ".");
}
if (sampleSpacing > 0 && tileSize % sampleSpacing != 0) {
errors.add(path + ".tileSize must be divisible by sampleSpacing.");
}
if (refinementSpacing > 0 && sampleSpacing % refinementSpacing != 0) {
errors.add(path + ".sampleSpacing must be divisible by refinementSpacing.");
}
if (minimumSurfaceCourseLength > maximumRouteLength) {
errors.add(path + ".branching.minimumSurfaceCourseLength must not exceed maximumRouteLength.");
errors.add(path + ".minimumSurfaceCourseLength must not exceed maximumRouteLength.");
}
if (minimumUndergroundCourseLength > maximumRouteLength) {
errors.add(path + ".branching.minimumUndergroundCourseLength must not exceed maximumRouteLength.");
errors.add(path + ".minimumUndergroundCourseLength must not exceed maximumRouteLength.");
}
Set<String> inlandOutlets = validateEnumArray(
@@ -312,28 +304,21 @@ final class PackHydrologyValidator {
if (!oceanOutlets && inlandOutlets.isEmpty()) {
errors.add(path + " must enable oceanOutlets or select at least one inlandOutlets entry.");
}
return new RoutingValues(
tileSize,
sampleSpacing,
refinementSpacing,
maximumRouteLength,
inlandOutlets
);
return new RoutingValues(tileSize, sampleSpacing, maximumRouteLength, inlandOutlets);
}
private static SourceBudget validateSurface(
String path,
JSONObject surface,
JSONObject dimension,
RoutingValues routing,
List<String> errors,
List<String> warnings
) {
PackJsonFieldChecks.validateOptionalBoolean(path, surface, "enabled", errors);
JSONObject sources = nestedObject(surface, "sources", path, errors);
JSONObject channel = nestedObject(surface, "channel", path, errors);
JSONObject hydraulics = nestedObject(surface, "hydraulics", path, errors);
JSONObject ridgeTunnels = nestedObject(surface, "ridgeTunnels", path, errors);
JSONObject banks = nestedObject(surface, "banks", path, errors);
JSONObject flow = nestedObject(surface, "flow", path, errors);
JSONObject mouths = nestedObject(surface, "mouths", path, errors);
SourceBudget budget = sources == null
@@ -342,14 +327,14 @@ final class PackHydrologyValidator {
if (channel != null) {
validateSurfaceChannel(path + ".channel", channel, errors);
}
if (hydraulics != null) {
validateHydraulics(path + ".hydraulics", hydraulics, errors);
if (banks != null) {
validateSurfaceBanks(path + ".banks", banks, errors);
}
if (ridgeTunnels != null) {
validateRidgeTunnels(path + ".ridgeTunnels", ridgeTunnels, routing, errors);
if (flow != null) {
validateSurfaceFlow(path + ".flow", flow, errors);
}
if (mouths != null) {
validateMouths(path + ".mouths", mouths, routing, errors);
validateMouths(path + ".mouths", mouths, errors);
}
boolean surfaceEnabled = booleanValue(surface, "enabled", true);
@@ -368,87 +353,34 @@ final class PackHydrologyValidator {
}
private static void validateSurfaceChannel(String path, JSONObject channel, List<String> errors) {
validateRange(path, channel, "width", 1D, 64D, 4D, 12D, errors);
validateRange(path, channel, "depth", 1D, 32D, 1D, 4D, errors);
validateRange(path, channel, "surfaceInset", 1D, 64D, 3D, 7D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "maximumIncision", 0, 64, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "shoreWidth", 1D, 2D, errors);
NumericRange blend = validateRange(
path,
channel,
"terrainBlendWidth",
4D,
64D,
10D,
24D,
errors
);
if (blend.minimum() < doubleValue(channel, "shoreWidth", 2D)) {
errors.add(path + ".terrainBlendWidth must remain independent of and outside shoreWidth.");
validateRange(path, channel, "width", 1D, 128D, 4D, 8D, errors);
validateRange(path, channel, "depth", 1D, 64D, 2D, 4D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "inset", 0, 3, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "maximumIncision", 1, 32, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "roughness", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "roughnessWavelength", 4, 64, errors);
}
private static void validateSurfaceBanks(String path, JSONObject banks, List<String> errors) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "freeboard", 0, 4, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "shoreWidth", 0.5D, 6D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, banks, "blendSlope", 0.5D, 12D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "minimumBlendWidth", 1, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, banks, "maximumBlendWidth", 1, 64, errors);
PackJsonFieldChecks.validateOptionalBoolean(path, banks, "exposeCutStrata", errors);
if (integerValue(banks, "minimumBlendWidth", 4) > integerValue(banks, "maximumBlendWidth", 32)) {
errors.add(path + ".minimumBlendWidth must not exceed maximumBlendWidth.");
}
}
private static void validateHydraulics(String path, JSONObject hydraulics, List<String> errors) {
NumericRange targetPoolLength = validateRange(
path,
hydraulics,
"targetPoolLength",
8D,
4096D,
80D,
180D,
errors
);
PackJsonFieldChecks.validateOptionalIntegerRange(path, hydraulics, "riffleDrop", 0, 16, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, hydraulics, "maximumGradualDrop", 0, 64, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, hydraulics, "maximumGradualLength", 1, 1024, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, hydraulics, "waterfallMinimumDrop", 1, 128, errors);
int riffleDrop = integerValue(hydraulics, "riffleDrop", 1);
int maximumGradualDrop = integerValue(hydraulics, "maximumGradualDrop", 7);
int maximumGradualLength = integerValue(hydraulics, "maximumGradualLength", 24);
int waterfallMinimumDrop = integerValue(hydraulics, "waterfallMinimumDrop", 8);
if (riffleDrop > maximumGradualDrop) {
errors.add(path + ".riffleDrop must not exceed maximumGradualDrop.");
}
if (waterfallMinimumDrop != maximumGradualDrop + 1) {
errors.add(path + ".waterfallMinimumDrop must equal maximumGradualDrop plus one.");
}
if (maximumGradualLength < maximumGradualDrop) {
errors.add(path + ".maximumGradualLength must be at least maximumGradualDrop.");
}
if (targetPoolLength.minimum() < maximumGradualLength) {
errors.add(path + ".targetPoolLength.min must be at least maximumGradualLength.");
}
private static void validateSurfaceFlow(String path, JSONObject flow, List<String> errors) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "cascadeRun", 1, 8, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, flow, "waterfallMinimumDrop", 2, 32, errors);
}
private static void validateRidgeTunnels(
String path,
JSONObject ridgeTunnels,
RoutingValues routing,
List<String> errors
) {
PackJsonFieldChecks.validateOptionalBoolean(path, ridgeTunnels, "enabled", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, ridgeTunnels, "maximumLength", 1, 4096, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, ridgeTunnels, "headroom", 1, 128, errors);
int maximumLength = integerValue(ridgeTunnels, "maximumLength", 192);
if (maximumLength > routing.maximumRouteLength()) {
errors.add(path + ".maximumLength must not exceed routing.maximumRouteLength.");
}
}
private static void validateMouths(
String path,
JSONObject mouths,
RoutingValues routing,
List<String> errors
) {
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "levelingDistance", 0, 2048, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumOceanApron", 0, 64, errors);
int levelingDistance = integerValue(mouths, "levelingDistance", 64);
if (levelingDistance > routing.maximumRouteLength()) {
errors.add(path + ".levelingDistance must not exceed routing.maximumRouteLength.");
}
private static void validateMouths(String path, JSONObject mouths, List<String> errors) {
PackJsonFieldChecks.validateOptionalDoubleRange(path, mouths, "flareRatio", 1D, 4D, errors);
PackJsonFieldChecks.validateOptionalIntegerRange(path, mouths, "maximumOceanApron", 0, 32, errors);
}
private static SourceBudget validateUnderground(
@@ -477,6 +409,14 @@ final class PackHydrologyValidator {
NumericRange depth = validateRange(path, underground, "depth", 1D, 32D, 1D, 3D, errors);
NumericRange headroom = validateRange(path, underground, "headroom", 1D, 128D, 6D, 14D, errors);
PackJsonFieldChecks.validateOptionalBoolean(path, underground, "connectToExistingCaves", errors);
PackJsonFieldChecks.validateOptionalIntegerRange(
path,
underground,
"mouthLevelingDistance",
16,
512,
errors
);
NumericRange height = dimensionHeight(dimension);
if (fluidLevel.minimum() - depth.maximum() <= height.minimum()) {
@@ -727,7 +667,7 @@ final class PackHydrologyValidator {
SourceBudget underground,
List<String> errors
) {
if (routing.sampleSpacing() <= 0 || routing.refinementSpacing() <= 0) {
if (routing.sampleSpacing() <= 0) {
return;
}
long latticeAxis = routing.tileSize() / (long) routing.sampleSpacing() + 1L;
@@ -738,7 +678,7 @@ final class PackHydrologyValidator {
}
long sources = surface.maximumExpectedSources() + underground.maximumExpectedSources();
long samplesPerRoute = divideCeiling(routing.maximumRouteLength(), routing.refinementSpacing()) + 1L;
long samplesPerRoute = divideCeiling(routing.maximumRouteLength(), CORRIDOR_SAMPLE_SPACING) + 1L;
long routeSamples = saturatedMultiply(sources, samplesPerRoute);
if (routeSamples > MAX_ROUTE_SAMPLES) {
errors.add(path + " may refine " + routeSamples
@@ -810,6 +750,7 @@ final class PackHydrologyValidator {
validateNullableDouble(path, policy, "depthMultiplier", 0.0001D, 16D, errors);
validateNullableDouble(path, policy, "incisionMultiplier", 0D, 16D, errors);
validateNullableDouble(path, policy, "routingMultiplier", 0D, 64D, errors);
validateNullableDouble(path, policy, "bankMultiplier", 0D, 4D, errors);
return policy;
}
@@ -1403,12 +1344,11 @@ final class PackHydrologyValidator {
private record RoutingValues(
int tileSize,
int sampleSpacing,
int refinementSpacing,
int maximumRouteLength,
Set<String> inlandOutlets
) {
private static RoutingValues defaults() {
return new RoutingValues(2048, 64, 8, 16384, Set.of());
return new RoutingValues(2048, 64, 16384, Set.of());
}
}
@@ -13,7 +13,6 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -21,20 +20,14 @@ import java.util.Objects;
import java.util.Set;
final class HydrologyFootprintCompiler {
private static final byte CHANNEL_ROLE = 1;
private static final byte SHORE_ROLE = 2;
private static final byte GRADING_ROLE = 3;
private static final int FEATURE_ROLE_COUNT = 7;
private static final int COURSE_FOOTPRINT_CACHE_SIZE = 8;
private static final int VALIDATION_RASTER_CACHE_SIZE = 8;
private static final int MINIMUM_ENDPOINT_PROFILE_LENGTH = 48;
private static final int MAXIMUM_ENDPOINT_PROFILE_LENGTH = 64;
private static final double MINIMUM_ENDPOINT_RAMP_WIDTHS = 2D;
private static final int SURFACE_SHAPE_PROFILE_LENGTH = 32;
private static final int[][] HORIZONTAL_NEIGHBORS = {
{1, 0}, {-1, 0}, {0, 1}, {0, -1}
};
private static final double MOUTH_FLARE_RATIO = 1.5D;
private static final double SURFACE_THALWEG_RATIO = 0.65D;
private static final long ORGANIC_SHAPE_FIRST_PHASE_SALT = 0x4f5247414e31L;
private static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L;
@@ -721,663 +714,10 @@ final class HydrologyFootprintCompiler {
return new RiverFootprint(immutable);
}
private void containSurfaceBanks(Long2ObjectLinkedOpenHashMap<MutableColumn> columns) {
for (MutableColumn column : columns.values()) {
if (column.maximumSurfaceFluidHead() != Integer.MIN_VALUE) {
continue;
}
int minimumBankHeight = Integer.MIN_VALUE;
for (int[] offset : HORIZONTAL_NEIGHBORS) {
MutableColumn neighbor = columns.get(RiverFootprint.pack(
column.x + offset[0],
column.z + offset[1]
));
if (neighbor == null) {
continue;
}
int fluidHead = neighbor.maximumSurfaceFluidHead();
if (fluidHead != Integer.MIN_VALUE) {
minimumBankHeight = Math.max(minimumBankHeight, Math.addExact(fluidHead, 1));
}
}
if (minimumBankHeight != Integer.MIN_VALUE) {
column.raiseDrySurfaceBed(minimumBankHeight);
}
}
}
private void materializeSurfaceBankTerrain(Long2ObjectLinkedOpenHashMap<MutableColumn> columns) {
ArrayList<Long> wetColumns = new ArrayList<>();
for (Long2ObjectMap.Entry<MutableColumn> entry : columns.long2ObjectEntrySet()) {
if (entry.getValue().maximumSurfaceFluidHead() != Integer.MIN_VALUE) {
wetColumns.add(entry.getLongKey());
}
}
for (long packed : wetColumns) {
int x = RiverFootprint.unpackX(packed);
int z = RiverFootprint.unpackZ(packed);
for (int[] offset : HORIZONTAL_NEIGHBORS) {
int bankX = Math.addExact(x, offset[0]);
int bankZ = Math.addExact(z, offset[1]);
long bankKey = RiverFootprint.pack(bankX, bankZ);
if (columns.containsKey(bankKey)) {
continue;
}
classifyNatural(bankX, bankZ);
HydrologyTerrainSample terrain = sampleTerrain(bankX, bankZ);
if (terrain != null) {
columns.put(bankKey, new MutableColumn(bankX, bankZ, terrain, settings.seaLevel()));
}
}
}
}
private static HydrologyColumnLayer raisedDrySurfaceLayer(
HydrologyColumnLayer layer,
int naturalHeight,
int minimumBed
) {
if (minimumBed == Integer.MIN_VALUE
|| !layer.feature().type().isSurface()
|| !layer.terrainOwned()
|| layer.channel()) {
return layer;
}
int bed = Math.min(naturalHeight, Math.max(layer.bedY(), minimumBed));
if (bed == layer.bedY()) {
return layer;
}
return new HydrologyColumnLayer(
layer.feature(),
bed,
bed,
bed,
false,
layer.shore(),
layer.grading(),
false,
false,
false,
true,
false,
false,
layer.profileKey(),
layer.surfaceBiomeKey(),
layer.mouthBiomeKey(),
layer.shoreBiomeKey(),
layer.bankBiomeKey(),
layer.floodedCaveBiomeKey()
);
}
private List<SurfaceSweep> surfaceSweeps(RiverCourse course, boolean includeFalling) {
ArrayList<SurfaceSweep> sweeps = new ArrayList<>();
int runStart = -1;
for (int segmentIndex = 0; segmentIndex <= course.segments().size(); segmentIndex++) {
boolean sweptSurface = segmentIndex < course.segments().size()
&& course.segments().get(segmentIndex).type().isSurface()
&& !course.segments().get(segmentIndex).fallingFluid();
if (sweptSurface && runStart < 0) {
runStart = segmentIndex;
}
if (sweptSurface || runStart < 0) {
continue;
}
sweeps.add(surfaceSweep(course, runStart, segmentIndex));
runStart = -1;
}
if (includeFalling) {
for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) {
HydraulicSegment segment = course.segments().get(segmentIndex);
if (!segment.type().isSurface() || !segment.fallingFluid()) {
continue;
}
sweeps.addAll(fallingSurfaceSweeps(course, segmentIndex));
}
}
return List.copyOf(sweeps);
}
private List<SurfaceSweep> fallingSurfaceSweeps(RiverCourse course, int segmentIndex) {
HydraulicSegment segment = course.segments().get(segmentIndex);
List<HydrologyPoint> centerline = continuousCenterline(segment);
HydrologyPoint throat = centerline.getFirst();
int flowX = flowDelta(centerline, 0, true);
int flowZ = flowDelta(centerline, 0, false);
SurfaceSweepPoint throatPoint = new SurfaceSweepPoint(
segment,
throat,
shape(
course,
segment,
throat,
segment.downstreamHeadY() - 1,
segment.upstreamHeadY(),
true,
false
),
segmentIndex == 0,
false,
true
);
ArrayList<SurfaceSweep> sweeps = new ArrayList<>();
sweeps.add(new SurfaceSweep(
course,
List.of(throatPoint),
segmentIndex == 0,
false,
flowX,
flowZ
));
if (centerline.size() == 1) {
return List.copyOf(sweeps);
}
ArrayList<SurfaceSweepPoint> downstream = new ArrayList<>(centerline.size() - 1);
for (int pointIndex = 1; pointIndex < centerline.size(); pointIndex++) {
HydrologyPoint point = centerline.get(pointIndex);
boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1;
downstream.add(new SurfaceSweepPoint(
segment,
point,
shape(
course,
segment,
point,
point.y() - segment.depth(),
point.y(),
false,
receiving
),
false,
receiving,
false
));
}
boolean roundEnd = segmentIndex + 1 >= course.segments().size()
|| !course.segments().get(segmentIndex + 1).fallingFluid();
sweeps.add(new SurfaceSweep(course, List.copyOf(downstream), false, roundEnd, 0, 0));
return List.copyOf(sweeps);
}
private SurfaceSweep surfaceSweep(RiverCourse course, int startSegmentIndex, int endSegmentIndex) {
ArrayList<SurfaceSweepPoint> points = new ArrayList<>();
for (int segmentIndex = startSegmentIndex; segmentIndex < endSegmentIndex; segmentIndex++) {
HydraulicSegment segment = course.segments().get(segmentIndex);
List<HydrologyPoint> centerline = continuousCenterline(segment);
for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) {
HydrologyPoint point = centerline.get(pointIndex);
boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1;
LayerShape pointShape = shape(
course,
segment,
point,
point.y() - segment.depth(),
point.y(),
false,
receiving
);
SurfaceSweepPoint sweepPoint = new SurfaceSweepPoint(
segment,
point,
pointShape,
false,
receiving,
false
);
if (!points.isEmpty()
&& points.getLast().point().x() == point.x()
&& points.getLast().point().z() == point.z()) {
SurfaceSweepPoint existing = points.getLast();
points.set(points.size() - 1, existing.receiving() ? existing : sweepPoint);
} else {
points.add(sweepPoint);
}
}
}
if (points.isEmpty()) {
throw new IllegalStateException("A surface sweep requires at least one centerline point.");
}
HydraulicSegment preceding = startSegmentIndex > 0
? course.segments().get(startSegmentIndex - 1)
: null;
HydraulicSegment following = endSegmentIndex < course.segments().size()
? course.segments().get(endSegmentIndex)
: null;
SurfaceTerminal terminal = surfaceTerminal(points.getLast().segment());
boolean source = startSegmentIndex == 0;
boolean roundStart = source;
boolean roundEnd = terminal != SurfaceTerminal.NONE;
return new SurfaceSweep(
course,
profileSurfaceSweep(points, source, preceding, terminal, following),
roundStart,
roundEnd,
0,
0
);
}
private SurfaceTerminal surfaceTerminal(HydraulicSegment finalSurface) {
if (finalSurface.type() == HydrologyFeatureType.MOUTH) {
return SurfaceTerminal.OCEAN_MOUTH;
}
return SurfaceTerminal.NONE;
}
private List<SurfaceSweepPoint> profileSurfaceSweep(
List<SurfaceSweepPoint> rawPoints,
boolean source,
HydraulicSegment preceding,
SurfaceTerminal terminal,
HydraulicSegment following
) {
double[] distances = new double[rawPoints.size()];
for (int pointIndex = 1; pointIndex < rawPoints.size(); pointIndex++) {
HydrologyPoint previous = rawPoints.get(pointIndex - 1).point();
HydrologyPoint current = rawPoints.get(pointIndex).point();
distances[pointIndex] = distances[pointIndex - 1]
+ StrictMath.hypot(current.x() - previous.x(), current.z() - previous.z());
}
double totalLength = distances[distances.length - 1];
boolean profileSource = source && totalLength > 0D;
boolean profileBoreEntry = following != null
&& following.type().isUnderground()
&& segmentsJoin(rawPoints.getLast().segment(), following)
&& totalLength > 0D;
boolean profileBoreExit = preceding != null
&& preceding.type().isUnderground()
&& preceding.type() != HydrologyFeatureType.SINKHOLE
&& segmentsJoin(preceding, rawPoints.getFirst().segment())
&& totalLength > 0D;
boolean profileTerminal = terminal != SurfaceTerminal.NONE
&& totalLength >= MINIMUM_ENDPOINT_PROFILE_LENGTH;
int portalLength = Math.min(
SURFACE_SHAPE_PROFILE_LENGTH,
Math.max(1, (int) StrictMath.floor(totalLength))
);
List<LayerShape> transitionShapes = smoothSurfaceSweepShapes(rawPoints, distances);
ArrayList<SurfaceSweepPoint> profiled = new ArrayList<>(rawPoints.size());
for (int pointIndex = 0; pointIndex < rawPoints.size(); pointIndex++) {
SurfaceSweepPoint point = rawPoints.get(pointIndex);
LayerShape pointShape = transitionShapes.get(pointIndex);
if (profileSource) {
int sourceLength = Math.min(
endpointProfileLength(pointShape.channelRadius()),
Math.max(1, (int) StrictMath.floor(totalLength))
);
if (profileTerminal) {
sourceLength = Math.min(
sourceLength,
Math.max(1, (int) StrictMath.floor(totalLength / 2D))
);
}
double sourceWeight = 1D - smoothStep(Math.min(1D, distances[pointIndex] / sourceLength));
pointShape = sourceTaper(pointShape, sourceWeight);
}
if (profileBoreExit) {
double exitWeight = 1D - smoothStep(Math.min(1D, distances[pointIndex] / portalLength));
pointShape = borePortalShape(pointShape, exitWeight);
}
if (profileBoreEntry) {
double remaining = totalLength - distances[pointIndex];
double entryWeight = 1D - smoothStep(Math.min(1D, remaining / portalLength));
pointShape = borePortalShape(pointShape, entryWeight);
}
boolean receiving = point.receiving();
if (profileTerminal) {
int terminalLength = endpointProfileLength(pointShape.channelRadius());
if (profileSource) {
terminalLength = Math.min(
terminalLength,
Math.max(1, (int) StrictMath.floor(totalLength / 2D))
);
}
double remaining = totalLength - distances[pointIndex];
double terminalWeight = 1D - smoothStep(Math.min(1D, remaining / terminalLength));
pointShape = terminalShape(pointShape, terminal, terminalWeight);
}
profiled.add(new SurfaceSweepPoint(
point.segment(),
point.point(),
pointShape,
point.source(),
receiving,
point.falling()
));
}
return List.copyOf(profiled);
}
private LayerShape borePortalShape(LayerShape shape, double weight) {
return resizedSurfaceShape(
shape,
shape.channelRadius(),
shape.shoreWidth() * (1D - weight),
shape.gradingWidth() * (1D - weight),
shape.fluidHead() - shape.bed()
);
}
private List<LayerShape> smoothSurfaceSweepShapes(
List<SurfaceSweepPoint> points,
double[] distances
) {
ArrayList<LayerShape> smoothed = new ArrayList<>(points.size());
for (int pointIndex = 0; pointIndex < points.size(); pointIndex++) {
LayerShape current = points.get(pointIndex).shape();
double channelRadius = current.channelRadius();
double shoreWidth = current.shoreWidth();
double gradingWidth = current.gradingWidth();
double depth = current.fluidHead() - current.bed();
int firstCandidate = pointIndex;
while (firstCandidate > 0
&& distances[pointIndex] - distances[firstCandidate - 1] <= SURFACE_SHAPE_PROFILE_LENGTH) {
firstCandidate--;
}
int lastCandidate = pointIndex;
while (lastCandidate + 1 < points.size()
&& distances[lastCandidate + 1] - distances[pointIndex] <= SURFACE_SHAPE_PROFILE_LENGTH) {
lastCandidate++;
}
for (int candidateIndex = firstCandidate; candidateIndex <= lastCandidate; candidateIndex++) {
double distance = StrictMath.abs(distances[candidateIndex] - distances[pointIndex]);
LayerShape candidate = points.get(candidateIndex).shape();
double influence = smoothStep(1D - distance / SURFACE_SHAPE_PROFILE_LENGTH);
channelRadius = Math.max(
channelRadius,
channelRadius + (candidate.channelRadius() - channelRadius) * influence
);
shoreWidth = Math.max(
shoreWidth,
shoreWidth + (candidate.shoreWidth() - shoreWidth) * influence
);
gradingWidth = Math.max(
gradingWidth,
gradingWidth + (candidate.gradingWidth() - gradingWidth) * influence
);
int candidateDepth = candidate.fluidHead() - candidate.bed();
depth = Math.max(depth, depth + (candidateDepth - depth) * influence);
}
smoothed.add(resizedSurfaceShape(
current,
(int) StrictMath.round(channelRadius),
shoreWidth,
gradingWidth,
(int) StrictMath.round(depth)
));
}
return List.copyOf(smoothed);
}
private int endpointProfileLength(int channelRadius) {
return Math.max(
MINIMUM_ENDPOINT_PROFILE_LENGTH,
Math.min(MAXIMUM_ENDPOINT_PROFILE_LENGTH, Math.multiplyExact(channelRadius, 6))
);
}
private LayerShape sourceTaper(LayerShape shape, double weight) {
return resizedSurfaceShape(
shape,
blend(shape.channelRadius(), 1, weight),
shape.shoreWidth(),
shape.gradingWidth(),
shape.fluidHead() - shape.bed()
);
}
private LayerShape terminalShape(
LayerShape shape,
SurfaceTerminal terminal,
double weight
) {
return switch (terminal) {
case NONE -> shape;
case OCEAN_MOUTH -> {
int targetRadius = mouthFlareRadius(shape);
yield resizedSurfaceShape(
shape,
blend(shape.channelRadius(), targetRadius, weight),
shape.shoreWidth() + weight,
shape.gradingWidth() + weight * Math.max(2D, shape.channelRadius() * 0.5D),
blend(shape.fluidHead() - shape.bed(), 1, weight)
);
}
};
}
private int mouthFlareRadius(LayerShape shape) {
return Math.max(
Math.addExact(shape.channelRadius(), 2),
(int) StrictMath.ceil(shape.channelRadius() * MOUTH_FLARE_RATIO)
);
}
private LayerShape resizedSurfaceShape(
LayerShape shape,
int channelRadius,
double shoreWidth,
double gradingWidth,
int depth
) {
return new LayerShape(
channelRadius,
shoreWidth,
gradingWidth,
shape.fluidHead() - depth,
shape.fluidHead(),
shape.ceiling(),
shape.ellipsoid(),
shape.archedChannel(),
shape.roundedSurfaceBed(),
shape.organicBoundary(),
shape.fallingThroat()
);
}
private int blend(int start, int end, double progress) {
return (int) StrictMath.round(start + (end - start) * progress);
}
private double smoothStep(double progress) {
return progress * progress * (3D - 2D * progress);
}
private void rasterizeSurfaceSweep(
Long2ObjectLinkedOpenHashMap<MutableColumn> columns,
SurfaceSweep sweep,
boolean validationOnly
) {
Long2ObjectOpenHashMap<SurfaceProjection> projections = new Long2ObjectOpenHashMap<>();
if (sweep.points().size() == 1) {
collectSurfaceEdge(projections, sweep, -1);
} else {
for (int edgeIndex = 0; edgeIndex < sweep.points().size() - 1; edgeIndex++) {
collectSurfaceEdge(projections, sweep, edgeIndex);
}
}
long[] positions = projections.keySet().toLongArray();
Arrays.sort(positions);
for (long position : positions) {
int x = RiverFootprint.unpackX(position);
int z = RiverFootprint.unpackZ(position);
rasterizeSurfaceProjection(columns, projections.get(position), x, z, validationOnly);
}
}
private void collectSurfaceEdge(
Long2ObjectOpenHashMap<SurfaceProjection> projections,
SurfaceSweep sweep,
int edgeIndex
) {
SurfaceSweepPoint start = sweep.points().get(Math.max(0, edgeIndex));
SurfaceSweepPoint end = edgeIndex < 0 ? start : sweep.points().get(edgeIndex + 1);
int radius = (int) StrictMath.ceil(Math.max(start.shape().totalRadius(), end.shape().totalRadius()));
int minimumX = Math.min(start.point().x(), end.point().x()) - radius;
int maximumX = Math.max(start.point().x(), end.point().x()) + radius;
int minimumZ = Math.min(start.point().z(), end.point().z()) - radius;
int maximumZ = Math.max(start.point().z(), end.point().z()) + radius;
for (int z = minimumZ; z <= maximumZ; z++) {
for (int x = minimumX; x <= maximumX; x++) {
SurfaceProjection candidate = surfaceProjection(sweep, edgeIndex, x, z);
if (candidate == null) {
continue;
}
long packed = RiverFootprint.pack(x, z);
SurfaceProjection selected = projections.get(packed);
if (selected == null || prefersSurfaceProjection(candidate, selected)) {
projections.put(packed, candidate);
}
}
}
}
private SurfaceProjection surfaceProjection(SurfaceSweep sweep, int edgeIndex, int x, int z) {
if (edgeIndex < 0) {
SurfaceSweepPoint point = sweep.points().getFirst();
double distance = StrictMath.hypot(x - point.point().x(), z - point.point().z());
if (distance > point.shape().totalRadius() + 0.25D) {
return null;
}
return new SurfaceProjection(
sweep,
edgeIndex,
point.segment(),
point.point(),
point.shape(),
point.source() || sweep.roundStart(),
point.receiving(),
point.falling(),
point.segment().type() == HydrologyFeatureType.MOUTH,
point.point().x(),
point.point().z(),
distance,
sweep.singletonFlowX(),
sweep.singletonFlowZ(),
false
);
}
SurfaceSweepPoint start = sweep.points().get(edgeIndex);
SurfaceSweepPoint end = sweep.points().get(edgeIndex + 1);
double deltaX = end.point().x() - start.point().x();
double deltaZ = end.point().z() - start.point().z();
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
if (lengthSquared == 0D) {
return null;
}
double rawProgress = ((x - start.point().x()) * deltaX + (z - start.point().z()) * deltaZ)
/ lengthSquared;
boolean extendStart = edgeIndex == 0 && sweep.roundStart();
boolean extendEnd = edgeIndex == sweep.points().size() - 2 && sweep.roundEnd();
if (rawProgress < 0D && !extendStart || rawProgress > 1D && !extendEnd) {
return null;
}
double progress = Math.max(0D, Math.min(1D, rawProgress));
double projectedX = start.point().x() + deltaX * progress;
double projectedZ = start.point().z() + deltaZ * progress;
double distance = StrictMath.hypot(x - projectedX, z - projectedZ);
LayerShape projectedShape = interpolateSurfaceShape(start.shape(), end.shape(), progress);
boolean secondHalf = progress >= 0.5D;
SurfaceSweepPoint selected = secondHalf ? end : start;
if (distance > projectedShape.totalRadius() + 0.25D) {
return null;
}
int tangentIndex = secondHalf ? edgeIndex + 1 : edgeIndex;
int[] tangent = surfaceTangent(sweep.points(), tangentIndex);
return new SurfaceProjection(
sweep,
edgeIndex,
selected.segment(),
selected.point(),
projectedShape,
sweep.roundStart() && edgeIndex == 0 && progress < 0.5D,
selected.receiving(),
selected.falling(),
selected.segment().type() == HydrologyFeatureType.MOUTH
&& edgeIndex == sweep.points().size() - 2
&& progress >= 0.5D,
projectedX,
projectedZ,
distance,
tangent[0],
tangent[1],
rawProgress < 0D || rawProgress > 1D
);
}
private LayerShape interpolateSurfaceShape(LayerShape start, LayerShape end, double progress) {
LayerShape selected = progress < 0.5D ? start : end;
return new LayerShape(
blend(start.channelRadius(), end.channelRadius(), progress),
start.shoreWidth() + (end.shoreWidth() - start.shoreWidth()) * progress,
start.gradingWidth() + (end.gradingWidth() - start.gradingWidth()) * progress,
blend(start.bed(), end.bed(), progress),
blend(start.fluidHead(), end.fluidHead(), progress),
blend(start.ceiling(), end.ceiling(), progress),
selected.ellipsoid(),
selected.archedChannel(),
selected.roundedSurfaceBed(),
selected.organicBoundary(),
selected.fallingThroat()
);
}
private int[] surfaceTangent(List<SurfaceSweepPoint> points, int pointIndex) {
int reach = Math.max(2, settings.routing().refinementSpacing() * 2);
HydrologyPoint start = points.get(Math.max(0, pointIndex - reach)).point();
HydrologyPoint end = points.get(Math.min(points.size() - 1, pointIndex + reach)).point();
int flowX = end.x() - start.x();
int flowZ = end.z() - start.z();
return flowX == 0 && flowZ == 0 ? new int[]{1, 0} : new int[]{flowX, flowZ};
}
private boolean prefersSurfaceProjection(SurfaceProjection candidate, SurfaceProjection selected) {
int distanceComparison = Double.compare(candidate.distance(), selected.distance());
if (distanceComparison != 0) {
return distanceComparison < 0;
}
if (candidate.receiving() != selected.receiving()) {
return candidate.receiving();
}
return candidate.edgeIndex() < selected.edgeIndex();
}
private void rasterizeSurfaceProjection(
Long2ObjectLinkedOpenHashMap<MutableColumn> columns,
SurfaceProjection projection,
int x,
int z,
boolean validationOnly
) {
SurfaceCell cell = surfaceCell(projection, x, z);
if (cell == null) {
return;
}
if (cell.layer() != null) {
if (!validationOnly) {
addLayer(columns, x, z, cell.terrain(), cell.layer());
}
return;
}
HydrologyFeatureRef[] pointFeatures = new HydrologyFeatureRef[FEATURE_ROLE_COUNT];
double distance = surfaceDistance(projection, x, z);
if (cell.naturalOcean() && oceanApronEligible(projection.segment(), distance)) {
addOceanApron(
columns,
projection.sweep().course(),
projection.segment(),
projection.anchor(),
x,
z,
distance,
projection.flowX(),
projection.flowZ(),
cell.terrain(),
pointFeatures
);
}
}
private void rasterizeSegment(
Long2ObjectLinkedOpenHashMap<MutableColumn> columns,
RiverCourse course,
@@ -2254,117 +1594,6 @@ final class HydrologyFootprintCompiler {
);
}
private SurfaceCell surfaceCell(SurfaceProjection projection, int x, int z) {
LayerShape shape = projection.shape();
double channelDistance = surfaceDistance(projection, x, z);
boolean channel = channelDistance <= shape.channelRadius() + 0.25D;
boolean shore = !channel
&& channelDistance <= shape.channelRadius() + shape.shoreWidth() + 0.25D;
boolean grading = !channel && !shore && projection.distance() <= shape.totalRadius() + 0.25D;
if (!channel && !shore && !grading) {
return null;
}
double distance = grading ? projection.distance() : channelDistance;
HydraulicSegment segment = projection.segment();
HydrologyRoutingTerrainSampler.NaturalClassification classification = classifyNatural(x, z);
boolean apronEligible = oceanApronEligible(segment, distance);
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN && !apronEligible) {
return null;
}
boolean surfaceCourse = projection.sweep().course().type() == RiverCourseType.SURFACE;
if (!surfaceCourse
&& classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND
&& projection.oceanConnection()) {
return null;
}
boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
&& (!projection.oceanConnection() || surfaceCourse)
&& !channel
&& segment.type() != HydrologyFeatureType.WATERFALL
&& segment.type() != HydrologyFeatureType.CASCADE;
HydrologyTerrainSample terrain = exactSlope ? sampleTerrain(x, z) : sampleTerrainBasis(x, z);
if (terrain == null) {
return null;
}
boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|| terrain.ocean()
|| naturallySubmergedSurfaceColumn(segment, terrain);
if (naturalOcean) {
return apronEligible ? new SurfaceCell(terrain, null, true) : null;
}
if (channel && !projection.falling() && !surfaceChannelTerrainSupported(shape, terrain, x, z)) {
return null;
}
if (!surfaceCourse && projection.oceanConnection()) {
return null;
}
int deltaX = x - projection.anchor().x();
int deltaZ = z - projection.anchor().z();
HydrologyFeatureRef[] pointFeatures = new HydrologyFeatureRef[FEATURE_ROLE_COUNT];
HydrologyColumnLayer layer = regularLayer(
projection.sweep().course(),
segment,
projection.anchor(),
shape,
terrain,
distance,
deltaX,
deltaZ,
projection.flowX(),
projection.flowZ(),
projection.source(),
channel,
shore,
grading,
projection.falling(),
projection.receiving(),
pointFeatures
);
return new SurfaceCell(terrain, layer, false);
}
private boolean surfaceChannelTerrainSupported(
LayerShape shape,
HydrologyTerrainSample terrain,
int x,
int z
) {
if (terrain.naturalHeight() <= shape.fluidHead()) {
return false;
}
for (int[] offset : HORIZONTAL_NEIGHBORS) {
HydrologyTerrainSample neighbor = sampleTerrainBasis(x + offset[0], z + offset[1]);
if (neighbor == null) {
return false;
}
if (shape.fluidHead() <= settings.seaLevel()
&& (neighbor.ocean() || neighbor.naturalHeight() <= settings.seaLevel())) {
continue;
}
if (neighbor.naturalHeight() <= shape.fluidHead()) {
return false;
}
}
return true;
}
private double surfaceDistance(SurfaceProjection projection, int x, int z) {
if (projection.roundedCap()) {
return projection.distance();
}
return shapedDistance(
projection.shape(),
projection.segment(),
x,
z,
x - projection.x(),
z - projection.z(),
projection.flowX(),
projection.flowZ(),
projection.distance()
);
}
private RasterStencil rasterStencil(LayerShape shape) {
RasterStencilKey key = new RasterStencilKey(
shape.channelRadius(),
@@ -2396,8 +1625,7 @@ final class HydrologyFootprintCompiler {
if (!channel && !shore && !grading) {
continue;
}
byte role = channel ? CHANNEL_ROLE : shore ? SHORE_ROLE : GRADING_ROLE;
builder.add(deltaX, deltaZ, distance, role);
builder.add(deltaX, deltaZ, distance);
}
}
return builder.build();
@@ -2958,56 +2186,6 @@ final class HydrologyFootprintCompiler {
}
}
private record SurfaceSweepPoint(
HydraulicSegment segment,
HydrologyPoint point,
LayerShape shape,
boolean source,
boolean receiving,
boolean falling
) {
}
private record SurfaceSweep(
RiverCourse course,
List<SurfaceSweepPoint> points,
boolean roundStart,
boolean roundEnd,
int singletonFlowX,
int singletonFlowZ
) {
}
private record SurfaceSweepEdge(
SurfaceSweep sweep,
int edgeIndex
) {
}
private record SurfaceProjection(
SurfaceSweep sweep,
int edgeIndex,
HydraulicSegment segment,
HydrologyPoint anchor,
LayerShape shape,
boolean source,
boolean receiving,
boolean falling,
boolean oceanConnection,
double x,
double z,
double distance,
int flowX,
int flowZ,
boolean roundedCap
) {
}
private enum SurfaceTerminal {
NONE,
OCEAN_MOUTH
}
private record CenterlineProjection(
int pointIndex,
double x,
@@ -3018,13 +2196,6 @@ final class HydrologyFootprintCompiler {
) {
}
private record SurfaceCell(
HydrologyTerrainSample terrain,
HydrologyColumnLayer layer,
boolean naturalOcean
) {
}
private record ValidationCourseRaster(
long courseId,
List<HydrologyColumnSample> columns,
@@ -3060,8 +2231,7 @@ final class HydrologyFootprintCompiler {
private record RasterStencil(
int[] deltaXs,
int[] deltaZs,
double[] distances,
byte[] roles
double[] distances
) {
private int size() {
return deltaXs.length;
@@ -3072,28 +2242,24 @@ final class HydrologyFootprintCompiler {
private int[] deltaXs;
private int[] deltaZs;
private double[] distances;
private byte[] roles;
private int size;
private RasterStencilBuilder() {
this.deltaXs = new int[64];
this.deltaZs = new int[64];
this.distances = new double[64];
this.roles = new byte[64];
}
private void add(int deltaX, int deltaZ, double distance, byte role) {
private void add(int deltaX, int deltaZ, double distance) {
if (size == deltaXs.length) {
int expandedSize = Math.multiplyExact(size, 2);
deltaXs = Arrays.copyOf(deltaXs, expandedSize);
deltaZs = Arrays.copyOf(deltaZs, expandedSize);
distances = Arrays.copyOf(distances, expandedSize);
roles = Arrays.copyOf(roles, expandedSize);
}
deltaXs[size] = deltaX;
deltaZs[size] = deltaZ;
distances[size] = distance;
roles[size] = role;
size++;
}
@@ -3101,8 +2267,7 @@ final class HydrologyFootprintCompiler {
return new RasterStencil(
Arrays.copyOf(deltaXs, size),
Arrays.copyOf(deltaZs, size),
Arrays.copyOf(distances, size),
Arrays.copyOf(roles, size)
Arrays.copyOf(distances, size)
);
}
}
@@ -3379,44 +2544,6 @@ final class HydrologyFootprintCompiler {
);
}
private int maximumSurfaceFluidHead() {
int maximum = Integer.MIN_VALUE;
if (singleLayer != null) {
return surfaceFluidHead(singleLayer);
}
if (layers == null) {
return maximum;
}
for (HydrologyColumnLayer layer : layers.values()) {
maximum = Math.max(maximum, surfaceFluidHead(layer));
}
return maximum;
}
private int surfaceFluidHead(HydrologyColumnLayer layer) {
return layer.feature().type().isSurface()
&& layer.channel()
&& layer.connectedFluid()
&& layer.fluidOwned()
&& !layer.oceanApron()
&& !layer.fallingFluid()
? layer.fluidHeadY()
: Integer.MIN_VALUE;
}
private void raiseDrySurfaceBed(int minimumBed) {
if (singleLayer != null) {
singleLayer = raisedDrySurfaceLayer(singleLayer, naturalHeight, minimumBed);
return;
}
if (layers == null) {
return;
}
for (Map.Entry<Long, HydrologyColumnLayer> entry : layers.entrySet()) {
entry.setValue(raisedDrySurfaceLayer(entry.getValue(), naturalHeight, minimumBed));
}
}
private List<HydrologyColumnLayer> resolveSurfaceHeadConflicts(List<HydrologyColumnLayer> candidates) {
HashMap<Long, HydrologyColumnLayer> selectedByCourse = new HashMap<>();
for (HydrologyColumnLayer candidate : candidates) {
File diff suppressed because it is too large Load Diff
@@ -21,7 +21,7 @@ public class IrisRiverHydrology {
@Desc("Organic centerline, bed, wall, and descending-water geometry.")
private IrisRiverGeometryConfig geometry = new IrisRiverGeometryConfig();
@Desc("Surface river source, channel, hydraulic, ridge-tunnel, and mouth configuration.")
@Desc("Surface river source, channel, bank, flow, and mouth configuration.")
private IrisSurfaceRiverConfig surface = new IrisSurfaceRiverConfig();
@Desc("Independent underground river source and channel configuration.")
@@ -50,14 +50,15 @@ public class PackHydrologyValidatorTest {
"derivative": "minecraft:river",
"riverPolicy": {
"profiles": ["default"],
"bankBiomes": ["river_dry"]
"bankBiomes": ["river_bank"],
"bankMultiplier": 1.25
}
}
""");
write(pack, "biomes/river_shore.json",
"{\"name\":\"Shore\",\"derivative\":\"minecraft:river\"}");
write(pack, "biomes/river_dry.json",
"{\"name\":\"Dry\",\"derivative\":\"minecraft:plains\"}");
write(pack, "biomes/river_bank.json",
"{\"name\":\"Bank\",\"derivative\":\"minecraft:plains\"}");
PackHydrologyValidator.Validation result = validate(pack);
@@ -66,7 +67,321 @@ public class PackHydrologyValidatorTest {
}
@Test
public void rejectsRoutingGeometryHydraulicComplexityAndCapabilityFailures() throws Exception {
public void acceptsEverySchemaBoundAtItsMinimum() throws Exception {
File pack = pack(riversDimension("""
{
"enabled": true,
"routing": {
"tileSize": 256,
"sampleSpacing": 8,
"maximumRouteLength": 256,
"minimumSurfaceCourseLength": 0,
"minimumUndergroundCourseLength": 0,
"maximumOutletsPerTile": 1,
"oceanOutlets": true,
"valleyPreference": 0,
"uphillPenalty": 0,
"slopePenalty": 0,
"confluenceAttraction": 0
},
"surface": {
"channel": {
"width": {"min": 1, "max": 1},
"depth": {"min": 1, "max": 1},
"inset": 0,
"maximumIncision": 1,
"roughness": 0,
"roughnessWavelength": 4
},
"banks": {
"freeboard": 0,
"shoreWidth": 0.5,
"blendSlope": 0.5,
"minimumBlendWidth": 1,
"maximumBlendWidth": 1,
"exposeCutStrata": false
},
"flow": {"cascadeRun": 1, "waterfallMinimumDrop": 2},
"mouths": {"flareRatio": 1, "maximumOceanApron": 0}
},
"underground": {"mouthLevelingDistance": 16}
}
""", "\"bankMultiplier\": 0"));
PackHydrologyValidator.Validation result = validate(pack);
assertTrue(result.errors().toString(), result.errors().isEmpty());
}
@Test
public void acceptsEverySchemaBoundAtItsMaximum() throws Exception {
File pack = pack(riversDimension("""
{
"enabled": true,
"routing": {
"tileSize": 8192,
"sampleSpacing": 64,
"maximumRouteLength": 32768,
"minimumSurfaceCourseLength": 32768,
"minimumUndergroundCourseLength": 32768,
"maximumOutletsPerTile": 256,
"oceanOutlets": true,
"valleyPreference": 8,
"uphillPenalty": 128,
"slopePenalty": 16,
"confluenceAttraction": 1
},
"surface": {
"channel": {
"width": {"min": 128, "max": 128},
"depth": {"min": 64, "max": 64},
"inset": 3,
"maximumIncision": 32,
"roughness": 1,
"roughnessWavelength": 64
},
"banks": {
"freeboard": 4,
"shoreWidth": 6,
"blendSlope": 12,
"minimumBlendWidth": 64,
"maximumBlendWidth": 64,
"exposeCutStrata": true
},
"flow": {"cascadeRun": 8, "waterfallMinimumDrop": 32},
"mouths": {"flareRatio": 4, "maximumOceanApron": 32}
},
"underground": {"mouthLevelingDistance": 512}
}
""", "\"bankMultiplier\": 4"));
PackHydrologyValidator.Validation result = validate(pack);
assertTrue(result.errors().toString(), result.errors().isEmpty());
}
@Test
public void rejectsRoutingWeightsSpacingAndCourseLengths() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"routing": {
"tileSize": 2048,
"sampleSpacing": 24,
"maximumRouteLength": 256,
"minimumSurfaceCourseLength": 512,
"minimumUndergroundCourseLength": 512,
"maximumOutletsPerTile": 0,
"valleyPreference": 8.5,
"uphillPenalty": 128.5,
"slopePenalty": 16.5,
"confluenceAttraction": 1.5,
"oceanOutlets": false,
"inlandOutlets": []
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"routing": {
"tileSize": 128,
"sampleSpacing": 4,
"maximumRouteLength": 128,
"valleyPreference": -0.5,
"uphillPenalty": -0.5,
"slopePenalty": -0.5,
"confluenceAttraction": -0.5
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
assertContains(aboveResult.errors(), ".sampleSpacing must be one of [8, 16, 32, 64].");
assertContains(aboveResult.errors(), "tileSize must be divisible by sampleSpacing");
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(), "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");
assertContains(aboveResult.errors(), "confluenceAttraction must be at most 1.0");
assertContains(aboveResult.errors(), "must enable oceanOutlets or select at least one");
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(), "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");
assertContains(belowResult.errors(), "confluenceAttraction must be at least 0.0");
}
@Test
public void rejectsSurfaceChannelValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"surface": {
"channel": {
"width": {"min": 20, "max": 2},
"depth": {"min": 2, "max": 65},
"inset": 4,
"maximumIncision": 33,
"roughness": 1.5,
"roughnessWavelength": 65
}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"surface": {
"channel": {
"width": {"min": 0, "max": 8},
"depth": {"min": 0, "max": 4},
"inset": -1,
"maximumIncision": 0,
"roughness": -0.1,
"roughnessWavelength": 3
}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
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(), "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");
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(), "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 rejectsSurfaceBankValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"surface": {
"banks": {
"freeboard": 5,
"shoreWidth": 6.5,
"blendSlope": 12.5,
"minimumBlendWidth": 40,
"maximumBlendWidth": 65,
"exposeCutStrata": "yes"
}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"surface": {
"banks": {
"freeboard": -1,
"shoreWidth": 0.4,
"blendSlope": 0.4,
"minimumBlendWidth": 0,
"maximumBlendWidth": 8
}
}
}
"""));
File unordered = pack(riversDimension("""
{
"enabled": true,
"surface": {
"banks": {"minimumBlendWidth": 20, "maximumBlendWidth": 10}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
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");
assertContains(unorderedResult.errors(), "minimumBlendWidth must not exceed maximumBlendWidth");
}
@Test
public void rejectsSurfaceFlowAndMouthValuesOutsideTheirBounds() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"surface": {
"flow": {"cascadeRun": 9, "waterfallMinimumDrop": 33},
"mouths": {"flareRatio": 4.5, "maximumOceanApron": 33}
}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"surface": {
"flow": {"cascadeRun": 0, "waterfallMinimumDrop": 1},
"mouths": {"flareRatio": 0.5, "maximumOceanApron": -1}
}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
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(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");
}
@Test
public void rejectsUndergroundMouthLevelingDistanceOutsideItsBounds() throws Exception {
File above = pack(riversDimension("""
{
"enabled": true,
"underground": {"mouthLevelingDistance": 513}
}
"""));
File below = pack(riversDimension("""
{
"enabled": true,
"underground": {"mouthLevelingDistance": 15}
}
"""));
PackHydrologyValidator.Validation aboveResult = validate(above);
PackHydrologyValidator.Validation belowResult = validate(below);
assertContains(aboveResult.errors(), "mouthLevelingDistance must be at most 512");
assertContains(belowResult.errors(), "mouthLevelingDistance must be at least 16");
}
@Test
public void rejectsGeometryComplexityAndCapabilityFailures() throws Exception {
File pack = pack("""
{
"regions": ["region"],
@@ -80,14 +395,7 @@ public class PackHydrologyValidatorTest {
"routing": {
"tileSize": 8192,
"sampleSpacing": 8,
"refinementSpacing": 3,
"branching": {
"minimumSurfaceCourseLength": 32769,
"minimumUndergroundCourseLength": 32769
},
"maximumRouteLength": 32768,
"oceanOutlets": false,
"inlandOutlets": []
"maximumRouteLength": 32768
},
"geometry": {
"meanders": {
@@ -95,7 +403,7 @@ public class PackHydrologyValidatorTest {
"detailWavelength": 129,
"maximumTurnDegrees": 151
},
"surface": {
"underground": {
"bedRoundness": 7,
"bedRoughness": -0.1,
"wallRoughness": 1.1,
@@ -113,24 +421,7 @@ public class PackHydrologyValidatorTest {
},
"surface": {
"sources": {"density": 64, "minimumElevation": 128, "minimumPerTile": 64,
"minimumSpacing": 8193},
"channel": {
"width": {"min": 20, "max": 2},
"depth": {"min": 2, "max": 5},
"surfaceInset": {"min": 0, "max": 65},
"maximumIncision": 65,
"shoreWidth": 3,
"terrainBlendWidth": {"min": 3, "max": 65}
},
"hydraulics": {
"targetPoolLength": {"min": 8, "max": 32},
"riffleDrop": 5,
"maximumGradualDrop": 3,
"maximumGradualLength": 2,
"waterfallMinimumDrop": 8
},
"ridgeTunnels": {"maximumLength": 4097, "headroom": 4},
"mouths": {"levelingDistance": 4, "maximumOceanApron": 8}
"minimumSpacing": 8193}
},
"underground": {
"sources": {"density": 64, "minimumPerTile": 64},
@@ -152,7 +443,6 @@ public class PackHydrologyValidatorTest {
PackHydrologyValidator.Validation result = validate(pack);
assertContains(result.errors(), "sampleSpacing must be divisible by refinementSpacing");
assertContains(result.errors(), "primaryWavelength must be at least 8");
assertContains(result.errors(), "detailWavelength must be at most 128");
assertContains(result.errors(), "detailWavelength must not exceed primaryWavelength");
@@ -161,8 +451,6 @@ public class PackHydrologyValidatorTest {
assertContains(result.errors(), "bedRoughness must be at least 0");
assertContains(result.errors(), "wallRoughness must be at most 1");
assertContains(result.errors(), "roughnessWavelength must be at least 3");
assertContains(result.errors(), "minimumSurfaceCourseLength must be at most 32768");
assertContains(result.errors(), "minimumUndergroundCourseLength must be at most 32768");
assertContains(result.errors(), "cascadeRunPerBlock must be at most 16");
assertContains(result.errors(), "cascadeExponent must be at least 0.25");
assertContains(result.errors(), "maximumCascadeStep must be at most 4");
@@ -171,18 +459,6 @@ public class PackHydrologyValidatorTest {
assertContains(result.errors(), "basinWidthRatio must be at most 4");
assertContains(result.errors(), "maximumBasinDepth must be at most 32");
assertContains(result.errors(), "minimumSpacing must be at most 8192");
assertContains(result.errors(), "must enable oceanOutlets or select at least one");
assertContains(result.errors(), "shoreWidth must be at most 2");
assertContains(result.errors(), "terrainBlendWidth.min must be at least 4");
assertContains(result.errors(), "terrainBlendWidth.max must be at most 64");
assertContains(result.errors(), "width.min must not exceed max");
assertContains(result.errors(), "surfaceInset.min must be at least 1");
assertContains(result.errors(), "surfaceInset.max must be at most 64");
assertContains(result.errors(), "maximumIncision must be at most 64");
assertContains(result.errors(), "riffleDrop must not exceed maximumGradualDrop");
assertContains(result.errors(), "waterfallMinimumDrop must equal maximumGradualDrop plus one");
assertContains(result.errors(), "maximumGradualLength must be at least maximumGradualDrop");
assertContains(result.errors(), "maximumLength must be at most 4096");
assertContains(result.errors(), "coarse lattice nodes");
assertContains(result.errors(), "route samples per tile");
assertContains(result.errors(), "requires useMantle to be true");
@@ -267,12 +543,13 @@ public class PackHydrologyValidatorTest {
"surfaceBiomes": ["missing"],
"mouthBiomes": "river",
"shoreBiomes": ["biome", "biome"],
"bankBiomes": [17],
"bankBiomes": ["missing_bank"],
"floodedCaveBiomes": ["../escape"],
"widthMultiplier": 0,
"depthMultiplier": 17,
"incisionMultiplier": -1,
"routingMultiplier": -1
"routingMultiplier": -1,
"bankMultiplier": 4.5
}
}
""");
@@ -286,12 +563,34 @@ public class PackHydrologyValidatorTest {
assertContains(result.errors(), "surfaceBiomes[0] references missing biome 'missing'");
assertContains(result.errors(), ".mouthBiomes must be an array or null");
assertContains(result.errors(), "shoreBiomes[1] duplicates biome 'biome'");
assertContains(result.errors(), "bankBiomes[0] must name a biome resource");
assertContains(result.errors(), "bankBiomes[0] references missing biome 'missing_bank'");
assertContains(result.errors(), "floodedCaveBiomes[0] must name a biome resource");
assertContains(result.errors(), "widthMultiplier must be at least");
assertContains(result.errors(), "depthMultiplier must be at most");
assertContains(result.errors(), "incisionMultiplier must be at least");
assertContains(result.errors(), "routingMultiplier must be at least");
assertContains(result.errors(), "bankMultiplier must be at most 4.0");
}
@Test
public void rejectsNegativeBankMultiplierAndNonBiomeBankReferences() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"hydrology": {
"rivers": {"enabled": true}
},
"riverPolicy": {
"bankBiomes": [17],
"bankMultiplier": -0.5
}
}
""");
PackHydrologyValidator.Validation result = validate(pack);
assertContains(result.errors(), "bankBiomes[0] must name a biome resource");
assertContains(result.errors(), "bankMultiplier must be at least 0.0");
}
@Test
@@ -706,9 +1005,8 @@ public class PackHydrologyValidatorTest {
"rivers": {
"enabled": true,
"routing": {
"tileSize": 2048,
"sampleSpacing": 63,
"refinementSpacing": 8
"tileSize": 1000,
"sampleSpacing": 64
},
"profiles": [
{
@@ -749,6 +1047,23 @@ public class PackHydrologyValidatorTest {
return pack;
}
private String riversDimension(String riversJson) {
return riversDimension(riversJson, null);
}
private String riversDimension(String riversJson, String policyEntries) {
String policy = policyEntries == null ? "" : ",\n\"riverPolicy\": {" + policyEntries + "}";
return """
{
"regions": ["region"],
"dimensionHeight": {"min": -256, "max": 512},
"hydrology": {
"rivers": %s
}%s
}
""".formatted(riversJson, policy);
}
private String canonicalDimension() {
return """
{
@@ -760,50 +1075,86 @@ public class PackHydrologyValidatorTest {
"routing": {
"tileSize": 2048,
"sampleSpacing": 64,
"refinementSpacing": 8,
"maximumRouteLength": 8192,
"minimumSurfaceCourseLength": 384,
"minimumUndergroundCourseLength": 192,
"maximumOutletsPerTile": 4,
"oceanOutlets": true,
"inlandOutlets": ["SINKHOLE_GROTTO"]
"inlandOutlets": ["SINKHOLE_GROTTO"],
"valleyPreference": 1.5,
"uphillPenalty": 24,
"slopePenalty": 2,
"confluenceAttraction": 0.2
},
"geometry": {
"meanders": {
"primaryWavelength": 64,
"detailWavelength": 12,
"primaryStrength": 0.34,
"detailStrength": 0.42,
"maximumOffsetRatio": 0.48,
"smoothingPasses": 1,
"maximumTurnDegrees": 82
},
"underground": {
"bedRoundness": 2.4,
"bedRoughness": 0.28,
"wallRoughness": 0.24,
"roughnessWavelength": 11
},
"grottos": {"bedRoundness": 2.4},
"drops": {
"cascadeRunPerBlock": 2,
"cascadeExponent": 1.4,
"maximumCascadeStep": 2,
"flowWidthRatio": 0.45,
"maximumFlowDepth": 2,
"basinWidthRatio": 1.8,
"maximumBasinDepth": 8
}
},
"surface": {
"enabled": true,
"sources": {
"density": 0.5,
"minimumElevation": 88,
"minimumPerTile": 1
"minimumPerTile": 1,
"minimumSpacing": 384
},
"channel": {
"width": {"min": 4, "max": 32, "style": {"style": "STATIC"}},
"depth": {"min": 2, "max": 5},
"maximumIncision": 12,
"width": {"min": 4, "max": 8, "style": {"style": "STATIC"}},
"depth": {"min": 2, "max": 4},
"inset": 1,
"maximumIncision": 10,
"roughness": 0.25,
"roughnessWavelength": 16
},
"banks": {
"freeboard": 1,
"shoreWidth": 1.5,
"terrainBlendWidth": {"min": 4, "max": 10}
"blendSlope": 3,
"minimumBlendWidth": 4,
"maximumBlendWidth": 32,
"exposeCutStrata": true
},
"hydraulics": {
"targetPoolLength": {"min": 48, "max": 160},
"riffleDrop": 1,
"maximumGradualDrop": 3,
"maximumGradualLength": 10,
"waterfallMinimumDrop": 4
},
"ridgeTunnels": {
"enabled": true,
"maximumLength": 192,
"headroom": 10
"flow": {
"cascadeRun": 2,
"waterfallMinimumDrop": 6
},
"mouths": {
"levelingDistance": 64,
"maximumOceanApron": 2
"flareRatio": 1.6,
"maximumOceanApron": 8
}
},
"underground": {
"enabled": true,
"sources": {"density": 0.5, "minimumPerTile": 1},
"sources": {"density": 0.5, "minimumPerTile": 1, "minimumSpacing": 512},
"fluidLevel": {"min": -48, "max": 50},
"channelWidth": {"min": 4, "max": 18},
"depth": {"min": 2, "max": 5},
"channelWidth": {"min": 3, "max": 8},
"depth": {"min": 1, "max": 3},
"headroom": {"min": 6, "max": 14},
"connectToExistingCaves": true
"connectToExistingCaves": true,
"mouthLevelingDistance": 64
},
"grottos": {
"coastal": {
@@ -859,12 +1210,13 @@ public class PackHydrologyValidatorTest {
"surfaceBiomes": ["river_surface"],
"mouthBiomes": ["river_surface"],
"shoreBiomes": ["river_shore"],
"bankBiomes": ["river_dry"],
"floodedCaveBiomes": ["river_dry"],
"bankBiomes": ["river_bank"],
"floodedCaveBiomes": ["river_bank"],
"widthMultiplier": 1,
"depthMultiplier": 1,
"incisionMultiplier": 1,
"routingMultiplier": 1
"routingMultiplier": 1,
"bankMultiplier": 1
}
}
""";