mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-18 14:34:05 +00:00
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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-877
@@ -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
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Reference in New Issue
Block a user