From 00f5a16c20f2a36ee4d00a647b4ee30d7f295142 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 1 Sep 2026 19:30:29 -0400 Subject: [PATCH] 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 --- .../core/pack/PackHydrologyValidator.java | 208 +-- .../hydrology/HydrologyFootprintCompiler.java | 881 +----------- .../engine/hydrology/HydrologyPlanner.java | 1193 ----------------- .../engine/object/IrisRiverHydrology.java | 2 +- .../core/pack/PackHydrologyValidatorTest.java | 510 +++++-- 5 files changed, 510 insertions(+), 2284 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java index 2024e65b4..b3c965aa7 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java @@ -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 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 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 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 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 errors, List 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 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 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 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 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 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 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 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 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 inlandOutlets ) { private static RoutingValues defaults() { - return new RoutingValues(2048, 64, 8, 16384, Set.of()); + return new RoutingValues(2048, 64, 16384, Set.of()); } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java index f0e371c5e..5490c38ff 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java @@ -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 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 columns) { - ArrayList wetColumns = new ArrayList<>(); - for (Long2ObjectMap.Entry 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 surfaceSweeps(RiverCourse course, boolean includeFalling) { - ArrayList 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 fallingSurfaceSweeps(RiverCourse course, int segmentIndex) { - HydraulicSegment segment = course.segments().get(segmentIndex); - List 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 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 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 points = new ArrayList<>(); - for (int segmentIndex = startSegmentIndex; segmentIndex < endSegmentIndex; segmentIndex++) { - HydraulicSegment segment = course.segments().get(segmentIndex); - List 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 profileSurfaceSweep( - List 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 transitionShapes = smoothSurfaceSweepShapes(rawPoints, distances); - ArrayList 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 smoothSurfaceSweepShapes( - List points, - double[] distances - ) { - ArrayList 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 columns, - SurfaceSweep sweep, - boolean validationOnly - ) { - Long2ObjectOpenHashMap 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 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 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 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 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 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 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 entry : layers.entrySet()) { - entry.setValue(raisedDrySurfaceLayer(entry.getValue(), naturalHeight, minimumBed)); - } - } - private List resolveSurfaceHeadConflicts(List candidates) { HashMap selectedByCourse = new HashMap<>(); for (HydrologyColumnLayer candidate : candidates) { diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java index fa8708b9d..ff3622ba7 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java @@ -19,8 +19,6 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -32,7 +30,6 @@ import java.util.Objects; import java.util.OptionalLong; import java.util.PriorityQueue; import java.util.Set; -import java.util.TreeMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; @@ -55,7 +52,6 @@ public final class HydrologyPlanner { private static final long GRADED_DROP_APPROACH_SALT = 0x474441505052L; private static final long GRADED_DROP_RUN_SALT = 0x474452554eL; private static final long GRADED_DROP_OUTFLOW_SALT = 0x47444f5554L; - private static final long MIXED_BORE_SEGMENT_SALT = 0x4d4958424f5245L; private static final long DIAGNOSTIC_SALT = 0x444941474e4f5354L; private static final long ROUTE_ANCHOR_X_SALT = 0x52414e434858L; private static final long ROUTE_ANCHOR_Z_SALT = 0x52414e43485aL; @@ -4099,15 +4095,6 @@ public final class HydrologyPlanner { return selectedIndex; } - private double maximumRouteTurnDegrees(List route) { - int sharpestIndex = sharpestRoutePoint(route); - return sharpestIndex < 1 ? 0D : routeTurnDegrees( - route.get(sharpestIndex - 1), - route.get(sharpestIndex), - route.get(sharpestIndex + 1) - ); - } - private double routeTurnDegrees(HydrologyPoint previous, HydrologyPoint point, HydrologyPoint next) { double incomingX = point.x() - previous.x(); double incomingZ = point.z() - previous.z(); @@ -4478,7 +4465,6 @@ public final class HydrologyPlanner { path.edges(), outlet, true, - false, path.organicSurfaceRequired() ); appendOutletSegments( @@ -4507,132 +4493,6 @@ public final class HydrologyPlanner { )); } - private void containUnsupportedSurfaceSegments(List segments) { - int conversionLimit = segments.size(); - if (segments.size() >= 3 - && segments.get(segments.size() - 2).type() == HydrologyFeatureType.SINKHOLE - && segments.getLast().type() == HydrologyFeatureType.INLAND_GROTTO) { - conversionLimit -= 2; - } - for (int segmentIndex = 0; segmentIndex < conversionLimit; segmentIndex++) { - HydraulicSegment segment = segments.get(segmentIndex); - if (!segment.type().isSurface() || segment.type() == HydrologyFeatureType.MOUTH) { - continue; - } - boolean containedTerrain = false; - for (HydrologyPoint point : segment.centerline()) { - HydrologyTerrainSample terrain = sampleDetailed(point.x(), point.z()); - HydrologyTerrainSample basis = sampleBasis(point.x(), point.z()); - boolean finalTerrainExceedsBasisLimit = terrain != null - && basis != null - && terrain.naturalHeight() - point.y() > permittedSurfaceIncision(terrain) - && basis.naturalHeight() - point.y() <= permittedSurfaceIncision(basis); - boolean flatChannelExceedsLimit = terrain != null - && segment.type() == HydrologyFeatureType.SURFACE_POOL - && terrain.naturalHeight() - point.y() > permittedSurfaceIncision(terrain); - if (terrain == null - || terrain.naturalHeight() <= settings.seaLevel() - || finalTerrainExceedsBasisLimit - || flatChannelExceedsLimit) { - containedTerrain = true; - break; - } - } - if (!containedTerrain) { - continue; - } - HydrologyFeatureType containedType = segment.drop() > 0 - ? HydrologyFeatureType.UNDERGROUND_DROP - : HydrologyFeatureType.RIDGE_BORE; - segments.set(segmentIndex, new HydraulicSegment( - segment.id(), - segment.courseId(), - containedType, - segment.upstreamHeadY(), - segment.downstreamHeadY(), - segment.width(), - segment.depth(), - false, - segment.receivingPool(), - segment.centerline() - )); - } - } - - private boolean surfaceTransitionRecoveriesAccepted(List segments) { - double recovery = Double.POSITIVE_INFINITY; - boolean transitionActive = false; - for (int segmentIndex = 0; segmentIndex < segments.size(); segmentIndex++) { - HydraulicSegment segment = segments.get(segmentIndex); - boolean surfaceDrop = segment.type().isSurface() && segment.type().isDrop(); - boolean coastalOutletDrop = surfaceDrop - && segmentIndex + 1 < segments.size() - && segments.get(segmentIndex + 1).type() == HydrologyFeatureType.MOUTH; - if (coastalOutletDrop) { - continue; - } - if (!segment.type().isSurface()) { - recovery = Double.POSITIVE_INFINITY; - transitionActive = false; - continue; - } - if (surfaceDrop) { - if (!transitionActive - && Double.isFinite(recovery) - && recovery < settings.hydraulics().minimumTargetPoolLength()) { - return false; - } - recovery = 0D; - transitionActive = !segment.receivingPool(); - continue; - } - if (transitionActive && segment.receivingPool()) { - transitionActive = false; - } else if (!transitionActive && Double.isFinite(recovery)) { - recovery += hydraulicSegmentLength(segment); - } - } - return true; - } - - private double hydraulicSegmentLength(HydraulicSegment segment) { - double length = 0D; - for (int pointIndex = 1; pointIndex < segment.centerline().size(); pointIndex++) { - HydrologyPoint previous = segment.centerline().get(pointIndex - 1); - HydrologyPoint current = segment.centerline().get(pointIndex); - length += StrictMath.hypot(current.x() - previous.x(), current.z() - previous.z()); - } - return length; - } - - private boolean surfaceExposureAccepted(List segments) { - int minimumExposedLength = settings.routing().branching().minimumSurfaceCourseLength(); - if (minimumExposedLength > 0 && (segments.isEmpty() - || !segments.getFirst().type().isSurface() - || segments.getFirst().type() == HydrologyFeatureType.MOUTH)) { - return false; - } - double exposedLength = 0D; - double exposedRunLength = 0D; - double longestExposedRunLength = 0D; - for (HydraulicSegment segment : segments) { - if (!segment.type().isSurface() || segment.type() == HydrologyFeatureType.MOUTH) { - exposedRunLength = 0D; - continue; - } - double length = hydraulicSegmentLength(segment); - exposedLength += length; - exposedRunLength += length; - longestExposedRunLength = Math.max(longestExposedRunLength, exposedRunLength); - } - int minimumContinuousLength = Math.min( - minimumExposedLength, - Math.multiplyExact(settings.routing().sampleSpacing(), 2) - ); - return exposedLength >= minimumExposedLength - && longestExposedRunLength >= minimumContinuousLength; - } - private boolean surfaceShapeAccepted(List routedCenterline) { List centerline = withoutDuplicateRoutePoints(routedCenterline); if (centerline.size() < 3) { @@ -4778,521 +4638,6 @@ public final class HydrologyPlanner { return List.copyOf(sampled); } - private void smoothFlatSurfaceDimensions(List segments) { - HydraulicSegment previous = null; - for (int segmentIndex = 0; segmentIndex < segments.size(); segmentIndex++) { - HydraulicSegment segment = segments.get(segmentIndex); - boolean flatSurface = segment.type().isSurface() - && segment.drop() == 0 - && !segment.fallingFluid(); - if (!flatSurface) { - previous = null; - continue; - } - if (previous == null) { - previous = segment; - continue; - } - int width = Math.max( - previous.width() - 2, - Math.min(previous.width() + 2, segment.width()) - ); - int depth = Math.max( - previous.depth() - 1, - Math.min(previous.depth() + 1, segment.depth()) - ); - if (width != segment.width() || depth != segment.depth()) { - segment = new HydraulicSegment( - segment.id(), - segment.courseId(), - segment.type(), - segment.upstreamHeadY(), - segment.downstreamHeadY(), - width, - depth, - segment.fallingFluid(), - segment.receivingPool(), - segment.centerline() - ); - segments.set(segmentIndex, segment); - } - previous = segment; - } - } - - private int surfaceTransitionRunEnd( - CoursePath path, - int[] heads, - int[] widths, - int[] depths, - int startPairIndex, - int minimumPoolLength - ) { - int lastDropPair = startPairIndex; - double recoveryLength = 0D; - for (int pairIndex = startPairIndex + 1; pairIndex < heads.length - 1; pairIndex++) { - if (isDirectMouthPair(path, pairIndex)) { - break; - } - HydrologyPoint start = path.points().get(pairIndex); - HydrologyPoint end = path.points().get(pairIndex + 1); - if (heads[pairIndex] > heads[pairIndex + 1]) { - if (recoveryLength >= minimumPoolLength) { - break; - } - HydrologyFeatureType type = surfaceSegmentType( - path, - pairIndex, - heads[pairIndex], - heads[pairIndex + 1], - widths[pairIndex], - depths[pairIndex] - ); - if (!type.isSurface()) { - break; - } - lastDropPair = pairIndex; - recoveryLength = 0D; - continue; - } - HydrologyFeatureType type = surfaceSegmentType( - path, - pairIndex, - heads[pairIndex], - heads[pairIndex + 1], - widths[pairIndex], - depths[pairIndex] - ); - if (type != HydrologyFeatureType.SURFACE_POOL) { - break; - } - recoveryLength += StrictMath.hypot(end.x() - start.x(), end.z() - start.z()); - } - return lastDropPair; - } - - private boolean addSurfaceTransitionRunSegments( - long courseId, - int startPairIndex, - int endPairIndex, - int width, - int depth, - CoursePath path, - int[] heads, - List segments - ) { - int upstreamHead = heads[startPairIndex]; - int downstreamHead = heads[endPairIndex + 1]; - HydrologyPlannerSettings.Drops drops = settings.geometry().drops(); - List raster = rasterCenterline( - transitionCenterline(path, heads, startPairIndex, endPairIndex) - ); - List graded = terrainBackedSurfaceTransitionGrade( - raster, - upstreamHead, - downstreamHead, - drops - ); - if (graded.size() < 2) { - return false; - } - int segmentStart = 0; - int segmentOrdinal = 0; - for (int pointIndex = 0; pointIndex < graded.size() - 1; pointIndex++) { - int stepDrop = graded.get(pointIndex).y() - graded.get(pointIndex + 1).y(); - if (stepDrop <= 1) { - continue; - } - if (pointIndex > segmentStart) { - addSurfaceTransitionSlice( - courseId, - startPairIndex, - segmentOrdinal++, - graded.subList(segmentStart, pointIndex + 1), - width, - depth, - false, - segments - ); - } - boolean finalSlice = pointIndex + 1 == graded.size() - 1; - segments.add(new HydraulicSegment( - HydrologyHash.mix( - worldSeed, - SEGMENT_SALT, - courseId, - startPairIndex, - GRADED_DROP_RUN_SALT, - segmentOrdinal++ - ), - courseId, - HydrologyFeatureType.WATERFALL, - graded.get(pointIndex).y(), - graded.get(pointIndex + 1).y(), - hydraulicDropWidth(width, true), - drops.flowDepth(depth), - true, - finalSlice, - List.of(graded.get(pointIndex), graded.get(pointIndex + 1)) - )); - segmentStart = pointIndex + 1; - } - if (!addSurfaceTransitionSlice( - courseId, - startPairIndex, - segmentOrdinal, - graded.subList(segmentStart, graded.size()), - width, - depth, - true, - segments - ) && segmentStart < graded.size() - 1) { - return false; - } - return true; - } - - private boolean addSurfaceTransitionSlice( - long courseId, - int startPairIndex, - int segmentOrdinal, - List centerline, - int width, - int depth, - boolean receivingPool, - List segments - ) { - if (centerline.size() < 2) { - return false; - } - int upstreamHead = centerline.getFirst().y(); - int downstreamHead = centerline.getLast().y(); - HydrologyFeatureType type = upstreamHead == downstreamHead - ? HydrologyFeatureType.SURFACE_POOL - : upstreamHead - downstreamHead <= settings.hydraulics().riffleDrop() - ? HydrologyFeatureType.RIFFLE - : HydrologyFeatureType.CASCADE; - int segmentWidth = receivingPool ? Math.addExact(width, 2) : width; - int segmentDepth = receivingPool ? Math.addExact(depth, 1) : depth; - segments.add(new HydraulicSegment( - HydrologyHash.mix( - worldSeed, - SEGMENT_SALT, - courseId, - startPairIndex, - GRADED_DROP_RUN_SALT, - segmentOrdinal - ), - courseId, - type, - upstreamHead, - downstreamHead, - segmentWidth, - segmentDepth, - false, - receivingPool, - List.copyOf(centerline) - )); - return true; - } - - private List terrainBackedSurfaceTransitionGrade( - List points, - int upstreamHead, - int downstreamHead, - HydrologyPlannerSettings.Drops drops - ) { - int pointCount = points.size(); - int drop = upstreamHead - downstreamHead; - if (pointCount < 2 || drop <= 0) { - return List.of(); - } - int[] maximumTerrainHeads = new int[pointCount]; - int[] minimumTerrainHeads = new int[pointCount]; - int[] requiredLowerHeads = new int[pointCount]; - int[] reachableUpperHeads = new int[pointCount]; - int[] stepCapacities = new int[pointCount - 1]; - for (int pointIndex = 0; pointIndex < pointCount; pointIndex++) { - HydrologyPoint point = points.get(pointIndex); - HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z()); - int permittedIncision = permittedSurfaceIncision(terrain); - maximumTerrainHeads[pointIndex] = terrain.naturalHeight() - 1; - for (GridOffset offset : CARDINAL_OFFSETS) { - HydrologyTerrainSample neighbor = sampleBasis( - point.x() + offset.x(), - point.z() + offset.z() - ); - maximumTerrainHeads[pointIndex] = Math.min( - maximumTerrainHeads[pointIndex], - neighbor.naturalHeight() - 1 - ); - } - minimumTerrainHeads[pointIndex] = terrain.naturalHeight() - permittedIncision; - if (pointIndex > 0) { - HydrologyTerrainSample previousTerrain = sampleBasis( - points.get(pointIndex - 1).x(), - points.get(pointIndex - 1).z() - ); - int naturalDrop = previousTerrain.naturalHeight() - terrain.naturalHeight(); - stepCapacities[pointIndex - 1] = naturalDrop >= settings.hydraulics().waterfallMinimumDrop() - ? naturalDrop - : 1; - } - } - reachableUpperHeads[pointCount - 1] = Math.min( - downstreamHead, - maximumTerrainHeads[pointCount - 1] - ); - requiredLowerHeads[pointCount - 1] = Math.max( - downstreamHead, - minimumTerrainHeads[pointCount - 1] - ); - for (int pointIndex = pointCount - 2; pointIndex >= 0; pointIndex--) { - reachableUpperHeads[pointIndex] = Math.min( - maximumTerrainHeads[pointIndex], - Math.addExact(reachableUpperHeads[pointIndex + 1], stepCapacities[pointIndex]) - ); - requiredLowerHeads[pointIndex] = Math.max( - minimumTerrainHeads[pointIndex], - requiredLowerHeads[pointIndex + 1] - ); - } - if (upstreamHead > reachableUpperHeads[0] - || upstreamHead < minimumTerrainHeads[0] - || downstreamHead < minimumTerrainHeads[pointCount - 1]) { - return List.of(); - } - ArrayList graded = new ArrayList<>(pointCount); - int previousHead = upstreamHead; - for (int pointIndex = 0; pointIndex < pointCount; pointIndex++) { - HydrologyPoint point = points.get(pointIndex); - double progress = pointIndex / (double) (pointCount - 1); - int idealHead = upstreamHead - (int) StrictMath.round( - drop * StrictMath.pow(progress, drops.cascadeExponent()) - ); - int maximumStep = pointIndex == 0 ? 0 : stepCapacities[pointIndex - 1]; - int lowerHead = Math.max( - requiredLowerHeads[pointIndex], - Math.max(downstreamHead, previousHead - maximumStep) - ); - int upperHead = Math.min( - previousHead, - Math.min(maximumTerrainHeads[pointIndex], reachableUpperHeads[pointIndex]) - ); - if (lowerHead > upperHead) { - return List.of(); - } - int head = Math.max(lowerHead, Math.min(idealHead, upperHead)); - if (pointIndex == 0) { - head = upstreamHead; - } - if (pointIndex == pointCount - 1) { - head = downstreamHead; - } - if (head < previousHead - maximumStep - || head > reachableUpperHeads[pointIndex] - || head < minimumTerrainHeads[pointIndex] - || head > maximumTerrainHeads[pointIndex]) { - return List.of(); - } - graded.add(new HydrologyPoint(point.x(), head, point.z())); - previousHead = head; - } - return List.copyOf(graded); - } - - private List transitionCenterline( - CoursePath path, - int[] heads, - int startPairIndex, - int endPairIndex - ) { - ArrayList centerline = new ArrayList<>(endPairIndex - startPairIndex + 2); - for (int pointIndex = startPairIndex; pointIndex <= endPairIndex + 1; pointIndex++) { - centerline.add(withY(path.points().get(pointIndex), heads[pointIndex])); - } - return List.copyOf(centerline); - } - - private int averageDimension(int[] dimensions, int startIndex, int endIndex) { - long total = 0L; - for (int index = startIndex; index <= endIndex; index++) { - total += dimensions[index]; - } - return Math.toIntExact(Math.round(total / (double) (endIndex - startIndex + 1))); - } - - private boolean addMixedSurfaceBoreSegments( - long courseId, - int pairIndex, - int upstreamHead, - int downstreamHead, - int width, - int depth, - HydrologyPoint start, - HydrologyPoint end, - List segments - ) { - List raster = rasterLine(start, end); - if (raster.size() < 3) { - return false; - } - int maximumStep = settings.geometry().drops().maximumCascadeStep(); - int drop = upstreamHead - downstreamHead; - if (drop > Math.multiplyExact(raster.size() - 1, maximumStep)) { - return false; - } - List graded = drop == 0 - ? levelSlice(raster, 0, raster.size() - 1, upstreamHead) - : gradedDropCenterline(raster, upstreamHead, downstreamHead, settings.geometry().drops(), maximumStep); - boolean[] boreEdges = new boolean[graded.size() - 1]; - boolean hasBore = false; - boolean hasSurface = false; - for (int edgeIndex = 0; edgeIndex < boreEdges.length; edgeIndex++) { - HydrologyPoint edgeStart = graded.get(edgeIndex); - HydrologyPoint edgeEnd = graded.get(edgeIndex + 1); - boolean bore = requiresBore(edgeStart, edgeStart.y()) || requiresBore(edgeEnd, edgeEnd.y()); - boreEdges[edgeIndex] = bore; - hasBore |= bore; - hasSurface |= !bore; - } - if (!hasBore || !hasSurface) { - return false; - } - int runStart = 0; - int runOrdinal = 0; - while (runStart < boreEdges.length) { - boolean bore = boreEdges[runStart]; - int runEnd = runStart; - while (runEnd + 1 < boreEdges.length && boreEdges[runEnd + 1] == bore) { - runEnd++; - } - List centerline = List.copyOf(graded.subList(runStart, runEnd + 2)); - int runDrop = centerline.getFirst().y() - centerline.getLast().y(); - HydrologyFeatureType type = bore - ? runDrop > 0 ? HydrologyFeatureType.UNDERGROUND_DROP : HydrologyFeatureType.RIDGE_BORE - : surfaceSliceType(centerline, runDrop); - boolean falling = type == HydrologyFeatureType.WATERFALL; - int segmentWidth = runDrop > 0 ? hydraulicDropWidth(width, true) : width; - int segmentDepth = runDrop > 0 ? settings.geometry().drops().flowDepth(depth) : depth; - segments.add(new HydraulicSegment( - HydrologyHash.mix( - worldSeed, - SEGMENT_SALT, - courseId, - pairIndex, - MIXED_BORE_SEGMENT_SALT, - runOrdinal, - type.ordinal() - ), - courseId, - type, - centerline.getFirst().y(), - centerline.getLast().y(), - segmentWidth, - segmentDepth, - falling, - false, - centerline - )); - runOrdinal++; - runStart = runEnd + 1; - } - return true; - } - - private HydrologyFeatureType surfaceSliceType(List centerline, int drop) { - if (drop == 0) { - return HydrologyFeatureType.SURFACE_POOL; - } - if (drop <= settings.hydraulics().riffleDrop()) { - return HydrologyFeatureType.RIFFLE; - } - for (int pointIndex = 1; pointIndex < centerline.size(); pointIndex++) { - if (centerline.get(pointIndex - 1).y() - centerline.get(pointIndex).y() > 1) { - return HydrologyFeatureType.WATERFALL; - } - } - return HydrologyFeatureType.CASCADE; - } - - private void containBriefSurfaceReopenings(List segments) { - int segmentIndex = 1; - while (segmentIndex < segments.size() - 1) { - if (!isSurfaceCourseBore(segments.get(segmentIndex - 1))) { - segmentIndex++; - continue; - } - int surfaceStart = segmentIndex; - double surfaceLength = 0D; - int maximumWidth = 1; - while (segmentIndex < segments.size() && segments.get(segmentIndex).type().isSurface()) { - HydraulicSegment segment = segments.get(segmentIndex); - if (segment.type() == HydrologyFeatureType.MOUTH) { - break; - } - surfaceLength += hydraulicSegmentLength(segment); - maximumWidth = Math.max(maximumWidth, segment.width()); - segmentIndex++; - } - if (surfaceStart == segmentIndex) { - segmentIndex++; - continue; - } - if (segmentIndex >= segments.size() - || !isSurfaceCourseBore(segments.get(segmentIndex))) { - continue; - } - double minimumEmergenceLength = Math.max(16D, maximumWidth * 3D); - if (surfaceLength >= minimumEmergenceLength - || !surfaceRunCanRemainRoofed(segments, surfaceStart, segmentIndex)) { - continue; - } - for (int conversionIndex = surfaceStart; conversionIndex < segmentIndex; conversionIndex++) { - HydraulicSegment segment = segments.get(conversionIndex); - HydrologyFeatureType type = segment.drop() > 0 - ? HydrologyFeatureType.UNDERGROUND_DROP - : HydrologyFeatureType.RIDGE_BORE; - segments.set(conversionIndex, new HydraulicSegment( - segment.id(), - segment.courseId(), - type, - segment.upstreamHeadY(), - segment.downstreamHeadY(), - segment.width(), - segment.depth(), - false, - false, - segment.centerline() - )); - } - } - } - - private boolean surfaceRunCanRemainRoofed( - List segments, - int startIndex, - int endIndex - ) { - for (int segmentIndex = startIndex; segmentIndex < endIndex; segmentIndex++) { - for (HydrologyPoint point : rasterCenterline(segments.get(segmentIndex).centerline())) { - HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z()); - if (terrain == null - || terrain.ocean() - || terrain.naturalHeight() < Math.addExact(point.y(), 2)) { - return false; - } - } - } - return true; - } - - private boolean isSurfaceCourseBore(HydraulicSegment segment) { - return segment.type() == HydrologyFeatureType.RIDGE_BORE - || segment.type() == HydrologyFeatureType.UNDERGROUND_DROP - || segment.type() == HydrologyFeatureType.UNDERGROUND_POOL; - } - private void compileUndergroundCourses( HydrologyTileKey key, SampledGrid grid, @@ -5833,52 +5178,6 @@ public final class HydrologyPlanner { return true; } - private boolean addCoastalMouthDropSegments( - long courseId, - int pairIndex, - int upstreamHead, - int downstreamHead, - int width, - int depth, - List centerline, - boolean surfaceCourse, - List segments - ) { - List raster = rasterCenterline(centerline); - if (raster.size() < 2) { - return false; - } - int lipIndex = raster.size() - 2; - addGradedTransitionSegment( - courseId, - pairIndex, - GRADED_DROP_APPROACH_SALT, - HydrologyFeatureType.SURFACE_POOL, - upstreamHead, - width, - depth, - levelSlice(raster, 0, lipIndex, upstreamHead), - segments - ); - HydrologyPlannerSettings.Drops drops = settings.geometry().drops(); - segments.add(new HydraulicSegment( - HydrologyHash.mix(worldSeed, SEGMENT_SALT, courseId, pairIndex, GRADED_DROP_RUN_SALT), - courseId, - HydrologyFeatureType.WATERFALL, - upstreamHead, - downstreamHead, - hydraulicDropWidth(width, surfaceCourse), - drops.flowDepth(depth), - true, - true, - List.of( - withY(raster.get(lipIndex), upstreamHead), - withY(raster.getLast(), downstreamHead) - ) - )); - return true; - } - private boolean addGradedDropSegments( long courseId, int pairIndex, @@ -6257,7 +5556,6 @@ public final class HydrologyPlanner { List.copyOf(edges), outlet, true, - routing.containedSurface(), routing.organicSurfaceRequired() ); } @@ -6762,7 +6060,6 @@ public final class HydrologyPlanner { List.copyOf(edges), outlet, true, - false, false ) : null; @@ -6823,435 +6120,6 @@ public final class HydrologyPlanner { return pointX * pointX + pointZ * pointZ; } - private int[] surfaceMaximumHeads( - long courseId, - String profileKey, - CoursePath path, - int[] widths - ) { - HydrologyPoint origin = path.points().getFirst(); - int surfaceInset = path.containedSurface() - ? containedSurfaceInset() - : sampleGeometry( - HydrologyGeometrySampler.Field.SURFACE_INSET, - profileKey, - origin.x(), - origin.z(), - courseId, - settings.surface().minimumSurfaceInset(), - settings.surface().maximumSurfaceInset() - ); - int[] maximumHeads = new int[path.points().size()]; - for (int pointIndex = 0; pointIndex < path.points().size(); pointIndex++) { - if (path.reachesOutlet() - && path.outlet().directOcean() - && pointIndex == path.points().size() - 1) { - maximumHeads[pointIndex] = outletHead(path.outlet()); - continue; - } - if (!path.reachesOutlet() && pointIndex == path.points().size() - 1) { - maximumHeads[pointIndex] = outletHead(path.outlet()); - continue; - } - HydrologyPoint point = path.points().get(pointIndex); - HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z()); - int terrainHead = terrain == null - ? Integer.MIN_VALUE - : Math.subtractExact(terrain.naturalHeight(), surfaceInset); - maximumHeads[pointIndex] = path.reachesOutlet() && path.outlet().directOcean() - ? Math.max(outletHead(path.outlet()), terrainHead) - : terrainHead; - } - constrainSurfaceSpanHeads(path, maximumHeads, widths); - return maximumHeads; - } - - private void constrainSurfaceSpanHeads(CoursePath path, int[] maximumHeads, int[] widths) { - int minimumIncision = settings.surface().minimumSurfaceInset(); - for (int pairIndex = 0; pairIndex < path.points().size() - 1; pairIndex++) { - boolean mouthApproach = surfaceMouthApproachPair(path, pairIndex); - HydrologyPoint start = path.points().get(pairIndex); - HydrologyPoint end = path.points().get(pairIndex + 1); - List raster = rasterLine(start, end); - Direction tangent = direction(start.x(), start.z(), end.x(), end.z()); - int bankDistance = surfaceBankDistance(widths[pairIndex]); - int spanCeiling = Integer.MAX_VALUE; - for (HydrologyPoint point : raster) { - HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z()); - if (terrain == null) { - spanCeiling = Integer.MIN_VALUE; - break; - } - spanCeiling = Math.min( - spanCeiling, - Math.subtractExact(terrain.naturalHeight(), minimumIncision) - ); - for (int bankOffset = -bankDistance; bankOffset <= bankDistance; bankOffset++) { - HydrologyTerrainSample corridor = surfaceBankTerrain(point, tangent, bankOffset); - if (corridor == null) { - if (mouthApproach) { - continue; - } - spanCeiling = Integer.MIN_VALUE; - break; - } - spanCeiling = Math.min( - spanCeiling, - Math.subtractExact(corridor.naturalHeight(), minimumIncision) - ); - } - if (spanCeiling == Integer.MIN_VALUE) { - break; - } - } - if (spanCeiling == Integer.MIN_VALUE) { - maximumHeads[pairIndex] = Integer.MIN_VALUE; - maximumHeads[pairIndex + 1] = Integer.MIN_VALUE; - continue; - } - if (mouthApproach) { - spanCeiling = Math.max(outletHead(path.outlet()), spanCeiling); - } - maximumHeads[pairIndex] = Math.min(maximumHeads[pairIndex], spanCeiling); - maximumHeads[pairIndex + 1] = Math.min(maximumHeads[pairIndex + 1], spanCeiling); - } - } - - private int containedSurfaceInset() { - return settings.surface().maximumSurfaceInset(); - } - - private boolean ridgeLengthAccepted(CoursePath path, int[] heads) { - if (!settings.surface().ridgeTunnelsEnabled()) { - for (int index = 0; index < path.points().size() - 1; index++) { - if (requiresBore( - path.points().get(index), - path.points().get(index + 1), - heads[index], - heads[index + 1] - )) { - return false; - } - } - return true; - } - double boreRunLength = 0D; - boolean subterrainPassage = false; - for (int index = 0; index < path.points().size() - 1; index++) { - if (isDirectMouthPair(path, index)) { - boreRunLength = 0D; - subterrainPassage = false; - continue; - } - HydrologyPoint first = path.points().get(index); - HydrologyPoint second = path.points().get(index + 1); - double pairLength = StrictMath.hypot(second.x() - first.x(), second.z() - first.z()); - boolean bore = requiresBore( - first, - second, - heads[index], - heads[index + 1] - ); - if (!bore) { - boreRunLength = 0D; - subterrainPassage = false; - continue; - } - if (traversesSeaLevelTerrain(first, second)) { - boreRunLength = 0D; - subterrainPassage = true; - continue; - } - if (subterrainPassage) { - continue; - } - boreRunLength += pairLength; - if (boreRunLength > settings.surface().maximumRidgeTunnelLength()) { - return false; - } - } - return true; - } - - private HydrologyFeatureType surfaceSegmentType( - CoursePath path, - int index, - int upstreamHead, - int downstreamHead, - int width, - int depth - ) { - boolean bore = !isDirectMouthPair(path, index) && requiresBore( - path.points().get(index), - path.points().get(index + 1), - upstreamHead, - downstreamHead - ); - int drop = upstreamHead - downstreamHead; - if (bore) { - return drop > 0 ? HydrologyFeatureType.UNDERGROUND_DROP : HydrologyFeatureType.RIDGE_BORE; - } - if (drop == 0) { - return HydrologyFeatureType.SURFACE_POOL; - } - if (drop <= settings.hydraulics().riffleDrop()) { - return HydrologyFeatureType.RIFFLE; - } - HydrologyPoint first = path.points().get(index); - HydrologyPoint second = path.points().get(index + 1); - double length = StrictMath.hypot(second.x() - first.x(), second.z() - first.z()); - int rasterLength = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z())) + 1; - int maximumCascadeStep = settings.geometry().drops().stepLimit(HydrologyFeatureType.CASCADE); - int minimumRun = Math.floorDiv(drop + maximumCascadeStep - 1, maximumCascadeStep) + 1; - if (drop <= settings.hydraulics().maximumGradualDrop() - && drop < settings.hydraulics().waterfallMinimumDrop() - && rasterLength >= minimumRun - && length <= settings.hydraulics().maximumGradualLength()) { - return HydrologyFeatureType.CASCADE; - } - return HydrologyFeatureType.WATERFALL; - } - - private boolean distributeSurfaceHeadLoss( - CoursePath path, - int[] heads, - int[] maximumHeads, - int minimumHead - ) { - for (int index = heads.length - 2; index >= 0; index--) { - int downstreamHead = heads[index + 1]; - int supportedDrop = surfacePairDropCapacity(path, index); - int supportedHead = Math.addExact(downstreamHead, supportedDrop); - heads[index] = Math.min(heads[index], Math.min(maximumHeads[index], supportedHead)); - if (heads[index] < downstreamHead || heads[index] < minimumHead) { - return false; - } - } - return true; - } - - private boolean surfaceHeadLossSupportedByTerrain( - CoursePath path, - int[] heads, - int[] widths, - int[] depths - ) { - if (heads.length != path.points().size() - || widths.length != path.points().size() - 1 - || depths.length != path.points().size() - 1) { - return false; - } - return surfaceHeadLossSupportedByTerrain(path, heads, widths, depths, 0, heads.length - 2); - } - - private boolean surfaceHeadLossSupportedByTerrain( - CoursePath path, - int[] heads, - int[] widths, - int[] depths, - int startPairIndex, - int endPairIndex - ) { - for (int index = startPairIndex; index <= endPairIndex; index++) { - int drop = heads[index] - heads[index + 1]; - if (drop <= 0) { - continue; - } - if (drop > surfacePairDropCapacity(path, index)) { - return false; - } - HydrologyFeatureType type = surfaceSegmentType( - path, - index, - heads[index], - heads[index + 1], - widths[index], - depths[index] - ); - if (type == HydrologyFeatureType.WATERFALL - && !isDirectMouthPair(path, index) - && waterfallLipIndex(rasterLine(path.points().get(index), path.points().get(index + 1)), drop) < 0) { - return false; - } - } - return true; - } - - private int surfacePairDropCapacity(CoursePath path, int index) { - HydrologyPoint upstream = path.points().get(index); - HydrologyPoint downstream = path.points().get(index + 1); - HydrologyTerrainSample upstreamTerrain = sampleBasis(upstream.x(), upstream.z()); - HydrologyTerrainSample downstreamTerrain = sampleBasis(downstream.x(), downstream.z()); - int downstreamNaturalHeight = isDirectMouthPair(path, index) - ? outletHead(path.outlet()) - : downstreamTerrain.naturalHeight(); - long terrainLoss = (long) upstreamTerrain.naturalHeight() - downstreamNaturalHeight; - if (terrainLoss <= 0L) { - return 0; - } - List raster = rasterLine(upstream, downstream); - int boundedTerrainLoss = (int) Math.min(Integer.MAX_VALUE, terrainLoss); - int gradualCapacity = Math.min(settings.hydraulics().riffleDrop(), boundedTerrainLoss); - double length = StrictMath.hypot(downstream.x() - upstream.x(), downstream.z() - upstream.z()); - if (length <= settings.hydraulics().maximumGradualLength()) { - int rasterCapacity = Math.multiplyExact( - raster.size() - 1, - settings.geometry().drops().stepLimit(HydrologyFeatureType.CASCADE) - ); - gradualCapacity = Math.min( - boundedTerrainLoss, - Math.min(settings.hydraulics().maximumGradualDrop(), rasterCapacity) - ); - } - if (isDirectMouthPair(path, index)) { - return Math.max(gradualCapacity, boundedTerrainLoss); - } - int waterfallIndex = waterfallLipIndex(raster, settings.hydraulics().waterfallMinimumDrop()); - if (waterfallIndex < 0) { - return gradualCapacity; - } - HydrologyTerrainSample lip = sampleBasis(raster.get(waterfallIndex).x(), raster.get(waterfallIndex).z()); - HydrologyTerrainSample receiver = sampleBasis( - raster.get(waterfallIndex + 1).x(), - raster.get(waterfallIndex + 1).z() - ); - int waterfallCapacity = lip.naturalHeight() - receiver.naturalHeight(); - return Math.max(gradualCapacity, Math.min(boundedTerrainLoss, waterfallCapacity)); - } - - private boolean isDirectMouthPair(CoursePath path, int index) { - return path.reachesOutlet() - && path.outlet().directOcean() - && index == path.points().size() - 2; - } - - private boolean requiresBore(HydrologyPoint point, int head) { - HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z()); - int permittedIncision = permittedSurfaceIncision(terrain); - return terrain.naturalHeight() <= settings.seaLevel() - || terrain.naturalHeight() - head > permittedIncision; - } - - private boolean requiresBore( - HydrologyPoint start, - HydrologyPoint end, - int upstreamHead, - int downstreamHead - ) { - List raster = rasterLine(start, end); - for (HydrologyPoint point : raster) { - int head = surfaceBoreHead(point, upstreamHead, downstreamHead); - if (requiresBore(point, head)) { - return true; - } - } - return false; - } - - private boolean traversesSeaLevelTerrain(HydrologyPoint start, HydrologyPoint end) { - for (HydrologyPoint point : rasterLine(start, end)) { - HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z()); - if (terrain.naturalHeight() <= settings.seaLevel()) { - return true; - } - } - return false; - } - - private int surfaceBoreHead(HydrologyPoint point, int upstreamHead, int downstreamHead) { - if (upstreamHead == downstreamHead) { - return upstreamHead; - } - HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z()); - int terrainHead = Math.subtractExact( - terrain.naturalHeight(), - settings.surface().minimumSurfaceInset() - ); - return Math.max(downstreamHead, Math.min(upstreamHead, terrainHead)); - } - - private boolean surfaceCorridorSupported( - CoursePath path, - int[] heads, - int[] widths - ) { - for (int pairIndex = 0; pairIndex < widths.length; pairIndex++) { - HydrologyPoint start = path.points().get(pairIndex); - HydrologyPoint end = path.points().get(pairIndex + 1); - if (heads[pairIndex] > heads[pairIndex + 1] - || surfaceMouthApproachPair(path, pairIndex) - || requiresBore( - start, - end, - heads[pairIndex], - heads[pairIndex + 1] - )) { - continue; - } - List raster = rasterLine(start, end); - Direction tangent = direction(start.x(), start.z(), end.x(), end.z()); - int bankDistance = surfaceBankDistance(widths[pairIndex]); - int maximumBankRise = path.containedSurface() - ? Integer.MAX_VALUE - : Math.addExact( - settings.surface().maximumIncision(), - (int) StrictMath.ceil(settings.surface().minimumTerrainBlendWidth() * 0.5D) - ); - for (int pointIndex = 0; pointIndex < raster.size(); pointIndex++) { - HydrologyPoint point = raster.get(pointIndex); - double progress = raster.size() < 2 ? 0D : pointIndex / (double) (raster.size() - 1); - int head = (int) StrictMath.round( - heads[pairIndex] + (heads[pairIndex + 1] - heads[pairIndex]) * progress - ); - HydrologyTerrainSample center = sampleLandBasis(point.x(), point.z()); - if (center == null) { - return false; - } - int permittedIncision = permittedSurfaceIncision(center); - int incision = center.naturalHeight() - head; - int minimumIncision = settings.surface().minimumSurfaceInset(); - if (incision < minimumIncision || incision > permittedIncision) { - return false; - } - boolean naturalTransition = center.slope() >= settings.hydraulics().waterfallMinimumDrop(); - boolean positiveBank = surfaceBankSupported( - point, - tangent, - bankDistance, - head, - maximumBankRise - ); - boolean negativeBank = surfaceBankSupported( - point, - tangent, - -bankDistance, - head, - maximumBankRise - ); - if (!naturalTransition && (!positiveBank || !negativeBank)) { - return false; - } - } - } - return true; - } - - private boolean surfaceMouthApproachPair(CoursePath path, int pairIndex) { - if (!path.reachesOutlet() || !path.outlet().directOcean()) { - return false; - } - double distance = 0D; - for (int index = pairIndex; index < path.points().size() - 1; index++) { - HydrologyPoint start = path.points().get(index); - HydrologyPoint end = path.points().get(index + 1); - distance += StrictMath.hypot(end.x() - start.x(), end.z() - start.z()); - if (distance > settings.outlets().mouthLevelingDistance()) { - return false; - } - } - return true; - } - private boolean surfaceBankSupported( HydrologyPoint point, Direction tangent, @@ -7314,66 +6182,6 @@ public final class HydrologyPlanner { heads[heads.length - 1] = outletHead; } - private void poolHeads( - CoursePath path, - int[] heads, - int[] maximumHeads, - int targetLength - ) { - if (heads.length < 3) { - return; - } - int[] raw = heads.clone(); - double distanceSinceDrop = 0D; - for (int index = 1; index < heads.length - 1; index++) { - HydrologyPoint upstream = path.points().get(index - 1); - HydrologyPoint current = path.points().get(index); - distanceSinceDrop += StrictMath.hypot(current.x() - upstream.x(), current.z() - upstream.z()); - int heldHead = heads[index - 1]; - if (raw[index] >= heldHead) { - heads[index] = heldHead; - continue; - } - if (maximumHeads != null && heldHead > maximumHeads[index]) { - heads[index] = raw[index]; - distanceSinceDrop = 0D; - continue; - } - int immediateDrop = raw[index - 1] - raw[index]; - if (immediateDrop >= settings.hydraulics().waterfallMinimumDrop()) { - heads[index] = raw[index]; - distanceSinceDrop = 0D; - continue; - } - if (distanceSinceDrop < targetLength) { - heads[index] = heldHead; - continue; - } - int transition = Math.min( - heldHead - raw[index], - settings.hydraulics().maximumGradualDrop() - ); - int transitionedHead = heldHead - Math.max(1, transition); - heads[index] = maximumHeads == null - ? transitionedHead - : Math.max(raw[index], Math.min(maximumHeads[index], transitionedHead)); - distanceSinceDrop = 0D; - } - } - - private int targetPoolLength(long courseId, String profileKey, CoursePath path) { - HydrologyPoint origin = path.points().getFirst(); - return sampleGeometry( - HydrologyGeometrySampler.Field.TARGET_POOL_LENGTH, - profileKey, - origin.x(), - origin.z(), - courseId, - settings.hydraulics().minimumTargetPoolLength(), - settings.hydraulics().maximumTargetPoolLength() - ); - } - private void appendOutletSegments( RiverCourseType courseType, long courseId, @@ -8940,7 +7748,6 @@ public final class HydrologyPlanner { List edges, RiverOutlet outlet, boolean reachesOutlet, - boolean containedSurface, boolean organicSurfaceRequired ) { } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverHydrology.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverHydrology.java index 1c53eeaa1..e0a05c8e0 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverHydrology.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverHydrology.java @@ -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.") diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java index 0a5932ec2..c748f7889 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java @@ -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 } } """;