From 85e12caf9bf324cbe3fd356affd0428c45950daa Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 8 Sep 2026 05:50:51 -0400 Subject: [PATCH] Split hydrology footprint and cave planners - Move segment rasterisation and channel shaping out of HydrologyFootprintCompiler into HydrologyFootprintRasterizer and HydrologyChannelGeometry - Move chamber, throat and pool construction out of HydrologyCaveContainmentPlanner into HydrologyCaveChamberPlanner - Promote the candidate span, candidate builder, stencil, column and cave observation types out of the three compilers to top level --- .../engine/hydrology/CaveCandidateBounds.java | 35 + .../hydrology/CaveCandidateBuilder.java | 189 +++ .../hydrology/CaveCandidateSpanBuilder.java | 360 +++++ .../engine/hydrology/CaveCourseGraph.java | 10 + .../iris/engine/hydrology/CaveSpanSet.java | 103 ++ .../hydrology/CaveSurfaceComposition.java | 18 + .../engine/hydrology/CaveSurfaceOpening.java | 74 + .../iris/engine/hydrology/CaveYSpan.java | 4 + .../hydrology/CrossTileResolutionContext.java | 6 +- .../iris/engine/hydrology/DraftProfile.java | 8 +- .../FootprintCenterlineProjection.java | 11 + .../hydrology/FootprintCourseRasterKey.java | 11 + .../engine/hydrology/FootprintFeatureKey.java | 11 + .../engine/hydrology/FootprintLayerShape.java | 19 + .../hydrology/FootprintMutableColumn.java | 173 ++ .../hydrology/FootprintRasterStencil.java | 11 + .../FootprintRasterStencilBuilder.java | 37 + .../hydrology/FootprintRasterStencilKey.java | 8 + .../FootprintValidationCourseRaster.java | 12 + .../hydrology/HydrologyCaveCourseFilter.java | 921 ++--------- .../hydrology/HydrologyChannelGeometry.java | 358 +++++ .../hydrology/HydrologyFootprintCompiler.java | 1395 ++--------------- .../HydrologyFootprintRasterizer.java | 565 +++++++ .../engine/hydrology/SourceSelection.java | 12 +- .../hydrology/cave/CaveBoundaryResult.java | 18 + .../hydrology/cave/CaveCachedValidation.java | 14 + .../engine/hydrology/cave/CaveClaimGroup.java | 9 + .../hydrology/cave/CaveComponentResult.java | 18 + .../hydrology/cave/CaveGrottoResult.java | 18 + .../cave/CaveIndependentDecision.java | 16 + .../engine/hydrology/cave/CavePathResult.java | 13 + .../cave/CaveSurfaceObservations.java | 40 + .../hydrology/cave/CaveValidationBatch.java | 11 + .../hydrology/cave/CaveViewObservations.java | 30 + .../cave/HydrologyCaveChamberPlanner.java | 809 ++++++++++ .../cave/HydrologyCaveContainmentPlanner.java | 1137 +------------- .../cave/RecordingCaveVoxelView.java | 96 ++ 37 files changed, 3370 insertions(+), 3210 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBounds.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBuilder.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateSpanBuilder.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveCourseGraph.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveSpanSet.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceComposition.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceOpening.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/CaveYSpan.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCenterlineProjection.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCourseRasterKey.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintFeatureKey.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintLayerShape.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintMutableColumn.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencil.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilBuilder.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilKey.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/FootprintValidationCourseRaster.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyChannelGeometry.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintRasterizer.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveBoundaryResult.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveCachedValidation.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveClaimGroup.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveComponentResult.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveGrottoResult.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveIndependentDecision.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CavePathResult.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveSurfaceObservations.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveValidationBatch.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveViewObservations.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveChamberPlanner.java create mode 100644 core/src/main/java/art/arcane/iris/engine/hydrology/cave/RecordingCaveVoxelView.java diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBounds.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBounds.java new file mode 100644 index 000000000..35013312c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBounds.java @@ -0,0 +1,35 @@ +package art.arcane.iris.engine.hydrology; + +import art.arcane.iris.engine.hydrology.cave.CavePosition; + +record CaveCandidateBounds( + int entryX, + int entryY, + int entryZ, + int waterHeadY, + int horizontalRadius, + int maximumDepth, + int dryHeadroom +) { + boolean containsColumn(int x, int z) { + long deltaX = (long) x - entryX; + long deltaZ = (long) z - entryZ; + long radiusSquared = (long) horizontalRadius * horizontalRadius; + return deltaX * deltaX + deltaZ * deltaZ <= radiusSquared; + } + + int minimumY() { + return Math.toIntExact((long) entryY - maximumDepth); + } + + int maximumY() { + return Math.max(entryY, Math.addExact(waterHeadY, dryHeadroom + 1)); + } + + boolean contains(CavePosition position) { + if (!containsColumn(position.x(), position.z())) { + return false; + } + return position.y() >= minimumY() && position.y() <= maximumY(); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBuilder.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBuilder.java new file mode 100644 index 000000000..deb7f24b9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateBuilder.java @@ -0,0 +1,189 @@ +package art.arcane.iris.engine.hydrology; + +import art.arcane.iris.engine.hydrology.cave.CavePosition; +import art.arcane.iris.engine.hydrology.cave.CavePositionIndex; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveFluidPolicy; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveGrottoShape; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveMode; +import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlannerSettings; +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveSource; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; + +final class CaveCandidateBuilder { + private final RiverCourse course; + private final HydraulicSegment representative; + private final LinkedHashMap actions; + private final LinkedHashSet openings; + private final CavePositionIndex openingIndex; + private final List surfaceOpenings; + private int minimumX; + private int maximumX; + private int minimumY; + private int maximumY; + private int minimumZ; + private int maximumZ; + + CaveCandidateBuilder(RiverCourse course, HydraulicSegment representative) { + this.course = course; + this.representative = representative; + this.actions = new LinkedHashMap<>(); + this.openings = new LinkedHashSet<>(); + this.openingIndex = new CavePositionIndex(); + this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course); + this.minimumX = Integer.MAX_VALUE; + this.maximumX = Integer.MIN_VALUE; + this.minimumY = Integer.MAX_VALUE; + this.maximumY = Integer.MIN_VALUE; + this.minimumZ = Integer.MAX_VALUE; + this.maximumZ = Integer.MIN_VALUE; + } + + private RiverCourse course() { + return course; + } + + private HydraulicSegment representative() { + return representative; + } + + boolean isEmpty() { + return actions.isEmpty(); + } + + void addAction(CavePosition position, HydrologyCaveAction action) { + HydrologyCaveAction existing = actions.get(position); + if (existing == null || actionPriority(action) < actionPriority(existing)) { + actions.put(position, action); + } + include(position); + } + + void addOpening(CavePosition position) { + if (openingIndex.add(position.x(), position.y(), position.z())) { + openings.add(position); + } + include(position); + } + + void addOpening(int x, int y, int z) { + if (openingIndex.add(x, y, z)) { + openings.add(new CavePosition(x, y, z)); + } + include(x, y, z); + } + + void addOpeningNeighborhood(CavePosition position) { + addOpening(position); + for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) { + addOpening( + position.x() + offset[0], + position.y() + offset[1], + position.z() + offset[2] + ); + } + } + + void addAdjacentSurfaceOpenings( + HydrologyFootprintCompiler.ValidationRaster validation + ) { + ArrayList actionPositions = new ArrayList<>(actions.keySet()); + for (CavePosition position : actionPositions) { + for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) { + int neighborX = position.x() + offset[0]; + int neighborZ = position.z() + offset[1]; + if (validation.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) { + addOpening(neighborX, position.y(), neighborZ); + } + } + } + } + + void addSurfaceOpening(HydrologyColumnLayer layer, CavePosition position) { + for (CaveSurfaceOpening opening : surfaceOpenings) { + if (!opening.matches(layer, position)) { + continue; + } + if (opening.includeNeighborhood()) { + addOpeningNeighborhood(position); + } else { + addOpening(position); + } + } + } + + private void include(CavePosition position) { + include(position.x(), position.y(), position.z()); + } + + private void include(int x, int y, int z) { + minimumX = Math.min(minimumX, x); + maximumX = Math.max(maximumX, x); + minimumY = Math.min(minimumY, y); + maximumY = Math.max(maximumY, y); + minimumZ = Math.min(minimumZ, z); + maximumZ = Math.max(maximumZ, z); + } + + HydrologyCaveCandidate build(HydrologyCaveCourseFilter.Options options) { + HydrologyPoint start = representative.start(); + int waterHead = representative.upstreamHeadY(); + int entryY = Math.max(waterHead, maximumY); + CavePosition entry = new CavePosition(start.x(), entryY, start.z()); + CavePosition target = new CavePosition(start.x(), waterHead, start.z()); + int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius( + entry.x(), + entry.z(), + minimumX, + maximumX, + minimumZ, + maximumZ + ); + int maximumDepth = Math.max(1, entryY - minimumY + 2); + int dryHeadroom = Math.max(0, maximumY - waterHead); + int volume = Math.max(1, actions.size()); + HydrologyCavePlannerSettings settings = new HydrologyCavePlannerSettings( + horizontalRadius, + maximumDepth, + volume, + 1, + 1, + 1, + 1, + dryHeadroom, + HydrologyCaveFluidPolicy.REJECT_EXISTING, + HydrologyCaveGrottoShape.ELLIPSOID, + horizontalRadius, + maximumDepth + ); + return new HydrologyCaveCandidate( + new HydrologyCaveSource( + course.id(), + entry, + target, + waterHead, + HydrologyCaveMode.GENERATED_GROTTO + ), + course.profileKey(), + settings, + options.connectToExistingCaves() && course.type() == RiverCourseType.UNDERGROUND, + actions, + openings + ); + } + + private static int actionPriority(HydrologyCaveAction action) { + return switch (action) { + case WET_SOURCE -> 0; + case FALLING_FLUID -> 1; + case DRY_AIR -> 2; + case SEAL_GUARD -> 3; + }; + } + +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateSpanBuilder.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateSpanBuilder.java new file mode 100644 index 000000000..e61f84ca6 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCandidateSpanBuilder.java @@ -0,0 +1,360 @@ +package art.arcane.iris.engine.hydrology; + +import art.arcane.iris.engine.hydrology.cave.CavePosition; +import art.arcane.iris.engine.hydrology.cave.CaveVoxelView; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class CaveCandidateSpanBuilder { + private final RiverCourse course; + private final HydraulicSegment representative; + private final LinkedHashMap actions; + private final LinkedHashMap openings; + private final LinkedHashMap coastalGrottoActions; + private final LinkedHashMap inlandGrottoActions; + private final List surfaceOpenings; + private int minimumX; + private int maximumX; + private int minimumY; + private int maximumY; + private int minimumZ; + private int maximumZ; + + CaveCandidateSpanBuilder(RiverCourse course, HydraulicSegment representative) { + this.course = course; + this.representative = representative; + this.actions = new LinkedHashMap<>(); + this.openings = new LinkedHashMap<>(); + this.coastalGrottoActions = new LinkedHashMap<>(); + this.inlandGrottoActions = new LinkedHashMap<>(); + this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course); + this.minimumX = Integer.MAX_VALUE; + this.maximumX = Integer.MIN_VALUE; + this.minimumY = Integer.MAX_VALUE; + this.maximumY = Integer.MIN_VALUE; + this.minimumZ = Integer.MAX_VALUE; + this.maximumZ = Integer.MIN_VALUE; + } + + RiverCourse course() { + return course; + } + + HydraulicSegment representative() { + return representative; + } + + boolean isEmpty() { + return actions.isEmpty(); + } + + boolean allowsIntentionalSurfaceExposure() { + return course.surfaceSinkholeContinuation(); + } + + void addAction(int x, int z, HydrologyColumnLayer layer) { + int minimumActionY = layer.bedY() + 1; + int maximumActionY = layer.ceilingY(); + if (minimumActionY > maximumActionY) { + return; + } + addSpan(actions, x, z, minimumActionY, maximumActionY); + if (layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO) { + addSpan(coastalGrottoActions, x, z, minimumActionY, maximumActionY); + } else if (layer.feature().type() == HydrologyFeatureType.INLAND_GROTTO) { + addSpan(inlandGrottoActions, x, z, minimumActionY, maximumActionY); + } + include(x, minimumActionY, z); + include(x, maximumActionY, z); + for (CaveSurfaceOpening opening : surfaceOpenings) { + if (!opening.matchesColumn(layer, x, z)) { + continue; + } + int minimumOpeningY = Math.max(minimumActionY, opening.minimumY()); + if (minimumOpeningY > maximumActionY) { + continue; + } + if (!opening.includeNeighborhood()) { + addOpening(x, z, minimumOpeningY, maximumActionY); + continue; + } + addOpening(x, z, minimumOpeningY - 1, maximumActionY + 1); + for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) { + addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumActionY); + } + } + } + + void addOceanOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) { + if (minimumOpeningY <= maximumOpeningY) { + addOpening(x, z, minimumOpeningY, maximumOpeningY); + } + } + + void addOpeningNeighborhood( + int x, + int z, + int minimumOpeningY, + int maximumOpeningY + ) { + if (minimumOpeningY > maximumOpeningY) { + return; + } + addOpening(x, z, minimumOpeningY - 1, maximumOpeningY + 1); + for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) { + addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumOpeningY); + } + } + + void addAdjacentSurfaceOpenings( + HydrologyFootprintCompiler.ValidationRaster validation + ) { + for (Map.Entry entry : actions.entrySet()) { + int x = RiverFootprint.unpackX(entry.getKey()); + int z = RiverFootprint.unpackZ(entry.getKey()); + for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) { + int neighborX = x + offset[0]; + int neighborZ = z + offset[1]; + if (!validation.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) { + continue; + } + for (CaveYSpan span : entry.getValue().spans()) { + addOpening(neighborX, neighborZ, span.minimumY(), span.maximumY()); + } + } + } + } + + HydrologyFeatureType oversizedGrotto(HydrologyCaveCourseFilter.Options options) { + if (positionCount(coastalGrottoActions) + > options.maximumVolume(HydrologyFeatureType.COASTAL_GROTTO)) { + return HydrologyFeatureType.COASTAL_GROTTO; + } + if (positionCount(inlandGrottoActions) + > options.maximumVolume(HydrologyFeatureType.INLAND_GROTTO)) { + return HydrologyFeatureType.INLAND_GROTTO; + } + return null; + } + + long positionCount() { + return positionCount(actions); + } + + boolean exposed(CaveVoxelView view) { + CaveCandidateBounds bounds = bounds(); + for (Map.Entry entry : actions.entrySet()) { + int x = RiverFootprint.unpackX(entry.getKey()); + int z = RiverFootprint.unpackZ(entry.getKey()); + CaveSpanSet actionSpans = entry.getValue(); + CaveSpanSet openingSpans = openings.get(entry.getKey()); + if (exposedDifference(actionSpans, null, openingSpans, x, z, bounds, view)) { + return true; + } + for (CaveYSpan span : actionSpans.spans()) { + int lowerBoundary = span.minimumY() - 1; + if (!actionSpans.contains(lowerBoundary) + && !contains(openingSpans, lowerBoundary) + && exposed(x, lowerBoundary, z, bounds, view)) { + return true; + } + int upperBoundary = span.maximumY() + 1; + if (!actionSpans.contains(upperBoundary) + && !contains(openingSpans, upperBoundary) + && exposed(x, upperBoundary, z, bounds, view)) { + return true; + } + } + for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) { + int neighborX = x + offset[0]; + int neighborZ = z + offset[1]; + long neighborKey = RiverFootprint.pack(neighborX, neighborZ); + if (exposedDifference( + actionSpans, + actions.get(neighborKey), + openings.get(neighborKey), + neighborX, + neighborZ, + bounds, + view + )) { + return true; + } + } + } + return false; + } + + private boolean exposedDifference( + CaveSpanSet source, + CaveSpanSet excludedActions, + CaveSpanSet excludedOpenings, + int x, + int z, + CaveCandidateBounds bounds, + CaveVoxelView view + ) { + for (CaveYSpan span : source.spans()) { + if (exposedDifference( + span, + excludedActions, + excludedOpenings, + x, + z, + bounds, + view + )) { + return true; + } + } + return false; + } + + private boolean exposedDifference( + CaveYSpan source, + CaveSpanSet excludedActions, + CaveSpanSet excludedOpenings, + int x, + int z, + CaveCandidateBounds bounds, + CaveVoxelView view + ) { + List actionSpans = excludedActions == null ? List.of() : excludedActions.spans(); + List openingSpans = excludedOpenings == null ? List.of() : excludedOpenings.spans(); + int actionIndex = 0; + int openingIndex = 0; + long cursor = source.minimumY(); + while (cursor <= source.maximumY()) { + while (actionIndex < actionSpans.size() + && actionSpans.get(actionIndex).maximumY() < cursor) { + actionIndex++; + } + while (openingIndex < openingSpans.size() + && openingSpans.get(openingIndex).maximumY() < cursor) { + openingIndex++; + } + CaveYSpan action = actionIndex < actionSpans.size() ? actionSpans.get(actionIndex) : null; + CaveYSpan opening = openingIndex < openingSpans.size() ? openingSpans.get(openingIndex) : null; + CaveYSpan excluded = first(action, opening); + if (excluded == null || excluded.minimumY() > source.maximumY()) { + return exposed(x, (int) cursor, source.maximumY(), z, bounds, view); + } + if (cursor < excluded.minimumY() + && exposed(x, (int) cursor, excluded.minimumY() - 1, z, bounds, view)) { + return true; + } + cursor = Math.max(cursor, (long) excluded.maximumY() + 1L); + if (excluded == action) { + actionIndex++; + } else { + openingIndex++; + } + } + return false; + } + + private CaveYSpan first(CaveYSpan first, CaveYSpan second) { + if (first == null) { + return second; + } + if (second == null) { + return first; + } + return first.minimumY() <= second.minimumY() ? first : second; + } + + boolean exposed( + int x, + int minimumY, + int maximumY, + int z, + CaveCandidateBounds bounds, + CaveVoxelView view + ) { + if (!bounds.containsColumn(x, z)) { + return false; + } + int boundedMinimumY = Math.max(minimumY, bounds.minimumY()); + int boundedMaximumY = Math.min(maximumY, bounds.maximumY()); + return boundedMinimumY <= boundedMaximumY + && view.hasAboveTerrainSurface(x, z, boundedMinimumY, boundedMaximumY); + } + + boolean exposed( + int x, + int y, + int z, + CaveCandidateBounds bounds, + CaveVoxelView view + ) { + CavePosition position = new CavePosition(x, y, z); + return bounds.contains(position) + && view.isInWorld(position) + && view.isAboveTerrainSurface(position); + } + + private CaveCandidateBounds bounds() { + HydrologyPoint start = representative.start(); + int waterHead = representative.upstreamHeadY(); + int entryY = Math.max(waterHead, maximumY); + int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius( + start.x(), + start.z(), + minimumX, + maximumX, + minimumZ, + maximumZ + ); + int maximumDepth = Math.max(1, entryY - minimumY + 2); + int dryHeadroom = Math.max(0, maximumY - waterHead); + return new CaveCandidateBounds( + start.x(), + entryY, + start.z(), + waterHead, + horizontalRadius, + maximumDepth, + dryHeadroom + ); + } + + private void addOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) { + addSpan(openings, x, z, minimumOpeningY, maximumOpeningY); + include(x, minimumOpeningY, z); + include(x, maximumOpeningY, z); + } + + private void include(int x, int y, int z) { + minimumX = Math.min(minimumX, x); + maximumX = Math.max(maximumX, x); + minimumY = Math.min(minimumY, y); + maximumY = Math.max(maximumY, y); + minimumZ = Math.min(minimumZ, z); + maximumZ = Math.max(maximumZ, z); + } + + private static void addSpan( + Map spans, + int x, + int z, + int minimumY, + int maximumY + ) { + spans.computeIfAbsent(RiverFootprint.pack(x, z), (Long ignored) -> new CaveSpanSet()) + .add(minimumY, maximumY); + } + + private static boolean contains(CaveSpanSet spans, int y) { + return spans != null && spans.contains(y); + } + + static long positionCount(Map spans) { + long count = 0L; + for (CaveSpanSet spanSet : spans.values()) { + count += spanSet.positionCount(); + } + return count; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCourseGraph.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCourseGraph.java new file mode 100644 index 000000000..db38ed40c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveCourseGraph.java @@ -0,0 +1,10 @@ +package art.arcane.iris.engine.hydrology; + +import java.util.List; + +record CaveCourseGraph( + List nodes, + List edges, + List outlets +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSpanSet.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSpanSet.java new file mode 100644 index 000000000..209cad45a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSpanSet.java @@ -0,0 +1,103 @@ +package art.arcane.iris.engine.hydrology; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +final class CaveSpanSet { + private int[] pending; + private int size; + private List normalized; + + CaveSpanSet() { + this.pending = new int[4]; + this.normalized = null; + } + + void add(int minimumY, int maximumY) { + if (minimumY > maximumY) { + return; + } + int requiredLength = Math.multiplyExact(size + 1, 2); + if (requiredLength > pending.length) { + pending = Arrays.copyOf(pending, Math.multiplyExact(pending.length, 2)); + } + pending[size * 2] = minimumY; + pending[size * 2 + 1] = maximumY; + size++; + normalized = null; + } + + List spans() { + if (normalized != null) { + return normalized; + } + sortPending(); + ArrayList merged = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + CaveYSpan span = new CaveYSpan(pending[index * 2], pending[index * 2 + 1]); + if (merged.isEmpty()) { + merged.add(span); + continue; + } + CaveYSpan previous = merged.getLast(); + if ((long) span.minimumY() > (long) previous.maximumY() + 1L) { + merged.add(span); + continue; + } + merged.set( + merged.size() - 1, + new CaveYSpan(previous.minimumY(), Math.max(previous.maximumY(), span.maximumY())) + ); + } + normalized = List.copyOf(merged); + return normalized; + } + + private void sortPending() { + for (int index = 1; index < size; index++) { + int minimumY = pending[index * 2]; + int maximumY = pending[index * 2 + 1]; + int insertionIndex = index; + while (insertionIndex > 0) { + int previousMinimumY = pending[(insertionIndex - 1) * 2]; + int previousMaximumY = pending[(insertionIndex - 1) * 2 + 1]; + if (previousMinimumY < minimumY + || previousMinimumY == minimumY && previousMaximumY <= maximumY) { + break; + } + pending[insertionIndex * 2] = previousMinimumY; + pending[insertionIndex * 2 + 1] = previousMaximumY; + insertionIndex--; + } + pending[insertionIndex * 2] = minimumY; + pending[insertionIndex * 2 + 1] = maximumY; + } + } + + boolean contains(int y) { + List spans = spans(); + int minimumIndex = 0; + int maximumIndex = spans.size() - 1; + while (minimumIndex <= maximumIndex) { + int index = (minimumIndex + maximumIndex) >>> 1; + CaveYSpan span = spans.get(index); + if (y < span.minimumY()) { + maximumIndex = index - 1; + } else if (y > span.maximumY()) { + minimumIndex = index + 1; + } else { + return true; + } + } + return false; + } + + long positionCount() { + long count = 0L; + for (CaveYSpan span : spans()) { + count += (long) span.maximumY() - span.minimumY() + 1L; + } + return count; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceComposition.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceComposition.java new file mode 100644 index 000000000..185ba356b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceComposition.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.hydrology; + +import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate; + +import java.util.Objects; + +record CaveSurfaceComposition( + HydrologyCaveCandidate candidate, + boolean accepted +) { + static CaveSurfaceComposition accepted(HydrologyCaveCandidate candidate) { + return new CaveSurfaceComposition(Objects.requireNonNull(candidate), true); + } + + static CaveSurfaceComposition rejected() { + return new CaveSurfaceComposition(null, false); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceOpening.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceOpening.java new file mode 100644 index 000000000..04fc9ca95 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveSurfaceOpening.java @@ -0,0 +1,74 @@ +package art.arcane.iris.engine.hydrology; + +import art.arcane.iris.engine.hydrology.cave.CavePosition; + +import java.util.List; + +record CaveSurfaceOpening( + long segmentId, + HydrologyPoint point, + double interiorX, + double interiorZ, + long radiusSquared, + int minimumY, + boolean includeNeighborhood +) { + static CaveSurfaceOpening create( + HydraulicSegment segment, + boolean start, + long radiusSquared, + int minimumY, + boolean includeNeighborhood + ) { + List centerline = segment.centerline(); + int boundaryIndex = start ? 0 : centerline.size() - 1; + int interiorIndex = start + ? Math.min(1, centerline.size() - 1) + : Math.max(0, centerline.size() - 2); + HydrologyPoint point = centerline.get(boundaryIndex); + HydrologyPoint interior = centerline.get(interiorIndex); + double deltaX = interior.x() - point.x(); + double deltaZ = interior.z() - point.z(); + double lengthSquared = deltaX * deltaX + deltaZ * deltaZ; + double scale = 0D; + if (includeNeighborhood) { + scale = lengthSquared <= radiusSquared + ? 1D + : StrictMath.sqrt(radiusSquared / lengthSquared); + } + return new CaveSurfaceOpening( + segment.id(), + point, + point.x() + deltaX * scale, + point.z() + deltaZ * scale, + radiusSquared, + minimumY, + includeNeighborhood + ); + } + + boolean matches(HydrologyColumnLayer layer, CavePosition position) { + return matchesColumn(layer, position.x(), position.z()) + && position.y() >= minimumY; + } + + boolean matchesColumn(HydrologyColumnLayer layer, int x, int z) { + return (includeNeighborhood || layer.feature().segmentId() == segmentId) + && distanceSquared(x, z) <= radiusSquared; + } + + private double distanceSquared(int x, int z) { + double pathX = interiorX - point.x(); + double pathZ = interiorZ - point.z(); + double pathLengthSquared = pathX * pathX + pathZ * pathZ; + double positionX = x - point.x(); + double positionZ = z - point.z(); + double progress = pathLengthSquared == 0D + ? 0D + : (positionX * pathX + positionZ * pathZ) / pathLengthSquared; + double clampedProgress = Math.max(0D, Math.min(1D, progress)); + double deltaX = positionX - pathX * clampedProgress; + double deltaZ = positionZ - pathZ * clampedProgress; + return deltaX * deltaX + deltaZ * deltaZ; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CaveYSpan.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveYSpan.java new file mode 100644 index 000000000..51d05a86b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CaveYSpan.java @@ -0,0 +1,4 @@ +package art.arcane.iris.engine.hydrology; + +record CaveYSpan(int minimumY, int maximumY) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/CrossTileResolutionContext.java b/core/src/main/java/art/arcane/iris/engine/hydrology/CrossTileResolutionContext.java index 079fc56f5..c1f167680 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/CrossTileResolutionContext.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/CrossTileResolutionContext.java @@ -8,10 +8,10 @@ import java.util.Set; final class CrossTileResolutionContext { final Map resolved; - final Set resolving; + private final Set resolving; final HydrologyTileKey root; - final long maximumTileOffset; - final int maximumOwners; + private final long maximumTileOffset; + private final int maximumOwners; int iterations; CrossTileResolutionContext( diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/DraftProfile.java b/core/src/main/java/art/arcane/iris/engine/hydrology/DraftProfile.java index adb15612e..9c1ab3d53 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/DraftProfile.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/DraftProfile.java @@ -7,10 +7,10 @@ import art.arcane.iris.spi.IrisLogging; * selection, settling, publication passes, and the time spent waiting on lower-rank neighbour drafts. */ final class DraftProfile { - final long[] phaseNanos = new long[DraftPhase.values().length]; - long dependencyNanos; - int dependencies; - int admissions; + private final long[] phaseNanos = new long[DraftPhase.values().length]; + private long dependencyNanos; + private int dependencies; + private int admissions; int earlyOwners; int routeCalls; int routeSolves; diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCenterlineProjection.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCenterlineProjection.java new file mode 100644 index 000000000..9c825afa0 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCenterlineProjection.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.hydrology; + +record FootprintCenterlineProjection( + int pointIndex, + double x, + double z, + double distance, + int flowX, + int flowZ +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCourseRasterKey.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCourseRasterKey.java new file mode 100644 index 000000000..7c64d0830 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintCourseRasterKey.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.hydrology; + +import java.util.List; + +record FootprintCourseRasterKey( + long courseId, + RiverCourseType courseType, + String profileKey, + List segments +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintFeatureKey.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintFeatureKey.java new file mode 100644 index 000000000..0e4742b1b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintFeatureKey.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.hydrology; + +record FootprintFeatureKey( + long courseId, + long segmentId, + HydrologyFeatureType type, + int role, + int flowX, + int flowZ +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintLayerShape.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintLayerShape.java new file mode 100644 index 000000000..86cf44cdd --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintLayerShape.java @@ -0,0 +1,19 @@ +package art.arcane.iris.engine.hydrology; + +record FootprintLayerShape( + int channelRadius, + double shoreWidth, + double gradingWidth, + int bed, + int fluidHead, + int ceiling, + boolean ellipsoid, + boolean archedChannel, + boolean roundedSurfaceBed, + boolean organicBoundary, + boolean fallingThroat +) { + double totalRadius() { + return channelRadius + shoreWidth + gradingWidth; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintMutableColumn.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintMutableColumn.java new file mode 100644 index 000000000..a8563b9d5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintMutableColumn.java @@ -0,0 +1,173 @@ +package art.arcane.iris.engine.hydrology; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; + +final class FootprintMutableColumn { + private final int x; + private final int z; + private final int naturalHeight; + private final int seaLevel; + private final boolean ocean; + private final String parentBiomeKey; + private HydrologyColumnLayer singleLayer; + private LinkedHashMap layers; + + FootprintMutableColumn(int x, int z, HydrologyTerrainSample terrain, int seaLevel) { + this.x = x; + this.z = z; + this.naturalHeight = terrain.naturalHeight(); + this.seaLevel = seaLevel; + this.ocean = terrain.ocean(); + this.parentBiomeKey = terrain.parentBiomeKey(); + } + + FootprintMutableColumn(HydrologyColumnSample sample) { + this.x = sample.x(); + this.z = sample.z(); + this.naturalHeight = sample.naturalHeight(); + this.seaLevel = sample.seaLevel(); + this.ocean = sample.ocean(); + this.parentBiomeKey = sample.parentBiomeKey(); + for (HydrologyColumnLayer layer : sample.layers()) { + add(layer); + } + } + + void add(HydrologyColumnLayer layer) { + if (layers == null) { + if (singleLayer == null) { + singleLayer = layer; + return; + } + if (singleLayer.feature().id() == layer.feature().id()) { + singleLayer = merge(singleLayer, layer); + return; + } + layers = new LinkedHashMap<>(); + layers.put(singleLayer.feature().id(), singleLayer); + singleLayer = null; + } + HydrologyColumnLayer existing = layers.get(layer.feature().id()); + layers.put(layer.feature().id(), existing == null ? layer : merge(existing, layer)); + } + + HydrologyColumnSample build() { + List builtLayers; + if (layers != null) { + builtLayers = resolveSurfaceHeadConflicts(new ArrayList<>(layers.values())); + } else if (singleLayer != null) { + builtLayers = List.of(singleLayer); + } else { + builtLayers = List.of(); + } + return new HydrologyColumnSample( + x, + z, + naturalHeight, + seaLevel, + ocean, + parentBiomeKey, + builtLayers + ); + } + + private List resolveSurfaceHeadConflicts(List candidates) { + HashMap selectedByCourse = new HashMap<>(); + for (HydrologyColumnLayer candidate : candidates) { + if (!conflictingSurfaceCandidate(candidate)) { + continue; + } + long courseId = candidate.feature().courseId(); + HydrologyColumnLayer selected = selectedByCourse.get(courseId); + if (selected == null || prefersSurfaceLayer(candidate, selected)) { + selectedByCourse.put(courseId, candidate); + } + } + if (selectedByCourse.isEmpty()) { + return candidates; + } + ArrayList resolved = new ArrayList<>(candidates.size()); + for (HydrologyColumnLayer candidate : candidates) { + HydrologyColumnLayer selected = selectedByCourse.get(candidate.feature().courseId()); + if (!conflictingSurfaceCandidate(candidate) + || selected == null + || candidate.fluidHeadY() == selected.fluidHeadY()) { + resolved.add(candidate); + } + } + return List.copyOf(resolved); + } + + private boolean conflictingSurfaceCandidate(HydrologyColumnLayer layer) { + return layer.feature().type().isSurface() + && layer.channel() + && layer.connectedFluid() + && layer.fluidOwned() + && !layer.fallingFluid(); + } + + private boolean prefersSurfaceLayer(HydrologyColumnLayer candidate, HydrologyColumnLayer selected) { + if (candidate.receivingPool() != selected.receivingPool()) { + return candidate.receivingPool(); + } + if (candidate.fluidHeadY() != selected.fluidHeadY()) { + return candidate.fluidHeadY() > selected.fluidHeadY(); + } + return candidate.feature().id() < selected.feature().id(); + } + + void merge(HydrologyColumnSample sample) { + if (sample.x() != x + || sample.z() != z + || sample.naturalHeight() != naturalHeight + || sample.seaLevel() != seaLevel + || sample.ocean() != ocean + || !sample.parentBiomeKey().equals(parentBiomeKey)) { + throw new IllegalStateException("Hydrology course footprints disagree on terrain metadata at " + + x + "," + z + "."); + } + for (HydrologyColumnLayer layer : sample.layers()) { + add(layer); + } + } + + private HydrologyColumnLayer merge(HydrologyColumnLayer first, HydrologyColumnLayer second) { + if (first.equals(second)) { + return first; + } + HydrologyFeatureRef feature = first.feature().y() <= second.feature().y() + ? first.feature() + : second.feature(); + boolean drySurfaceBlend = first.feature().type().isSurface() + && second.feature().type().isSurface() + && !first.channel() + && !second.channel(); + int bedY = drySurfaceBlend + ? Math.max(first.bedY(), second.bedY()) + : Math.min(first.bedY(), second.bedY()); + return new HydrologyColumnLayer( + feature, + bedY, + Math.max(first.fluidHeadY(), second.fluidHeadY()), + Math.max(first.ceilingY(), second.ceilingY()), + first.channel() || second.channel(), + first.shore() || second.shore(), + first.grading() || second.grading(), + first.connectedFluid() || second.connectedFluid(), + first.fallingFluid() || second.fallingFluid(), + first.receivingPool() || second.receivingPool(), + first.terrainOwned() || second.terrainOwned(), + first.fluidOwned() || second.fluidOwned(), + first.oceanApron() || second.oceanApron(), + first.profileKey(), + first.surfaceBiomeKey(), + first.mouthBiomeKey(), + first.shoreBiomeKey(), + first.bankBiomeKey(), + first.floodedCaveBiomeKey() + ); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencil.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencil.java new file mode 100644 index 000000000..b8f02130b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencil.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.hydrology; + +record FootprintRasterStencil( + int[] deltaXs, + int[] deltaZs, + double[] distances +) { + int size() { + return deltaXs.length; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilBuilder.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilBuilder.java new file mode 100644 index 000000000..fde7bdf4b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilBuilder.java @@ -0,0 +1,37 @@ +package art.arcane.iris.engine.hydrology; + +import java.util.Arrays; + +final class FootprintRasterStencilBuilder { + private int[] deltaXs; + private int[] deltaZs; + private double[] distances; + private int size; + + FootprintRasterStencilBuilder() { + this.deltaXs = new int[64]; + this.deltaZs = new int[64]; + this.distances = new double[64]; + } + + 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); + } + deltaXs[size] = deltaX; + deltaZs[size] = deltaZ; + distances[size] = distance; + size++; + } + + FootprintRasterStencil build() { + return new FootprintRasterStencil( + Arrays.copyOf(deltaXs, size), + Arrays.copyOf(deltaZs, size), + Arrays.copyOf(distances, size) + ); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilKey.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilKey.java new file mode 100644 index 000000000..a1c00ce49 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintRasterStencilKey.java @@ -0,0 +1,8 @@ +package art.arcane.iris.engine.hydrology; + +record FootprintRasterStencilKey( + int channelRadius, + double shoreWidth, + double gradingWidth +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintValidationCourseRaster.java b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintValidationCourseRaster.java new file mode 100644 index 000000000..f16229bd4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/FootprintValidationCourseRaster.java @@ -0,0 +1,12 @@ +package art.arcane.iris.engine.hydrology; + +import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint; + +import java.util.List; + +record FootprintValidationCourseRaster( + long courseId, + List columns, + SurfaceFootprint surface +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCaveCourseFilter.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCaveCourseFilter.java index 0e2802747..26af0e5d1 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCaveCourseFilter.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCaveCourseFilter.java @@ -1,24 +1,17 @@ package art.arcane.iris.engine.hydrology; import art.arcane.iris.engine.hydrology.cave.CavePosition; -import art.arcane.iris.engine.hydrology.cave.CavePositionIndex; import art.arcane.iris.engine.hydrology.cave.CaveVoxel; import art.arcane.iris.engine.hydrology.cave.CaveVoxelView; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveContainmentPlanner; -import art.arcane.iris.engine.hydrology.cave.HydrologyCaveFluidPolicy; -import art.arcane.iris.engine.hydrology.cave.HydrologyCaveGrottoShape; -import art.arcane.iris.engine.hydrology.cave.HydrologyCaveMode; import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan; import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlannerSettings; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveRejection; -import art.arcane.iris.engine.hydrology.cave.HydrologyCaveSource; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; @@ -31,16 +24,23 @@ import java.util.Objects; import java.util.Set; final class HydrologyCaveCourseFilter { - private static final long DIAGNOSTIC_SALT = 0x43415645464c5452L; - private static final int[][] NEIGHBORS = { + static final long DIAGNOSTIC_SALT = 0x43415645464c5452L; + + static final int[][] NEIGHBORS = { {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1} - }; - private static final int[][] HORIZONTAL_NEIGHBORS = { + } + +; + + static final int[][] HORIZONTAL_NEIGHBORS = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} - }; - private static final CaveVoxelView GENERATED_CHANNEL_VIEW = new CaveVoxelView() { + } + +; + + static final CaveVoxelView GENERATED_CHANNEL_VIEW = new CaveVoxelView() { @Override public boolean isInWorld(CavePosition position) { return true; @@ -60,14 +60,16 @@ final class HydrologyCaveCourseFilter { public boolean isAboveTerrainSurface(CavePosition position) { return false; } - }; + } - private final HydrologyCaveContainmentPlanner planner; - private final CaveVoxelView view; - private final Options options; - private final Map candidateCache; - private final HydrologyCaveContainmentPlanner.ValidationCache validationCache; - private final HydrologyObservedPlannedSurface plannedSurface; +; + + final HydrologyCaveContainmentPlanner planner; + final CaveVoxelView view; + final Options options; + final Map candidateCache; + final HydrologyCaveContainmentPlanner.ValidationCache validationCache; + final HydrologyObservedPlannedSurface plannedSurface; HydrologyCaveCourseFilter(CaveVoxelView view, Options options) { this(view, options, null, null, null); @@ -119,7 +121,7 @@ final class HydrologyCaveCourseFilter { return filter(nodes, edges, outlets, courses, null, validation, diagnostics); } - private Result filter( + Result filter( List nodes, List edges, List outlets, @@ -162,11 +164,11 @@ final class HydrologyCaveCourseFilter { } } uncachedCourses.removeIf((RiverCourse course) -> preflightRejectedCourseIds.contains(course.id())); - LinkedHashMap builders = validation == null + LinkedHashMap builders = validation == null ? candidateBuilders(uncachedCourses, columns) : candidateBuilders(uncachedCourses, validation); if (candidateCourses.isEmpty() && preflightRejectedCourseIds.isEmpty()) { - Graph graph = acceptedGraph(nodes, edges, outlets, normalizedCourses); + CaveCourseGraph graph = acceptedGraph(nodes, edges, outlets, normalizedCourses); return new Result( graph.nodes(), graph.edges(), @@ -185,7 +187,7 @@ final class HydrologyCaveCourseFilter { CandidateKey key = candidateKeys.get(course.id()); HydrologyCaveCandidate candidate = cachedCandidates.get(course.id()); if (candidate == null) { - CandidateBuilder builder = builders.get(course.id()); + CaveCandidateBuilder builder = builders.get(course.id()); if (builder == null) { throw new IllegalStateException("Hydrology cave candidate raster was empty."); } @@ -195,8 +197,8 @@ final class HydrologyCaveCourseFilter { } exposureValidatedCandidates.add(candidate); } - SurfaceComposition composition = validation == null - ? SurfaceComposition.accepted(candidate) + CaveSurfaceComposition composition = validation == null + ? CaveSurfaceComposition.accepted(candidate) : composeSurfacePublication(candidate, course, validation, publishedCoursesById); if (!composition.accepted()) { if (rejectedCourseIds.add(course.id())) { @@ -242,7 +244,7 @@ final class HydrologyCaveCourseFilter { } } if (rejectedCourseIds.isEmpty()) { - Graph graph = acceptedGraph(nodes, edges, outlets, normalizedCourses); + CaveCourseGraph graph = acceptedGraph(nodes, edges, outlets, normalizedCourses); return new Result( graph.nodes(), graph.edges(), @@ -258,7 +260,7 @@ final class HydrologyCaveCourseFilter { acceptedCourses.add(course); } } - Graph graph = acceptedGraph(nodes, edges, outlets, acceptedCourses); + CaveCourseGraph graph = acceptedGraph(nodes, edges, outlets, acceptedCourses); return new Result( graph.nodes(), graph.edges(), @@ -268,7 +270,7 @@ final class HydrologyCaveCourseFilter { ); } - private boolean compatibleSharedCandidates( + boolean compatibleSharedCandidates( HydrologyCaveCandidate first, HydrologyCaveCandidate second, Map coursesById @@ -282,7 +284,7 @@ final class HydrologyCaveCourseFilter { || sharesTerminalGrotto(firstCourse, secondCourse, first); } - private List alignSharedTerminalPlanActions( + List alignSharedTerminalPlanActions( List plans, List candidates, Map coursesById @@ -330,7 +332,7 @@ final class HydrologyCaveCourseFilter { return List.copyOf(aligned); } - private HydrologyCavePlan withActions( + HydrologyCavePlan withActions( HydrologyCavePlan plan, Map actions ) { @@ -343,7 +345,7 @@ final class HydrologyCaveCourseFilter { ); } - private void alignSharedTerminalCandidateActions( + void alignSharedTerminalCandidateActions( List candidates, Map coursesById, Set exposureValidatedCandidates @@ -386,7 +388,7 @@ final class HydrologyCaveCourseFilter { } } - private HydrologyCaveAction sharedTerminalAction( + HydrologyCaveAction sharedTerminalAction( HydrologyCaveAction first, HydrologyCaveAction second ) { @@ -395,7 +397,7 @@ final class HydrologyCaveCourseFilter { : second; } - private int sharedTerminalActionPriority(HydrologyCaveAction action) { + int sharedTerminalActionPriority(HydrologyCaveAction action) { return switch (action) { case FALLING_FLUID -> 0; case WET_SOURCE -> 1; @@ -404,7 +406,7 @@ final class HydrologyCaveCourseFilter { }; } - private HydrologyCaveCandidate withActions( + HydrologyCaveCandidate withActions( HydrologyCaveCandidate candidate, Map actions ) { @@ -418,7 +420,7 @@ final class HydrologyCaveCourseFilter { ); } - private void replaceExposureCandidate( + void replaceExposureCandidate( Set exposureValidatedCandidates, HydrologyCaveCandidate existing, HydrologyCaveCandidate replacement @@ -428,7 +430,7 @@ final class HydrologyCaveCourseFilter { } } - private SurfaceComposition composeSurfacePublication( + CaveSurfaceComposition composeSurfacePublication( HydrologyCaveCandidate candidate, RiverCourse course, HydrologyFootprintCompiler.ValidationRaster validation, @@ -476,7 +478,7 @@ final class HydrologyCaveCourseFilter { || (surfaceCourseId != course.id() && !candidate.intentionalOpenings().contains(position) && !sharedTerminalGrotto)) { - return SurfaceComposition.rejected(); + return CaveSurfaceComposition.rejected(); } if (caveAction == surfaceCell.action()) { continue; @@ -484,7 +486,7 @@ final class HydrologyCaveCourseFilter { if (surfaceCourseId != course.id() && !sharesDrainageOutlet(course, surfaceCourse) && !sharedTerminalGrotto) { - return SurfaceComposition.rejected(); + return CaveSurfaceComposition.rejected(); } if (composedActions == null) { composedActions = new LinkedHashMap<>(candidate.actions()); @@ -493,9 +495,9 @@ final class HydrologyCaveCourseFilter { } } if (composedActions == null) { - return SurfaceComposition.accepted(candidate); + return CaveSurfaceComposition.accepted(candidate); } - return SurfaceComposition.accepted(new HydrologyCaveCandidate( + return CaveSurfaceComposition.accepted(new HydrologyCaveCandidate( candidate.source(), candidate.profileKey(), candidate.settings(), @@ -505,7 +507,7 @@ final class HydrologyCaveCourseFilter { )); } - private boolean sharesTerminalGrotto( + boolean sharesTerminalGrotto( RiverCourse first, RiverCourse second, HydrologyCaveCandidate candidate @@ -532,7 +534,7 @@ final class HydrologyCaveCourseFilter { && Math.abs(firstPoint.y() - secondPoint.y()) <= verticalRadius; } - private HydraulicSegment terminalGrotto(RiverCourse course) { + HydraulicSegment terminalGrotto(RiverCourse course) { if (course == null || course.segments().isEmpty()) { return null; } @@ -540,14 +542,14 @@ final class HydrologyCaveCourseFilter { return segment.type().isGrotto() ? segment : null; } - private boolean sharesDrainageOutlet(RiverCourse first, RiverCourse second) { + boolean sharesDrainageOutlet(RiverCourse first, RiverCourse second) { return second != null && first.outletId().isPresent() && second.outletId().isPresent() && first.outletId().getAsLong() == second.outletId().getAsLong(); } - private int maximumCaveY(HydrologyColumnSample sample, long courseId) { + int maximumCaveY(HydrologyColumnSample sample, long courseId) { int maximumY = Integer.MIN_VALUE; for (HydrologyColumnLayer layer : sample.layers()) { if (layer.feature().courseId() == courseId @@ -566,28 +568,28 @@ final class HydrologyCaveCourseFilter { Iterable columns, List diagnostics ) { - LinkedHashMap builders = candidateSpanBuilders(courses, columns); + LinkedHashMap builders = candidateSpanBuilders(courses, columns); return preflightRejectedCourseIds(builders, diagnostics); } - private Set preflightRejectedCourseIds( + Set preflightRejectedCourseIds( List courses, HydrologyFootprintCompiler.ValidationRaster validation, List diagnostics ) { - LinkedHashMap builders = candidateSpanBuilders(courses, validation); + LinkedHashMap builders = candidateSpanBuilders(courses, validation); return preflightRejectedCourseIds(builders, diagnostics); } - private Set preflightRejectedCourseIds( - LinkedHashMap builders, + Set preflightRejectedCourseIds( + LinkedHashMap builders, List diagnostics ) { if (builders.isEmpty()) { return Set.of(); } HashSet rejectedCourseIds = new HashSet<>(); - for (CandidateSpanBuilder builder : builders.values()) { + for (CaveCandidateSpanBuilder builder : builders.values()) { HydrologyFeatureType oversizedGrotto = builder.oversizedGrotto(options); boolean oversizedCourse = builder.positionCount() > HydrologyCavePlannerSettings.MAXIMUM_PLANNED_MUTATIONS; @@ -604,7 +606,7 @@ final class HydrologyCaveCourseFilter { HydrologyCandidateRejection.VOLUME_LIMIT.ordinal() ); } - for (CandidateSpanBuilder builder : builders.values()) { + for (CaveCandidateSpanBuilder builder : builders.values()) { if (rejectedCourseIds.contains(builder.course().id()) || !builder.exposed(view) || builder.allowsIntentionalSurfaceExposure()) { @@ -623,7 +625,7 @@ final class HydrologyCaveCourseFilter { return Set.copyOf(rejectedCourseIds); } - private List withoutZeroContributionEdges(List courses) { + List withoutZeroContributionEdges(List courses) { ArrayList normalized = new ArrayList<>(courses.size()); boolean changed = false; for (RiverCourse course : courses) { @@ -653,15 +655,15 @@ final class HydrologyCaveCourseFilter { return changed ? List.copyOf(normalized) : courses; } - private LinkedHashMap candidateSpanBuilders( + LinkedHashMap candidateSpanBuilders( List courses, Iterable columns ) { - LinkedHashMap builders = new LinkedHashMap<>(); + LinkedHashMap builders = new LinkedHashMap<>(); for (RiverCourse course : courses) { HydraulicSegment representative = representativeCaveSegment(course); if (representative != null) { - builders.put(course.id(), new CandidateSpanBuilder(course, representative)); + builders.put(course.id(), new CaveCandidateSpanBuilder(course, representative)); } } if (builders.isEmpty()) { @@ -670,7 +672,7 @@ final class HydrologyCaveCourseFilter { for (HydrologyColumnSample sample : columns) { for (HydrologyColumnLayer layer : sample.layers()) { - CandidateSpanBuilder builder = builders.get(layer.feature().courseId()); + CaveCandidateSpanBuilder builder = builders.get(layer.feature().courseId()); if (builder == null || !isCaveLayer(layer) || layer.oceanApron() || !layer.channel() || !layer.terrainOwned()) { continue; @@ -678,27 +680,27 @@ final class HydrologyCaveCourseFilter { builder.addAction(sample.x(), sample.z(), layer); } for (HydrologyColumnLayer layer : sample.layers()) { - CandidateSpanBuilder builder = builders.get(layer.feature().courseId()); + CaveCandidateSpanBuilder builder = builders.get(layer.feature().courseId()); if (builder != null && layer.oceanApron()) { builder.addOceanOpening(sample.x(), sample.z(), sample.naturalHeight() + 1, layer.ceilingY()); } } } - builders.values().removeIf(CandidateSpanBuilder::isEmpty); + builders.values().removeIf(CaveCandidateSpanBuilder::isEmpty); return builders; } - private LinkedHashMap candidateSpanBuilders( + LinkedHashMap candidateSpanBuilders( List courses, HydrologyFootprintCompiler.ValidationRaster validation ) { - LinkedHashMap builders = new LinkedHashMap<>(); + LinkedHashMap builders = new LinkedHashMap<>(); for (RiverCourse course : courses) { HydraulicSegment representative = representativeCaveSegment(course); if (representative == null) { continue; } - CandidateSpanBuilder builder = new CandidateSpanBuilder(course, representative); + CaveCandidateSpanBuilder builder = new CaveCandidateSpanBuilder(course, representative); for (HydrologyColumnSample sample : validation.columnsForCourse(course.id())) { int maximumCaveY = Integer.MIN_VALUE; for (HydrologyColumnLayer layer : sample.layers()) { @@ -750,15 +752,15 @@ final class HydrologyCaveCourseFilter { return builders; } - private LinkedHashMap candidateBuilders( + LinkedHashMap candidateBuilders( List courses, Iterable columns ) { - LinkedHashMap builders = new LinkedHashMap<>(); + LinkedHashMap builders = new LinkedHashMap<>(); for (RiverCourse course : courses) { HydraulicSegment representative = representativeCaveSegment(course); if (representative != null) { - builders.put(course.id(), new CandidateBuilder(course, representative)); + builders.put(course.id(), new CaveCandidateBuilder(course, representative)); } } if (builders.isEmpty()) { @@ -767,7 +769,7 @@ final class HydrologyCaveCourseFilter { for (HydrologyColumnSample sample : columns) { for (HydrologyColumnLayer layer : sample.layers()) { - CandidateBuilder builder = builders.get(layer.feature().courseId()); + CaveCandidateBuilder builder = builders.get(layer.feature().courseId()); if (builder == null || !isCaveLayer(layer) || layer.oceanApron() || !layer.channel() || !layer.terrainOwned()) { continue; @@ -779,7 +781,7 @@ final class HydrologyCaveCourseFilter { } } for (HydrologyColumnLayer layer : sample.layers()) { - CandidateBuilder builder = builders.get(layer.feature().courseId()); + CaveCandidateBuilder builder = builders.get(layer.feature().courseId()); if (builder == null || !layer.oceanApron()) { continue; } @@ -788,21 +790,21 @@ final class HydrologyCaveCourseFilter { } } } - builders.values().removeIf(CandidateBuilder::isEmpty); + builders.values().removeIf(CaveCandidateBuilder::isEmpty); return builders; } - private LinkedHashMap candidateBuilders( + LinkedHashMap candidateBuilders( List courses, HydrologyFootprintCompiler.ValidationRaster validation ) { - LinkedHashMap builders = new LinkedHashMap<>(); + LinkedHashMap builders = new LinkedHashMap<>(); for (RiverCourse course : courses) { HydraulicSegment representative = representativeCaveSegment(course); if (representative == null) { continue; } - CandidateBuilder builder = new CandidateBuilder(course, representative); + CaveCandidateBuilder builder = new CaveCandidateBuilder(course, representative); for (HydrologyColumnSample sample : validation.columnsForCourse(course.id())) { int maximumCaveY = Integer.MIN_VALUE; for (HydrologyColumnLayer layer : sample.layers()) { @@ -853,7 +855,7 @@ final class HydrologyCaveCourseFilter { return builders; } - private static HydraulicSegment representativeCaveSegment(RiverCourse course) { + static HydraulicSegment representativeCaveSegment(RiverCourse course) { HydraulicSegment selected = null; for (HydraulicSegment segment : course.segments()) { if (!segment.type().isUnderground() && !segment.type().isDeepFluid()) { @@ -866,7 +868,7 @@ final class HydrologyCaveCourseFilter { return selected; } - private static int candidatePriority(HydrologyFeatureType type) { + static int candidatePriority(HydrologyFeatureType type) { if (type.isGrotto()) { return 0; } @@ -876,11 +878,11 @@ final class HydrologyCaveCourseFilter { return 2; } - private boolean isCaveLayer(HydrologyColumnLayer layer) { + boolean isCaveLayer(HydrologyColumnLayer layer) { return layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid(); } - private HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) { + HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) { if (y > layer.fluidHeadY()) { return HydrologyCaveAction.DRY_AIR; } @@ -890,7 +892,7 @@ final class HydrologyCaveCourseFilter { return HydrologyCaveAction.WET_SOURCE; } - private void addDiagnostic( + void addDiagnostic( RiverCourse course, HydrologyCavePlan plan, List diagnostics @@ -906,7 +908,7 @@ final class HydrologyCaveCourseFilter { ); } - private void addDiagnostic( + void addDiagnostic( RiverCourse course, HydraulicSegment representative, HydrologyFeatureType type, @@ -930,7 +932,7 @@ final class HydrologyCaveCourseFilter { )); } - private static Graph acceptedGraph( + static CaveCourseGraph acceptedGraph( List nodes, List edges, List outlets, @@ -975,7 +977,7 @@ final class HydrologyCaveCourseFilter { acceptedOutlets.add(outlet); } } - return new Graph( + return new CaveCourseGraph( List.copyOf(acceptedNodes), List.copyOf(acceptedEdges), List.copyOf(acceptedOutlets) @@ -1000,7 +1002,7 @@ final class HydrologyCaveCourseFilter { acceptedPlans.add(plan); } } - Graph graph = acceptedGraph( + CaveCourseGraph graph = acceptedGraph( result.nodes(), result.edges(), result.outlets(), @@ -1048,26 +1050,6 @@ final class HydrologyCaveCourseFilter { ) { } - private record Graph( - List nodes, - List edges, - List outlets - ) { - } - - private record SurfaceComposition( - HydrologyCaveCandidate candidate, - boolean accepted - ) { - private static SurfaceComposition accepted(HydrologyCaveCandidate candidate) { - return new SurfaceComposition(Objects.requireNonNull(candidate), true); - } - - private static SurfaceComposition rejected() { - return new SurfaceComposition(null, false); - } - } - record Options( boolean connectToExistingCaves, int coastalGrottoMaximumVolume, @@ -1079,7 +1061,7 @@ final class HydrologyCaveCourseFilter { } } - private int maximumVolume(HydrologyFeatureType type) { + int maximumVolume(HydrologyFeatureType type) { return switch (type) { case COASTAL_GROTTO -> coastalGrottoMaximumVolume; case INLAND_GROTTO -> inlandGrottoMaximumVolume; @@ -1200,492 +1182,7 @@ final class HydrologyCaveCourseFilter { } } - private static final class CandidateSpanBuilder { - private final RiverCourse course; - private final HydraulicSegment representative; - private final LinkedHashMap actions; - private final LinkedHashMap openings; - private final LinkedHashMap coastalGrottoActions; - private final LinkedHashMap inlandGrottoActions; - private final List surfaceOpenings; - private int minimumX; - private int maximumX; - private int minimumY; - private int maximumY; - private int minimumZ; - private int maximumZ; - - private CandidateSpanBuilder(RiverCourse course, HydraulicSegment representative) { - this.course = course; - this.representative = representative; - this.actions = new LinkedHashMap<>(); - this.openings = new LinkedHashMap<>(); - this.coastalGrottoActions = new LinkedHashMap<>(); - this.inlandGrottoActions = new LinkedHashMap<>(); - this.surfaceOpenings = surfaceOpenings(course); - this.minimumX = Integer.MAX_VALUE; - this.maximumX = Integer.MIN_VALUE; - this.minimumY = Integer.MAX_VALUE; - this.maximumY = Integer.MIN_VALUE; - this.minimumZ = Integer.MAX_VALUE; - this.maximumZ = Integer.MIN_VALUE; - } - - private RiverCourse course() { - return course; - } - - private HydraulicSegment representative() { - return representative; - } - - private boolean isEmpty() { - return actions.isEmpty(); - } - - private boolean allowsIntentionalSurfaceExposure() { - return course.surfaceSinkholeContinuation(); - } - - private void addAction(int x, int z, HydrologyColumnLayer layer) { - int minimumActionY = layer.bedY() + 1; - int maximumActionY = layer.ceilingY(); - if (minimumActionY > maximumActionY) { - return; - } - addSpan(actions, x, z, minimumActionY, maximumActionY); - if (layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO) { - addSpan(coastalGrottoActions, x, z, minimumActionY, maximumActionY); - } else if (layer.feature().type() == HydrologyFeatureType.INLAND_GROTTO) { - addSpan(inlandGrottoActions, x, z, minimumActionY, maximumActionY); - } - include(x, minimumActionY, z); - include(x, maximumActionY, z); - for (SurfaceOpening opening : surfaceOpenings) { - if (!opening.matchesColumn(layer, x, z)) { - continue; - } - int minimumOpeningY = Math.max(minimumActionY, opening.minimumY()); - if (minimumOpeningY > maximumActionY) { - continue; - } - if (!opening.includeNeighborhood()) { - addOpening(x, z, minimumOpeningY, maximumActionY); - continue; - } - addOpening(x, z, minimumOpeningY - 1, maximumActionY + 1); - for (int[] offset : HORIZONTAL_NEIGHBORS) { - addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumActionY); - } - } - } - - private void addOceanOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) { - if (minimumOpeningY <= maximumOpeningY) { - addOpening(x, z, minimumOpeningY, maximumOpeningY); - } - } - - private void addOpeningNeighborhood( - int x, - int z, - int minimumOpeningY, - int maximumOpeningY - ) { - if (minimumOpeningY > maximumOpeningY) { - return; - } - addOpening(x, z, minimumOpeningY - 1, maximumOpeningY + 1); - for (int[] offset : HORIZONTAL_NEIGHBORS) { - addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumOpeningY); - } - } - - private void addAdjacentSurfaceOpenings( - HydrologyFootprintCompiler.ValidationRaster validation - ) { - for (Map.Entry entry : actions.entrySet()) { - int x = RiverFootprint.unpackX(entry.getKey()); - int z = RiverFootprint.unpackZ(entry.getKey()); - for (int[] offset : HORIZONTAL_NEIGHBORS) { - int neighborX = x + offset[0]; - int neighborZ = z + offset[1]; - if (!validation.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) { - continue; - } - for (YSpan span : entry.getValue().spans()) { - addOpening(neighborX, neighborZ, span.minimumY(), span.maximumY()); - } - } - } - } - - private HydrologyFeatureType oversizedGrotto(Options options) { - if (positionCount(coastalGrottoActions) - > options.maximumVolume(HydrologyFeatureType.COASTAL_GROTTO)) { - return HydrologyFeatureType.COASTAL_GROTTO; - } - if (positionCount(inlandGrottoActions) - > options.maximumVolume(HydrologyFeatureType.INLAND_GROTTO)) { - return HydrologyFeatureType.INLAND_GROTTO; - } - return null; - } - - private long positionCount() { - return positionCount(actions); - } - - private boolean exposed(CaveVoxelView view) { - CandidateBounds bounds = bounds(); - for (Map.Entry entry : actions.entrySet()) { - int x = RiverFootprint.unpackX(entry.getKey()); - int z = RiverFootprint.unpackZ(entry.getKey()); - SpanSet actionSpans = entry.getValue(); - SpanSet openingSpans = openings.get(entry.getKey()); - if (exposedDifference(actionSpans, null, openingSpans, x, z, bounds, view)) { - return true; - } - for (YSpan span : actionSpans.spans()) { - int lowerBoundary = span.minimumY() - 1; - if (!actionSpans.contains(lowerBoundary) - && !contains(openingSpans, lowerBoundary) - && exposed(x, lowerBoundary, z, bounds, view)) { - return true; - } - int upperBoundary = span.maximumY() + 1; - if (!actionSpans.contains(upperBoundary) - && !contains(openingSpans, upperBoundary) - && exposed(x, upperBoundary, z, bounds, view)) { - return true; - } - } - for (int[] offset : HORIZONTAL_NEIGHBORS) { - int neighborX = x + offset[0]; - int neighborZ = z + offset[1]; - long neighborKey = RiverFootprint.pack(neighborX, neighborZ); - if (exposedDifference( - actionSpans, - actions.get(neighborKey), - openings.get(neighborKey), - neighborX, - neighborZ, - bounds, - view - )) { - return true; - } - } - } - return false; - } - - private boolean exposedDifference( - SpanSet source, - SpanSet excludedActions, - SpanSet excludedOpenings, - int x, - int z, - CandidateBounds bounds, - CaveVoxelView view - ) { - for (YSpan span : source.spans()) { - if (exposedDifference( - span, - excludedActions, - excludedOpenings, - x, - z, - bounds, - view - )) { - return true; - } - } - return false; - } - - private boolean exposedDifference( - YSpan source, - SpanSet excludedActions, - SpanSet excludedOpenings, - int x, - int z, - CandidateBounds bounds, - CaveVoxelView view - ) { - List actionSpans = excludedActions == null ? List.of() : excludedActions.spans(); - List openingSpans = excludedOpenings == null ? List.of() : excludedOpenings.spans(); - int actionIndex = 0; - int openingIndex = 0; - long cursor = source.minimumY(); - while (cursor <= source.maximumY()) { - while (actionIndex < actionSpans.size() - && actionSpans.get(actionIndex).maximumY() < cursor) { - actionIndex++; - } - while (openingIndex < openingSpans.size() - && openingSpans.get(openingIndex).maximumY() < cursor) { - openingIndex++; - } - YSpan action = actionIndex < actionSpans.size() ? actionSpans.get(actionIndex) : null; - YSpan opening = openingIndex < openingSpans.size() ? openingSpans.get(openingIndex) : null; - YSpan excluded = first(action, opening); - if (excluded == null || excluded.minimumY() > source.maximumY()) { - return exposed(x, (int) cursor, source.maximumY(), z, bounds, view); - } - if (cursor < excluded.minimumY() - && exposed(x, (int) cursor, excluded.minimumY() - 1, z, bounds, view)) { - return true; - } - cursor = Math.max(cursor, (long) excluded.maximumY() + 1L); - if (excluded == action) { - actionIndex++; - } else { - openingIndex++; - } - } - return false; - } - - private YSpan first(YSpan first, YSpan second) { - if (first == null) { - return second; - } - if (second == null) { - return first; - } - return first.minimumY() <= second.minimumY() ? first : second; - } - - private boolean exposed( - int x, - int minimumY, - int maximumY, - int z, - CandidateBounds bounds, - CaveVoxelView view - ) { - if (!bounds.containsColumn(x, z)) { - return false; - } - int boundedMinimumY = Math.max(minimumY, bounds.minimumY()); - int boundedMaximumY = Math.min(maximumY, bounds.maximumY()); - return boundedMinimumY <= boundedMaximumY - && view.hasAboveTerrainSurface(x, z, boundedMinimumY, boundedMaximumY); - } - - private boolean exposed( - int x, - int y, - int z, - CandidateBounds bounds, - CaveVoxelView view - ) { - CavePosition position = new CavePosition(x, y, z); - return bounds.contains(position) - && view.isInWorld(position) - && view.isAboveTerrainSurface(position); - } - - private CandidateBounds bounds() { - HydrologyPoint start = representative.start(); - int waterHead = representative.upstreamHeadY(); - int entryY = Math.max(waterHead, maximumY); - int horizontalRadius = horizontalRadius( - start.x(), - start.z(), - minimumX, - maximumX, - minimumZ, - maximumZ - ); - int maximumDepth = Math.max(1, entryY - minimumY + 2); - int dryHeadroom = Math.max(0, maximumY - waterHead); - return new CandidateBounds( - start.x(), - entryY, - start.z(), - waterHead, - horizontalRadius, - maximumDepth, - dryHeadroom - ); - } - - private void addOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) { - addSpan(openings, x, z, minimumOpeningY, maximumOpeningY); - include(x, minimumOpeningY, z); - include(x, maximumOpeningY, z); - } - - private void include(int x, int y, int z) { - minimumX = Math.min(minimumX, x); - maximumX = Math.max(maximumX, x); - minimumY = Math.min(minimumY, y); - maximumY = Math.max(maximumY, y); - minimumZ = Math.min(minimumZ, z); - maximumZ = Math.max(maximumZ, z); - } - - private static void addSpan( - Map spans, - int x, - int z, - int minimumY, - int maximumY - ) { - spans.computeIfAbsent(RiverFootprint.pack(x, z), (Long ignored) -> new SpanSet()) - .add(minimumY, maximumY); - } - - private static boolean contains(SpanSet spans, int y) { - return spans != null && spans.contains(y); - } - - private static long positionCount(Map spans) { - long count = 0L; - for (SpanSet spanSet : spans.values()) { - count += spanSet.positionCount(); - } - return count; - } - } - - private static final class SpanSet { - private int[] pending; - private int size; - private List normalized; - - private SpanSet() { - this.pending = new int[4]; - this.normalized = null; - } - - private void add(int minimumY, int maximumY) { - if (minimumY > maximumY) { - return; - } - int requiredLength = Math.multiplyExact(size + 1, 2); - if (requiredLength > pending.length) { - pending = Arrays.copyOf(pending, Math.multiplyExact(pending.length, 2)); - } - pending[size * 2] = minimumY; - pending[size * 2 + 1] = maximumY; - size++; - normalized = null; - } - - private List spans() { - if (normalized != null) { - return normalized; - } - sortPending(); - ArrayList merged = new ArrayList<>(size); - for (int index = 0; index < size; index++) { - YSpan span = new YSpan(pending[index * 2], pending[index * 2 + 1]); - if (merged.isEmpty()) { - merged.add(span); - continue; - } - YSpan previous = merged.getLast(); - if ((long) span.minimumY() > (long) previous.maximumY() + 1L) { - merged.add(span); - continue; - } - merged.set( - merged.size() - 1, - new YSpan(previous.minimumY(), Math.max(previous.maximumY(), span.maximumY())) - ); - } - normalized = List.copyOf(merged); - return normalized; - } - - private void sortPending() { - for (int index = 1; index < size; index++) { - int minimumY = pending[index * 2]; - int maximumY = pending[index * 2 + 1]; - int insertionIndex = index; - while (insertionIndex > 0) { - int previousMinimumY = pending[(insertionIndex - 1) * 2]; - int previousMaximumY = pending[(insertionIndex - 1) * 2 + 1]; - if (previousMinimumY < minimumY - || previousMinimumY == minimumY && previousMaximumY <= maximumY) { - break; - } - pending[insertionIndex * 2] = previousMinimumY; - pending[insertionIndex * 2 + 1] = previousMaximumY; - insertionIndex--; - } - pending[insertionIndex * 2] = minimumY; - pending[insertionIndex * 2 + 1] = maximumY; - } - } - - private boolean contains(int y) { - List spans = spans(); - int minimumIndex = 0; - int maximumIndex = spans.size() - 1; - while (minimumIndex <= maximumIndex) { - int index = (minimumIndex + maximumIndex) >>> 1; - YSpan span = spans.get(index); - if (y < span.minimumY()) { - maximumIndex = index - 1; - } else if (y > span.maximumY()) { - minimumIndex = index + 1; - } else { - return true; - } - } - return false; - } - - private long positionCount() { - long count = 0L; - for (YSpan span : spans()) { - count += (long) span.maximumY() - span.minimumY() + 1L; - } - return count; - } - } - - private record YSpan(int minimumY, int maximumY) { - } - - private record CandidateBounds( - int entryX, - int entryY, - int entryZ, - int waterHeadY, - int horizontalRadius, - int maximumDepth, - int dryHeadroom - ) { - private boolean containsColumn(int x, int z) { - long deltaX = (long) x - entryX; - long deltaZ = (long) z - entryZ; - long radiusSquared = (long) horizontalRadius * horizontalRadius; - return deltaX * deltaX + deltaZ * deltaZ <= radiusSquared; - } - - private int minimumY() { - return Math.toIntExact((long) entryY - maximumDepth); - } - - private int maximumY() { - return Math.max(entryY, Math.addExact(waterHeadY, dryHeadroom + 1)); - } - - private boolean contains(CavePosition position) { - if (!containsColumn(position.x(), position.z())) { - return false; - } - return position.y() >= minimumY() && position.y() <= maximumY(); - } - } - - private static int horizontalRadius( + static int horizontalRadius( int entryX, int entryZ, int minimumX, @@ -1708,11 +1205,11 @@ final class HydrologyCaveCourseFilter { return Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(maximumDistanceSquared)) + 2); } - private static List surfaceOpenings(RiverCourse course) { + static List surfaceOpenings(RiverCourse course) { if (course.type() != RiverCourseType.SURFACE) { return List.of(); } - ArrayList openings = new ArrayList<>(); + ArrayList openings = new ArrayList<>(); List segments = course.segments(); for (int index = 0; index < segments.size(); index++) { HydraulicSegment segment = segments.get(index); @@ -1738,7 +1235,7 @@ final class HydrologyCaveCourseFilter { boolean includeNeighborhood = segment.type() != HydrologyFeatureType.SINKHOLE; if (index == 0 || !segments.get(index - 1).type().isUnderground()) { - openings.add(SurfaceOpening.create( + openings.add(CaveSurfaceOpening.create( segment, true, radiusSquared, @@ -1751,7 +1248,7 @@ final class HydrologyCaveCourseFilter { if (terminalCoastalOpening || (index < segments.size() - 1 && !segments.get(index + 1).type().isUnderground())) { - openings.add(SurfaceOpening.create( + openings.add(CaveSurfaceOpening.create( segment, false, radiusSquared, @@ -1762,246 +1259,4 @@ final class HydrologyCaveCourseFilter { } return List.copyOf(openings); } - - private static final class CandidateBuilder { - private final RiverCourse course; - private final HydraulicSegment representative; - private final LinkedHashMap actions; - private final LinkedHashSet openings; - private final CavePositionIndex openingIndex; - private final List surfaceOpenings; - private int minimumX; - private int maximumX; - private int minimumY; - private int maximumY; - private int minimumZ; - private int maximumZ; - - private CandidateBuilder(RiverCourse course, HydraulicSegment representative) { - this.course = course; - this.representative = representative; - this.actions = new LinkedHashMap<>(); - this.openings = new LinkedHashSet<>(); - this.openingIndex = new CavePositionIndex(); - this.surfaceOpenings = surfaceOpenings(course); - this.minimumX = Integer.MAX_VALUE; - this.maximumX = Integer.MIN_VALUE; - this.minimumY = Integer.MAX_VALUE; - this.maximumY = Integer.MIN_VALUE; - this.minimumZ = Integer.MAX_VALUE; - this.maximumZ = Integer.MIN_VALUE; - } - - private RiverCourse course() { - return course; - } - - private HydraulicSegment representative() { - return representative; - } - - private boolean isEmpty() { - return actions.isEmpty(); - } - - private void addAction(CavePosition position, HydrologyCaveAction action) { - HydrologyCaveAction existing = actions.get(position); - if (existing == null || actionPriority(action) < actionPriority(existing)) { - actions.put(position, action); - } - include(position); - } - - private void addOpening(CavePosition position) { - if (openingIndex.add(position.x(), position.y(), position.z())) { - openings.add(position); - } - include(position); - } - - private void addOpening(int x, int y, int z) { - if (openingIndex.add(x, y, z)) { - openings.add(new CavePosition(x, y, z)); - } - include(x, y, z); - } - - private void addOpeningNeighborhood(CavePosition position) { - addOpening(position); - for (int[] offset : NEIGHBORS) { - addOpening( - position.x() + offset[0], - position.y() + offset[1], - position.z() + offset[2] - ); - } - } - - private void addAdjacentSurfaceOpenings( - HydrologyFootprintCompiler.ValidationRaster validation - ) { - ArrayList actionPositions = new ArrayList<>(actions.keySet()); - for (CavePosition position : actionPositions) { - for (int[] offset : HORIZONTAL_NEIGHBORS) { - int neighborX = position.x() + offset[0]; - int neighborZ = position.z() + offset[1]; - if (validation.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) { - addOpening(neighborX, position.y(), neighborZ); - } - } - } - } - - private void addSurfaceOpening(HydrologyColumnLayer layer, CavePosition position) { - for (SurfaceOpening opening : surfaceOpenings) { - if (!opening.matches(layer, position)) { - continue; - } - if (opening.includeNeighborhood()) { - addOpeningNeighborhood(position); - } else { - addOpening(position); - } - } - } - - private void include(CavePosition position) { - include(position.x(), position.y(), position.z()); - } - - private void include(int x, int y, int z) { - minimumX = Math.min(minimumX, x); - maximumX = Math.max(maximumX, x); - minimumY = Math.min(minimumY, y); - maximumY = Math.max(maximumY, y); - minimumZ = Math.min(minimumZ, z); - maximumZ = Math.max(maximumZ, z); - } - - private HydrologyCaveCandidate build(Options options) { - HydrologyPoint start = representative.start(); - int waterHead = representative.upstreamHeadY(); - int entryY = Math.max(waterHead, maximumY); - CavePosition entry = new CavePosition(start.x(), entryY, start.z()); - CavePosition target = new CavePosition(start.x(), waterHead, start.z()); - int horizontalRadius = horizontalRadius( - entry.x(), - entry.z(), - minimumX, - maximumX, - minimumZ, - maximumZ - ); - int maximumDepth = Math.max(1, entryY - minimumY + 2); - int dryHeadroom = Math.max(0, maximumY - waterHead); - int volume = Math.max(1, actions.size()); - HydrologyCavePlannerSettings settings = new HydrologyCavePlannerSettings( - horizontalRadius, - maximumDepth, - volume, - 1, - 1, - 1, - 1, - dryHeadroom, - HydrologyCaveFluidPolicy.REJECT_EXISTING, - HydrologyCaveGrottoShape.ELLIPSOID, - horizontalRadius, - maximumDepth - ); - return new HydrologyCaveCandidate( - new HydrologyCaveSource( - course.id(), - entry, - target, - waterHead, - HydrologyCaveMode.GENERATED_GROTTO - ), - course.profileKey(), - settings, - options.connectToExistingCaves() && course.type() == RiverCourseType.UNDERGROUND, - actions, - openings - ); - } - - private static int actionPriority(HydrologyCaveAction action) { - return switch (action) { - case WET_SOURCE -> 0; - case FALLING_FLUID -> 1; - case DRY_AIR -> 2; - case SEAL_GUARD -> 3; - }; - } - - } - - private record SurfaceOpening( - long segmentId, - HydrologyPoint point, - double interiorX, - double interiorZ, - long radiusSquared, - int minimumY, - boolean includeNeighborhood - ) { - private static SurfaceOpening create( - HydraulicSegment segment, - boolean start, - long radiusSquared, - int minimumY, - boolean includeNeighborhood - ) { - List centerline = segment.centerline(); - int boundaryIndex = start ? 0 : centerline.size() - 1; - int interiorIndex = start - ? Math.min(1, centerline.size() - 1) - : Math.max(0, centerline.size() - 2); - HydrologyPoint point = centerline.get(boundaryIndex); - HydrologyPoint interior = centerline.get(interiorIndex); - double deltaX = interior.x() - point.x(); - double deltaZ = interior.z() - point.z(); - double lengthSquared = deltaX * deltaX + deltaZ * deltaZ; - double scale = 0D; - if (includeNeighborhood) { - scale = lengthSquared <= radiusSquared - ? 1D - : StrictMath.sqrt(radiusSquared / lengthSquared); - } - return new SurfaceOpening( - segment.id(), - point, - point.x() + deltaX * scale, - point.z() + deltaZ * scale, - radiusSquared, - minimumY, - includeNeighborhood - ); - } - - private boolean matches(HydrologyColumnLayer layer, CavePosition position) { - return matchesColumn(layer, position.x(), position.z()) - && position.y() >= minimumY; - } - - private boolean matchesColumn(HydrologyColumnLayer layer, int x, int z) { - return (includeNeighborhood || layer.feature().segmentId() == segmentId) - && distanceSquared(x, z) <= radiusSquared; - } - - private double distanceSquared(int x, int z) { - double pathX = interiorX - point.x(); - double pathZ = interiorZ - point.z(); - double pathLengthSquared = pathX * pathX + pathZ * pathZ; - double positionX = x - point.x(); - double positionZ = z - point.z(); - double progress = pathLengthSquared == 0D - ? 0D - : (positionX * pathX + positionZ * pathZ) / pathLengthSquared; - double clampedProgress = Math.max(0D, Math.min(1D, progress)); - double deltaX = positionX - pathX * clampedProgress; - double deltaZ = positionZ - pathZ * clampedProgress; - return deltaX * deltaX + deltaZ * deltaZ; - } - } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyChannelGeometry.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyChannelGeometry.java new file mode 100644 index 000000000..53e76b48c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyChannelGeometry.java @@ -0,0 +1,358 @@ +package art.arcane.iris.engine.hydrology; + +final class HydrologyChannelGeometry { + private final HydrologyFootprintCompiler compiler; + + HydrologyChannelGeometry(HydrologyFootprintCompiler compiler) { + this.compiler = compiler; + } + + static final long ORGANIC_SHAPE_FIRST_PHASE_SALT = 0x4f5247414e31L; + static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L; + static final long ORGANIC_BED_VARIATION_SALT = 0x424544564152L; + static final long ORGANIC_CEILING_VARIATION_SALT = 0x4345494c564152L; + + double smoothStep(double progress) { + return progress * progress * (3D - 2D * progress); + } + + FootprintLayerShape shape( + RiverCourse course, + HydraulicSegment segment, + HydrologyPoint point, + int bed, + int fluidHead, + boolean falling, + boolean receiving + ) { + HydrologyFeatureType type = segment.type(); + int channelRadius = Math.max(1, segment.width() / 2); + double shoreWidth = 0D; + double gradingWidth = 0D; + int ceiling = fluidHead; + boolean ellipsoid = false; + boolean archedChannel = false; + boolean roundedSurfaceBed = false; + boolean organicBoundary = type.isSurface() || type.isUnderground() || type.isDeepFluid() || falling || receiving; + if (course.type() == RiverCourseType.SURFACE && type.isSurface()) { + roundedSurfaceBed = !falling; + shoreWidth = compiler.settings.surface().shoreWidth(); + gradingWidth = compiler.geometrySampler.sample( + HydrologyGeometrySampler.Field.SURFACE_BLEND_WIDTH, + course.profileKey(), + point.x(), + point.z(), + segment.id(), + compiler.settings.surface().banks().minimumBlendWidth(), + compiler.settings.surface().banks().maximumBlendWidth() + ); + if (type == HydrologyFeatureType.WATERFALL) { + shoreWidth = Math.min(2.5D, shoreWidth); + gradingWidth = Math.min(gradingWidth, Math.max(8D, channelRadius * 4D)); + } + } + if (type == HydrologyFeatureType.UNDERGROUND_POOL + || type == HydrologyFeatureType.UNDERGROUND_DROP + || type == HydrologyFeatureType.SINKHOLE) { + ceiling = fluidHead + compiler.geometrySampler.sample( + HydrologyGeometrySampler.Field.UNDERGROUND_HEADROOM, + course.profileKey(), + point.x(), + point.z(), + course.id(), + compiler.settings.underground().minimumHeadroom(), + compiler.settings.underground().maximumHeadroom() + ); + archedChannel = !falling; + } else if (type == HydrologyFeatureType.COASTAL_GROTTO) { + HydrologyPlannerSettings.Grotto grotto = compiler.settings.outlets().coastalGrotto(); + channelRadius = grotto.horizontalRadius(); + bed = fluidHead - grotto.verticalRadius(); + ceiling = fluidHead + grotto.headroom(); + ellipsoid = true; + organicBoundary = true; + } else if (type == HydrologyFeatureType.INLAND_GROTTO) { + HydrologyPlannerSettings.Grotto grotto = compiler.settings.outlets().inlandGrotto(); + channelRadius = grotto.horizontalRadius(); + bed = fluidHead - grotto.verticalRadius(); + ceiling = fluidHead + grotto.headroom(); + ellipsoid = true; + organicBoundary = true; + } else if (type.isDeepFluid()) { + HydrologyPlannerSettings.DeepFluid deepFluid = compiler.deepFluids.get(course.profileKey()); + if (deepFluid == null) { + throw new IllegalStateException("Missing deep-fluid planner settings for " + course.profileKey() + "."); + } + if (type == HydrologyFeatureType.DEEP_POOL) { + channelRadius = Math.max(1, segment.width() / 2); + bed = fluidHead - Math.max(segment.depth(), deepFluid.minimumVerticalRadius()); + ellipsoid = true; + } else { + archedChannel = !falling; + } + ceiling = fluidHead + deepFluid.headroom(); + organicBoundary = true; + } + if (receiving) { + HydrologyPlannerSettings.Drops drops = compiler.settings.geometry().drops(); + if (course.type() != RiverCourseType.SURFACE || !type.isSurface()) { + channelRadius = Math.max( + channelRadius, + (int) StrictMath.ceil(drops.basinWidth(segment.width()) / 2D) + ); + } + bed = Math.min(bed, fluidHead - drops.basinDepth(segment.depth(), segment.drop())); + if (course.type() == RiverCourseType.SURFACE && type.isSurface()) { + gradingWidth = Math.max( + gradingWidth, + Math.min(compiler.settings.surface().banks().maximumBlendWidth(), channelRadius * 2D) + ); + } + } + if (type == HydrologyFeatureType.UNDERGROUND_POOL && segment.centerline().size() > 1) { + int depthVariation = Math.min( + 2, + Math.max(0, compiler.settings.underground().maximumDepth() - (fluidHead - bed)) + ); + bed -= organicVerticalVariation( + HydrologyHash.mix(segment.id(), ORGANIC_BED_VARIATION_SALT), + point.x(), + point.z(), + depthVariation + ); + } + if (falling) { + bed = Math.min(bed, segment.downstreamHeadY() - 1); + ceiling = Math.max(ceiling, segment.upstreamHeadY()); + } + return new FootprintLayerShape( + channelRadius, + shoreWidth, + gradingWidth, + bed, + fluidHead, + ceiling, + ellipsoid, + archedChannel, + roundedSurfaceBed, + organicBoundary, + falling + ); + } + + int organicVerticalVariation( + long seed, + int x, + int z, + int maximum + ) { + if (maximum == 0) { + return 0; + } + double sampled = organicNoise(seed, x, z, 12); + return (int) StrictMath.round(sampled * maximum); + } + + double ellipsoidScale(int radius, double distance, double roundness) { + double normalized = Math.min(1D, distance / Math.max(1D, radius)); + return StrictMath.pow( + Math.max(0D, 1D - StrictMath.pow(normalized, roundness)), + 1D / roundness + ); + } + + double shapedDistance( + FootprintLayerShape shape, + HydraulicSegment segment, + int worldX, + int worldZ, + double deltaX, + double deltaZ, + int flowX, + int flowZ, + double rawDistance + ) { + if (segment.type() == HydrologyFeatureType.DEEP_POOL) { + return deepPoolDistance(shape, segment, worldX, worldZ, deltaX, deltaZ); + } + if (segment.type().isSurface() && !shape.fallingThroat()) { + return surfaceChannelDistance(shape, segment, worldX, worldZ, deltaX, deltaZ, flowX, flowZ); + } + if (rawDistance == 0D) { + return 0D; + } + double distance = rawDistance; + if (shape.fallingThroat() && (flowX != 0 || flowZ != 0)) { + double flowLength = StrictMath.hypot(flowX, flowZ); + double along = StrictMath.abs(deltaX * flowX + deltaZ * flowZ) / flowLength; + double across = StrictMath.abs(deltaX * -flowZ + deltaZ * flowX) / flowLength; + if (along > 0.75D) { + return shape.totalRadius() + 1D; + } + distance = across; + } + if (!shape.organicBoundary()) { + return distance; + } + HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type()); + if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO + || segment.type() == HydrologyFeatureType.INLAND_GROTTO) { + double orientation = HydrologyHash.unit(HydrologyHash.mix( + segment.courseId(), + ORGANIC_SHAPE_FIRST_PHASE_SALT + )) * StrictMath.PI * 2D; + double cosine = StrictMath.cos(orientation); + double sine = StrictMath.sin(orientation); + double rotatedX = deltaX * cosine + deltaZ * sine; + double rotatedZ = -deltaX * sine + deltaZ * cosine; + double aspect = channelShape.aspectMinimum() + HydrologyHash.unit(HydrologyHash.mix( + segment.courseId(), + ORGANIC_SHAPE_SECOND_PHASE_SALT + )) * channelShape.aspectRange(); + distance = StrictMath.hypot(rotatedX, rotatedZ / aspect); + } + double angle = StrictMath.atan2(deltaZ, deltaX); + double firstPhase = HydrologyHash.unit(HydrologyHash.mix( + segment.courseId(), + ORGANIC_SHAPE_FIRST_PHASE_SALT + )) * StrictMath.PI * 2D; + double secondPhase = HydrologyHash.unit(HydrologyHash.mix( + segment.courseId(), + ORGANIC_SHAPE_SECOND_PHASE_SALT + )) * StrictMath.PI * 2D; + double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase); + double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase); + double coherent = signedOrganicNoise( + segment.courseId(), + worldX, + worldZ, + channelShape.roughnessWavelength() + ); + double detail = signedOrganicNoise( + HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT), + worldX, + worldZ, + Math.max(3, channelShape.roughnessWavelength() / 2) + ); + double radialScale = channelShape.radialBase() + + (firstLobe - 0.5D) * channelShape.primaryLobeStrength() + + (secondLobe - 0.5D) * channelShape.detailLobeStrength() + + coherent * channelShape.wallRoughness() * 0.7D + + detail * channelShape.wallRoughness() * 0.3D; + radialScale = Math.max(channelShape.radialMinimum(), Math.min(channelShape.radialMaximum(), radialScale)); + return distance / radialScale; + } + + double surfaceChannelDistance( + FootprintLayerShape shape, + HydraulicSegment segment, + int worldX, + int worldZ, + double deltaX, + double deltaZ, + int flowX, + int flowZ + ) { + double flowLength = StrictMath.hypot(flowX, flowZ); + if (flowLength == 0D) { + return deepPoolDistance(shape, segment, worldX, worldZ, deltaX, deltaZ); + } + double signedCross = (deltaX * -flowZ + deltaZ * flowX) / flowLength; + HydrologyPlannerSettings.ChannelShape channelShape = compiler.settings.geometry().surface(); + double thalweg = signedOrganicNoise( + HydrologyHash.mix(segment.courseId(), ORGANIC_BED_VARIATION_SALT), + worldX, + worldZ, + channelShape.roughnessWavelength() + ) * shape.channelRadius() * channelShape.wallRoughness(); + long bankSeed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT); + double bankNoise = signedOrganicNoise( + bankSeed, + worldX, + worldZ, + channelShape.roughnessWavelength() + ); + double sideBias = signedCross < thalweg + ? HydrologyHash.unit(HydrologyHash.mix(bankSeed, 1L)) + : HydrologyHash.unit(HydrologyHash.mix(bankSeed, 2L)); + double widthScale = 0.86D + + bankNoise * channelShape.wallRoughness() + + (sideBias - 0.5D) * 0.08D; + widthScale = Math.max(1D, Math.min(1.18D, widthScale)); + double organicDistance = StrictMath.abs(signedCross - thalweg) / widthScale; + return Math.min(StrictMath.abs(signedCross), organicDistance); + } + + HydrologyPlannerSettings.ChannelShape channelShape(HydrologyFeatureType type) { + if (type.isGrotto() || type == HydrologyFeatureType.DEEP_POOL) { + return compiler.settings.geometry().grottos(); + } + if (type.isUnderground() || type == HydrologyFeatureType.DEEP_CHANNEL) { + return compiler.settings.geometry().underground(); + } + return compiler.settings.geometry().surface(); + } + + double signedOrganicNoise(long seed, int x, int z, int scale) { + return organicNoise(seed, x, z, scale) * 2D - 1D; + } + + double deepPoolDistance( + FootprintLayerShape shape, + HydraulicSegment segment, + int worldX, + int worldZ, + double deltaX, + double deltaZ + ) { + double radius = Math.max(1D, shape.channelRadius()); + long seed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_FIRST_PHASE_SALT); + double orientation = HydrologyHash.unit(seed) * StrictMath.PI * 2D; + double cosine = StrictMath.cos(orientation); + double sine = StrictMath.sin(orientation); + double rotatedX = deltaX * cosine + deltaZ * sine; + double rotatedZ = -deltaX * sine + deltaZ * cosine; + double angle = StrictMath.atan2(rotatedZ, rotatedX); + double secondPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 2L)) * StrictMath.PI * 2D; + double thirdPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 3L)) * StrictMath.PI * 2D; + double fifthPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 5L)) * StrictMath.PI * 2D; + double radialScale = 0.84D + + StrictMath.sin(angle * 2D + secondPhase) * 0.07D + + StrictMath.sin(angle * 3D + thirdPhase) * 0.05D + + StrictMath.sin(angle * 5D + fifthPhase) * 0.035D; + radialScale = Math.max(0.68D, Math.min(1D, radialScale)); + double normalizedDistance = StrictMath.hypot( + rotatedX / (radius * 0.92D), + rotatedZ / (radius * 0.7D) + ); + return normalizedDistance * radius / radialScale; + } + + double organicNoise(long seed, int x, int z, int scale) { + int cellX = Math.floorDiv(x, scale); + int cellZ = Math.floorDiv(z, scale); + double localX = Math.floorMod(x, scale) / (double) scale; + double localZ = Math.floorMod(z, scale) / (double) scale; + double smoothX = localX * localX * (3D - 2D * localX); + double smoothZ = localZ * localZ * (3D - 2D * localZ); + double top = interpolate( + organicCorner(seed, cellX, cellZ), + organicCorner(seed, cellX + 1, cellZ), + smoothX + ); + double bottom = interpolate( + organicCorner(seed, cellX, cellZ + 1), + organicCorner(seed, cellX + 1, cellZ + 1), + smoothX + ); + return interpolate(top, bottom, smoothZ); + } + + double organicCorner(long seed, int cellX, int cellZ) { + return HydrologyHash.unit(HydrologyHash.mix(seed, cellX, cellZ)); + } + + double interpolate(double first, double second, double progress) { + return first + (second - first) * progress; + } +} 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 bb66f0b63..e5e3a9cd0 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 @@ -9,7 +9,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; @@ -18,32 +17,34 @@ import java.util.Map; import java.util.Objects; final class HydrologyFootprintCompiler { - private static final int FEATURE_ROLE_COUNT = 7; - private static final int COURSE_FOOTPRINT_CACHE_SIZE = 32; - private static final int VALIDATION_RASTER_CACHE_SIZE = 32; - private static final int[][] HORIZONTAL_NEIGHBORS = { - {1, 0}, {-1, 0}, {0, 1}, {0, -1} - }; - private static final long ORGANIC_SHAPE_FIRST_PHASE_SALT = 0x4f5247414e31L; - private static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L; - private static final long ORGANIC_BED_VARIATION_SALT = 0x424544564152L; - private static final long ORGANIC_CEILING_VARIATION_SALT = 0x4345494c564152L; + static final int FEATURE_ROLE_COUNT = 7; + static final int COURSE_FOOTPRINT_CACHE_SIZE = 32; + static final int VALIDATION_RASTER_CACHE_SIZE = 32; - private final HydrologyPlannerSettings settings; - private final HydrologyTerrainSampler sampler; - private final HydrologyNaturalTerrainSampler naturalSampler; - private final HydrologyGeometrySampler geometrySampler; - private final Map deepFluids; - private final Long2ObjectOpenHashMap terrainSamples; - private final Long2ObjectOpenHashMap terrainBases; - private final Long2ObjectOpenHashMap naturalClassifications; - private final Map features; - private final LinkedHashMap courseFootprints; - private final LinkedHashMap validationCourseRasters; - private final Map rasterStencils; - private final SurfaceFootprintCompiler surfaceCompiler; - private final LinkedHashMap surfaceFootprints; - private int fullMaterializationCount; + static final int[][] HORIZONTAL_NEIGHBORS = { + {1, 0}, {-1, 0}, {0, 1}, {0, -1} + } + +; + + final HydrologyPlannerSettings settings; + final HydrologyTerrainSampler sampler; + final HydrologyNaturalTerrainSampler naturalSampler; + final HydrologyGeometrySampler geometrySampler; + final Map deepFluids; + final Long2ObjectOpenHashMap terrainSamples; + final Long2ObjectOpenHashMap terrainBases; + final Long2ObjectOpenHashMap naturalClassifications; + final Map features; + final LinkedHashMap courseFootprints; + final LinkedHashMap validationCourseRasters; + final Map rasterStencils; + final SurfaceFootprintCompiler surfaceCompiler; + final LinkedHashMap surfaceFootprints; + + int fullMaterializationCount; + final HydrologyFootprintRasterizer rasterizer; + final HydrologyChannelGeometry channelGeometry; HydrologyFootprintCompiler( HydrologyPlannerSettings settings, @@ -83,10 +84,12 @@ final class HydrologyFootprintCompiler { this.surfaceCompiler = new SurfaceFootprintCompiler(settings, this::sampleTerrainBasis, this.geometrySampler); this.surfaceFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true); this.fullMaterializationCount = 0; + this.rasterizer = new HydrologyFootprintRasterizer(this); + this.channelGeometry = new HydrologyChannelGeometry(this); } SurfaceFootprint surfaceFootprint(RiverCourse course) { - CourseRasterKey rasterKey = new CourseRasterKey( + FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey( course.id(), course.type(), course.profileKey(), @@ -112,14 +115,14 @@ final class HydrologyFootprintCompiler { if (courses.size() == 1) { return compileCourse(courses.getFirst()); } - Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); + Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); for (RiverCourse course : courses) { RiverFootprint footprint = compileCourse(course); for (Map.Entry entry : footprint.columns().entrySet()) { HydrologyColumnSample sample = entry.getValue(); - MutableColumn column = columns.get(entry.getKey()); + FootprintMutableColumn column = columns.get(entry.getKey()); if (column == null) { - columns.put(entry.getKey(), new MutableColumn(sample)); + columns.put(entry.getKey(), new FootprintMutableColumn(sample)); continue; } column.merge(sample); @@ -143,11 +146,11 @@ final class HydrologyFootprintCompiler { ); } - ArrayList courseRasters = new ArrayList<>(courses.size()); + ArrayList courseRasters = new ArrayList<>(courses.size()); ArrayList surfaces = new ArrayList<>(); Long2ObjectOpenHashMap firstSamples = new Long2ObjectOpenHashMap<>(); for (RiverCourse course : courses) { - ValidationCourseRaster raster = compileValidationCourse(course); + FootprintValidationCourseRaster raster = compileValidationCourse(course); courseRasters.add(raster); surfaces.add(raster.surface()); for (HydrologyColumnSample sample : raster.columns()) { @@ -168,21 +171,21 @@ final class HydrologyFootprintCompiler { return fullMaterializationCount; } - private ValidationCourseRaster compileValidationCourse(RiverCourse course) { - CourseRasterKey rasterKey = new CourseRasterKey( + FootprintValidationCourseRaster compileValidationCourse(RiverCourse course) { + FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey( course.id(), course.type(), course.profileKey(), course.segments() ); - ValidationCourseRaster cached = validationCourseRasters.get(rasterKey); + FootprintValidationCourseRaster cached = validationCourseRasters.get(rasterKey); if (cached != null) { return cached; } boolean caveCourse = hasCaveSegment(course); features.clear(); - Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); + Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); SurfaceFootprint surface = surfaceFootprint(course); for (SurfaceLayerColumn column : surface.columns()) { addLayer(columns, column.x(), column.z(), column.terrain(), column.layer()); @@ -192,12 +195,12 @@ final class HydrologyFootprintCompiler { HydraulicSegment segment = course.segments().get(segmentIndex); boolean firstSegment = segmentIndex == 0; boolean clipStart = segmentIndex > 0 - && segmentsJoin(course.segments().get(segmentIndex - 1), segment); + && rasterizer.segmentsJoin(course.segments().get(segmentIndex - 1), segment); boolean clipEnd = segmentIndex + 1 < course.segments().size() - && segmentsJoin(segment, course.segments().get(segmentIndex + 1)); + && rasterizer.segmentsJoin(segment, course.segments().get(segmentIndex + 1)); if (caveCourse && (segment.type().isUnderground() || segment.type().isDeepFluid())) { - rasterizeSegment( + rasterizer.rasterizeSegment( columns, course, segment, @@ -209,7 +212,7 @@ final class HydrologyFootprintCompiler { ); } } - ValidationCourseRaster raster = new ValidationCourseRaster( + FootprintValidationCourseRaster raster = new FootprintValidationCourseRaster( course.id(), buildValidationColumns(columns), surface @@ -221,7 +224,7 @@ final class HydrologyFootprintCompiler { return raster; } - private boolean hasCaveSegment(RiverCourse course) { + boolean hasCaveSegment(RiverCourse course) { for (HydraulicSegment segment : course.segments()) { if (segment.type().isUnderground() || segment.type().isDeepFluid()) { return true; @@ -230,11 +233,11 @@ final class HydrologyFootprintCompiler { return false; } - private List buildValidationColumns( - Long2ObjectLinkedOpenHashMap columns + List buildValidationColumns( + Long2ObjectLinkedOpenHashMap columns ) { ArrayList built = new ArrayList<>(columns.size()); - for (MutableColumn column : columns.values()) { + for (FootprintMutableColumn column : columns.values()) { built.add(column.build()); } built.sort(Comparator.comparingLong( @@ -243,7 +246,7 @@ final class HydrologyFootprintCompiler { return List.copyOf(built); } - private static void validateMatchingTerrainMetadata( + static void validateMatchingTerrainMetadata( HydrologyColumnSample first, HydrologyColumnSample second ) { @@ -258,8 +261,8 @@ final class HydrologyFootprintCompiler { } } - private RiverFootprint compileCourse(RiverCourse course) { - CourseRasterKey rasterKey = new CourseRasterKey( + RiverFootprint compileCourse(RiverCourse course) { + FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey( course.id(), course.type(), course.profileKey(), @@ -270,7 +273,7 @@ final class HydrologyFootprintCompiler { return cached; } features.clear(); - Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); + Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); SurfaceFootprint surface = surfaceFootprint(course); for (SurfaceLayerColumn column : surface.columns()) { addLayer(columns, column.x(), column.z(), column.terrain(), column.layer()); @@ -282,10 +285,10 @@ final class HydrologyFootprintCompiler { continue; } boolean clipStart = segmentIndex > 0 - && segmentsJoin(course.segments().get(segmentIndex - 1), segment); + && rasterizer.segmentsJoin(course.segments().get(segmentIndex - 1), segment); boolean clipEnd = segmentIndex + 1 < course.segments().size() - && segmentsJoin(segment, course.segments().get(segmentIndex + 1)); - rasterizeSegment( + && rasterizer.segmentsJoin(segment, course.segments().get(segmentIndex + 1)); + rasterizer.rasterizeSegment( columns, course, segment, @@ -304,707 +307,15 @@ final class HydrologyFootprintCompiler { return footprint; } - private RiverFootprint build(Long2ObjectLinkedOpenHashMap columns) { + RiverFootprint build(Long2ObjectLinkedOpenHashMap columns) { LinkedHashMap immutable = new LinkedHashMap<>(); - for (Long2ObjectMap.Entry entry : columns.long2ObjectEntrySet()) { + for (Long2ObjectMap.Entry entry : columns.long2ObjectEntrySet()) { immutable.put(entry.getLongKey(), entry.getValue().build()); } return new RiverFootprint(immutable); } - private double smoothStep(double progress) { - return progress * progress * (3D - 2D * progress); - } - - private void rasterizeSegment( - Long2ObjectLinkedOpenHashMap columns, - RiverCourse course, - HydraulicSegment segment, - boolean firstSegment, - boolean clipStart, - boolean clipEnd, - boolean validationOnly, - SurfaceRasterIndex plannedSurface - ) { - List centerline = continuousCenterline(segment); - if (segment.fallingFluid() && centerline.size() > 1) { - HydrologyPoint throat = centerline.getFirst(); - int fluidHead = segment.upstreamHeadY(); - LayerShape throatShape = shape( - course, - segment, - throat, - segment.downstreamHeadY() - 1, - fluidHead, - true, - false - ); - rasterizePoint( - columns, - course, - segment, - throat, - 0, - throatShape, - flowDelta(centerline, 0, true), - flowDelta(centerline, 0, false), - firstSegment, - true, - false, - false, - validationOnly, - plannedSurface - ); - rasterizeSweptSegment( - columns, - course, - segment, - List.copyOf(centerline.subList(1, centerline.size())), - false, - true, - clipEnd, - validationOnly, - plannedSurface - ); - return; - } - if (sweptChannel(segment, centerline)) { - rasterizeSweptSegment( - columns, - course, - segment, - centerline, - firstSegment, - clipStart, - clipEnd, - validationOnly, - plannedSurface - ); - return; - } - for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) { - HydrologyPoint point = centerline.get(pointIndex); - boolean oceanConnection = (segment.type() == HydrologyFeatureType.MOUTH - || segment.type() == HydrologyFeatureType.COASTAL_GROTTO) - && pointIndex == centerline.size() - 1; - boolean falling = segment.fallingFluid() && pointIndex == 0; - boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1; - int fluidHead = falling ? segment.upstreamHeadY() : point.y(); - int bed = falling - ? segment.downstreamHeadY() - 1 - : fluidHead - segment.depth(); - LayerShape shape = shape(course, segment, point, bed, fluidHead, falling, receiving); - int flowX = flowDelta(centerline, pointIndex, true); - int flowZ = flowDelta(centerline, pointIndex, false); - rasterizePoint( - columns, - course, - segment, - point, - pointIndex, - shape, - flowX, - flowZ, - firstSegment && pointIndex == 0, - falling, - receiving, - oceanConnection, - validationOnly, - plannedSurface - ); - } - } - - private boolean sweptChannel( - HydraulicSegment segment, - List centerline - ) { - if (centerline.size() < 2 || segment.fallingFluid()) { - return false; - } - return segment.type().isSurface() - || segment.type() == HydrologyFeatureType.UNDERGROUND_POOL - || segment.type() == HydrologyFeatureType.UNDERGROUND_DROP - || segment.type() == HydrologyFeatureType.SINKHOLE - || segment.type() == HydrologyFeatureType.DEEP_CHANNEL; - } - - private void rasterizeSweptSegment( - Long2ObjectLinkedOpenHashMap columns, - RiverCourse course, - HydraulicSegment segment, - List centerline, - boolean firstSegment, - boolean clipStart, - boolean clipEnd, - boolean validationOnly, - SurfaceRasterIndex plannedSurface - ) { - LayerShape[] shapes = new LayerShape[centerline.size()]; - int maximumRadius = 1; - int minimumX = Integer.MAX_VALUE; - int maximumX = Integer.MIN_VALUE; - int minimumZ = Integer.MAX_VALUE; - int maximumZ = Integer.MIN_VALUE; - for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) { - HydrologyPoint point = centerline.get(pointIndex); - int fluidHead = point.y(); - boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1; - LayerShape shape = shape( - course, - segment, - point, - fluidHead - segment.depth(), - fluidHead, - false, - receiving - ); - shapes[pointIndex] = shape; - int rasterRadius = segment.type().isSurface() - ? (int) StrictMath.ceil(shape.totalRadius()) - : shape.channelRadius(); - maximumRadius = Math.max(maximumRadius, rasterRadius); - minimumX = Math.min(minimumX, point.x()); - maximumX = Math.max(maximumX, point.x()); - minimumZ = Math.min(minimumZ, point.z()); - maximumZ = Math.max(maximumZ, point.z()); - } - minimumX = Math.subtractExact(minimumX, maximumRadius); - maximumX = Math.addExact(maximumX, maximumRadius); - minimumZ = Math.subtractExact(minimumZ, maximumRadius); - maximumZ = Math.addExact(maximumZ, maximumRadius); - HydrologyFeatureRef[][] pointFeatures = new HydrologyFeatureRef[centerline.size()][FEATURE_ROLE_COUNT]; - for (int z = minimumZ; z <= maximumZ; z++) { - for (int x = minimumX; x <= maximumX; x++) { - if (!withinLongitudinalBounds(centerline, x, z, clipStart, clipEnd)) { - continue; - } - CenterlineProjection projection = projectCenterline(centerline, x, z); - int pointIndex = projection.pointIndex(); - HydrologyPoint point = centerline.get(pointIndex); - boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1; - LayerShape shape = shapes[pointIndex]; - int deltaX = x - point.x(); - int deltaZ = z - point.z(); - double rawDistance = projection.distance(); - double rasterRadius = segment.type().isSurface() - ? shape.totalRadius() - : shape.channelRadius(); - if (rawDistance > rasterRadius + 0.25D) { - continue; - } - int flowX = projection.flowX(); - int flowZ = projection.flowZ(); - double channelDistance = shapedDistance( - shape, - segment, - x, - z, - x - projection.x(), - z - projection.z(), - flowX, - flowZ, - rawDistance - ); - boolean channel = channelDistance <= shape.channelRadius() + 0.25D; - if (!segment.type().isSurface() && !channel) { - continue; - } - boolean shore = !channel - && channelDistance <= shape.channelRadius() + shape.shoreWidth() + 0.25D; - boolean grading = !channel && !shore && rawDistance <= shape.totalRadius() + 0.25D; - if (!channel && !shore && !grading) { - continue; - } - double distance = grading ? rawDistance : channelDistance; - HydrologyRoutingTerrainSampler.NaturalClassification classification = classifyNatural(x, z); - boolean oceanConnection = (segment.type() == HydrologyFeatureType.MOUTH - || segment.type() == HydrologyFeatureType.COASTAL_GROTTO) - && pointIndex == centerline.size() - 1; - boolean apronEligible = oceanApronEligible(segment, distance); - if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - && !apronEligible) { - continue; - } - if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND - && oceanConnection) { - continue; - } - boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - && !oceanConnection - && !channel - && segment.type() != HydrologyFeatureType.WATERFALL - && segment.type() != HydrologyFeatureType.CASCADE; - HydrologyTerrainSample terrain = exactSlope ? sampleTerrain(x, z) : sampleTerrainBasis(x, z); - if (terrain == null) { - continue; - } - boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - || terrain.ocean() - || naturallySubmergedSurfaceColumn(segment, terrain); - if (oceanConnection || naturalOcean) { - if (naturalOcean && apronEligible) { - addOceanApron(columns, course, segment, point, x, z, distance, flowX, flowZ, - terrain, pointFeatures[pointIndex]); - } - continue; - } - boolean source = firstSegment && pointIndex == 0; - if (validationOnly && segment.type().isSurface()) { - feature(course, segment, x, shape.fluidHead(), z, flowX, flowZ, - source && deltaX == 0 && deltaZ == 0, channel, shore, grading, - false, false, pointFeatures[pointIndex]); - continue; - } - HydrologyColumnLayer layer = regularLayer( - course, - segment, - point, - shape, - terrain, - distance, - deltaX, - deltaZ, - flowX, - flowZ, - source, - channel, - shore, - grading, - false, - receiving, - pointFeatures[pointIndex] - ); - layer = terrainContainedCaveLayer(course, segment, terrain, layer, x, z, plannedSurface); - if (layer == null) { - continue; - } - addLayer(columns, x, z, terrain, layer); - } - } - } - - private CenterlineProjection projectCenterline(List centerline, int x, int z) { - if (centerline.size() == 1) { - HydrologyPoint point = centerline.getFirst(); - return new CenterlineProjection( - 0, - point.x(), - point.z(), - StrictMath.hypot(x - point.x(), z - point.z()), - 1, - 0 - ); - } - int selectedPoint = 0; - double selectedX = centerline.getFirst().x(); - double selectedZ = centerline.getFirst().z(); - double selectedDistanceSquared = Double.POSITIVE_INFINITY; - int selectedFlowX = 1; - int selectedFlowZ = 0; - for (int segmentIndex = 0; segmentIndex < centerline.size() - 1; segmentIndex++) { - HydrologyPoint start = centerline.get(segmentIndex); - HydrologyPoint end = centerline.get(segmentIndex + 1); - double flowX = end.x() - start.x(); - double flowZ = end.z() - start.z(); - double lengthSquared = flowX * flowX + flowZ * flowZ; - if (lengthSquared == 0D) { - continue; - } - double progress = ((x - start.x()) * flowX + (z - start.z()) * flowZ) / lengthSquared; - progress = Math.max(0D, Math.min(1D, progress)); - double projectedX = start.x() + flowX * progress; - double projectedZ = start.z() + flowZ * progress; - double deltaX = x - projectedX; - double deltaZ = z - projectedZ; - double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; - if (distanceSquared >= selectedDistanceSquared) { - continue; - } - selectedDistanceSquared = distanceSquared; - selectedX = projectedX; - selectedZ = projectedZ; - selectedPoint = progress < 0.5D ? segmentIndex : segmentIndex + 1; - selectedFlowX = end.x() - start.x(); - selectedFlowZ = end.z() - start.z(); - } - int tangentReach = Math.max(2, settings.routing().refinementSpacing() * 2); - HydrologyPoint tangentStart = centerline.get(Math.max(0, selectedPoint - tangentReach)); - HydrologyPoint tangentEnd = centerline.get(Math.min(centerline.size() - 1, selectedPoint + tangentReach)); - int tangentX = tangentEnd.x() - tangentStart.x(); - int tangentZ = tangentEnd.z() - tangentStart.z(); - if (tangentX != 0 || tangentZ != 0) { - selectedFlowX = tangentX; - selectedFlowZ = tangentZ; - } - return new CenterlineProjection( - selectedPoint, - selectedX, - selectedZ, - StrictMath.sqrt(selectedDistanceSquared), - selectedFlowX, - selectedFlowZ - ); - } - - private boolean withinLongitudinalBounds( - List centerline, - int x, - int z, - boolean clipStart, - boolean clipEnd - ) { - if (centerline.size() < 2) { - return true; - } - HydrologyPoint start = centerline.getFirst(); - HydrologyPoint afterStart = centerline.get(1); - long startDot = (long) (x - start.x()) * (afterStart.x() - start.x()) - + (long) (z - start.z()) * (afterStart.z() - start.z()); - if (clipStart && startDot < 0L) { - return false; - } - if (!clipEnd) { - return true; - } - HydrologyPoint end = centerline.getLast(); - HydrologyPoint beforeEnd = centerline.get(centerline.size() - 2); - long endDot = (long) (x - end.x()) * (end.x() - beforeEnd.x()) - + (long) (z - end.z()) * (end.z() - beforeEnd.z()); - return endDot <= 0L; - } - - private boolean segmentsJoin(HydraulicSegment upstream, HydraulicSegment downstream) { - HydrologyPoint upstreamEnd = upstream.centerline().getLast(); - HydrologyPoint downstreamStart = downstream.centerline().getFirst(); - return upstreamEnd.x() == downstreamStart.x() - && upstreamEnd.y() == downstreamStart.y() - && upstreamEnd.z() == downstreamStart.z(); - } - - private List continuousCenterline(HydraulicSegment segment) { - List configured = segment.centerline(); - if (configured.size() == 1) { - return configured; - } - ArrayList continuous = new ArrayList<>(); - for (int pairIndex = 0; pairIndex < configured.size() - 1; pairIndex++) { - HydrologyPoint start = configured.get(pairIndex); - HydrologyPoint end = configured.get(pairIndex + 1); - int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z())); - if (steps == 0) { - if (continuous.isEmpty()) { - continuous.add(start); - } - continue; - } - int firstStep = continuous.isEmpty() ? 0 : 1; - for (int step = firstStep; step <= steps; step++) { - double progress = step / (double) steps; - int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress); - int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress); - int y = segment.fallingFluid() && !continuous.isEmpty() - ? segment.downstreamHeadY() - : (int) StrictMath.round(start.y() + (end.y() - start.y()) * progress); - HydrologyPoint previous = continuous.isEmpty() ? null : continuous.getLast(); - if (previous == null || previous.x() != x || previous.z() != z) { - continuous.add(new HydrologyPoint(x, y, z)); - } - } - } - return List.copyOf(continuous); - } - - private LayerShape shape( - RiverCourse course, - HydraulicSegment segment, - HydrologyPoint point, - int bed, - int fluidHead, - boolean falling, - boolean receiving - ) { - HydrologyFeatureType type = segment.type(); - int channelRadius = Math.max(1, segment.width() / 2); - double shoreWidth = 0D; - double gradingWidth = 0D; - int ceiling = fluidHead; - boolean ellipsoid = false; - boolean archedChannel = false; - boolean roundedSurfaceBed = false; - boolean organicBoundary = type.isSurface() || type.isUnderground() || type.isDeepFluid() || falling || receiving; - if (course.type() == RiverCourseType.SURFACE && type.isSurface()) { - roundedSurfaceBed = !falling; - shoreWidth = settings.surface().shoreWidth(); - gradingWidth = geometrySampler.sample( - HydrologyGeometrySampler.Field.SURFACE_BLEND_WIDTH, - course.profileKey(), - point.x(), - point.z(), - segment.id(), - settings.surface().banks().minimumBlendWidth(), - settings.surface().banks().maximumBlendWidth() - ); - if (type == HydrologyFeatureType.WATERFALL) { - shoreWidth = Math.min(2.5D, shoreWidth); - gradingWidth = Math.min(gradingWidth, Math.max(8D, channelRadius * 4D)); - } - } - if (type == HydrologyFeatureType.UNDERGROUND_POOL - || type == HydrologyFeatureType.UNDERGROUND_DROP - || type == HydrologyFeatureType.SINKHOLE) { - ceiling = fluidHead + geometrySampler.sample( - HydrologyGeometrySampler.Field.UNDERGROUND_HEADROOM, - course.profileKey(), - point.x(), - point.z(), - course.id(), - settings.underground().minimumHeadroom(), - settings.underground().maximumHeadroom() - ); - archedChannel = !falling; - } else if (type == HydrologyFeatureType.COASTAL_GROTTO) { - HydrologyPlannerSettings.Grotto grotto = settings.outlets().coastalGrotto(); - channelRadius = grotto.horizontalRadius(); - bed = fluidHead - grotto.verticalRadius(); - ceiling = fluidHead + grotto.headroom(); - ellipsoid = true; - organicBoundary = true; - } else if (type == HydrologyFeatureType.INLAND_GROTTO) { - HydrologyPlannerSettings.Grotto grotto = settings.outlets().inlandGrotto(); - channelRadius = grotto.horizontalRadius(); - bed = fluidHead - grotto.verticalRadius(); - ceiling = fluidHead + grotto.headroom(); - ellipsoid = true; - organicBoundary = true; - } else if (type.isDeepFluid()) { - HydrologyPlannerSettings.DeepFluid deepFluid = deepFluids.get(course.profileKey()); - if (deepFluid == null) { - throw new IllegalStateException("Missing deep-fluid planner settings for " + course.profileKey() + "."); - } - if (type == HydrologyFeatureType.DEEP_POOL) { - channelRadius = Math.max(1, segment.width() / 2); - bed = fluidHead - Math.max(segment.depth(), deepFluid.minimumVerticalRadius()); - ellipsoid = true; - } else { - archedChannel = !falling; - } - ceiling = fluidHead + deepFluid.headroom(); - organicBoundary = true; - } - if (receiving) { - HydrologyPlannerSettings.Drops drops = settings.geometry().drops(); - if (course.type() != RiverCourseType.SURFACE || !type.isSurface()) { - channelRadius = Math.max( - channelRadius, - (int) StrictMath.ceil(drops.basinWidth(segment.width()) / 2D) - ); - } - bed = Math.min(bed, fluidHead - drops.basinDepth(segment.depth(), segment.drop())); - if (course.type() == RiverCourseType.SURFACE && type.isSurface()) { - gradingWidth = Math.max( - gradingWidth, - Math.min(settings.surface().banks().maximumBlendWidth(), channelRadius * 2D) - ); - } - } - if (type == HydrologyFeatureType.UNDERGROUND_POOL && segment.centerline().size() > 1) { - int depthVariation = Math.min( - 2, - Math.max(0, settings.underground().maximumDepth() - (fluidHead - bed)) - ); - bed -= organicVerticalVariation( - HydrologyHash.mix(segment.id(), ORGANIC_BED_VARIATION_SALT), - point.x(), - point.z(), - depthVariation - ); - } - if (falling) { - bed = Math.min(bed, segment.downstreamHeadY() - 1); - ceiling = Math.max(ceiling, segment.upstreamHeadY()); - } - return new LayerShape( - channelRadius, - shoreWidth, - gradingWidth, - bed, - fluidHead, - ceiling, - ellipsoid, - archedChannel, - roundedSurfaceBed, - organicBoundary, - falling - ); - } - - private int organicVerticalVariation( - long seed, - int x, - int z, - int maximum - ) { - if (maximum == 0) { - return 0; - } - double sampled = organicNoise(seed, x, z, 12); - return (int) StrictMath.round(sampled * maximum); - } - - private void rasterizePoint( - Long2ObjectLinkedOpenHashMap columns, - RiverCourse course, - HydraulicSegment segment, - HydrologyPoint point, - int pointIndex, - LayerShape shape, - int flowX, - int flowZ, - boolean source, - boolean falling, - boolean receiving, - boolean oceanConnection, - boolean validationOnly, - SurfaceRasterIndex plannedSurface - ) { - RasterStencil stencil = rasterStencil(shape); - HydrologyFeatureRef[] pointFeatures = new HydrologyFeatureRef[FEATURE_ROLE_COUNT]; - boolean underground = segment.type().isUnderground() || segment.type().isDeepFluid(); - for (int offsetIndex = 0; offsetIndex < stencil.size(); offsetIndex++) { - int deltaX = stencil.deltaXs()[offsetIndex]; - int deltaZ = stencil.deltaZs()[offsetIndex]; - int x = point.x() + deltaX; - int z = point.z() + deltaZ; - double channelDistance = shapedDistance( - shape, - segment, - x, - z, - deltaX, - deltaZ, - flowX, - flowZ, - stencil.distances()[offsetIndex] - ); - boolean channel = channelDistance <= shape.channelRadius() + 0.25D; - if (underground && !channel) { - continue; - } - boolean shore = !channel - && stencil.distances()[offsetIndex] <= shape.channelRadius() + shape.shoreWidth() + 0.25D; - boolean grading = !channel - && !shore - && stencil.distances()[offsetIndex] <= shape.totalRadius() + 0.25D; - double distance = grading ? stencil.distances()[offsetIndex] : channelDistance; - HydrologyRoutingTerrainSampler.NaturalClassification classification = classifyNatural(x, z); - boolean apronEligible = oceanApronEligible(segment, distance); - if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - && !apronEligible) { - continue; - } - if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND - && oceanConnection) { - continue; - } - boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - && !oceanConnection - && !channel - && segment.type() != HydrologyFeatureType.WATERFALL - && segment.type() != HydrologyFeatureType.CASCADE; - HydrologyTerrainSample terrain = exactSlope - ? sampleTerrain(x, z) - : sampleTerrainBasis(x, z); - if (terrain == null) { - continue; - } - boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - || terrain.ocean() - || naturallySubmergedSurfaceColumn(segment, terrain); - if (oceanConnection) { - if (naturalOcean) { - addOceanApron( - columns, - course, - segment, - point, - x, - z, - distance, - flowX, - flowZ, - terrain, - pointFeatures - ); - } - continue; - } - if (naturalOcean) { - addOceanApron( - columns, - course, - segment, - point, - x, - z, - distance, - flowX, - flowZ, - terrain, - pointFeatures - ); - continue; - } - if (validationOnly && !underground) { - feature( - course, - segment, - x, - shape.fluidHead(), - z, - flowX, - flowZ, - source && deltaX == 0 && deltaZ == 0, - channel, - shore, - grading, - channel && falling, - channel && receiving, - pointFeatures - ); - continue; - } - HydrologyColumnLayer layer = regularLayer( - course, - segment, - point, - shape, - terrain, - distance, - deltaX, - deltaZ, - flowX, - flowZ, - source, - channel, - shore, - grading, - falling, - receiving, - pointFeatures - ); - layer = terrainContainedCaveLayer(course, segment, terrain, layer, x, z, plannedSurface); - if (layer == null) { - continue; - } - addLayer(columns, x, z, terrain, layer); - if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) { - addAdjacentSeaApron(columns, course, segment, point, x, z, flowX, flowZ, pointFeatures); - } - } - } - - private HydrologyColumnLayer terrainContainedCaveLayer( + HydrologyColumnLayer terrainContainedCaveLayer( RiverCourse course, HydraulicSegment segment, HydrologyTerrainSample terrain, @@ -1062,7 +373,7 @@ final class HydrologyFootprintCompiler { return withCeiling(layer, maximumCeiling); } - private HydrologyColumnLayer withCeiling(HydrologyColumnLayer layer, int ceiling) { + HydrologyColumnLayer withCeiling(HydrologyColumnLayer layer, int ceiling) { return new HydrologyColumnLayer( layer.feature(), layer.bedY(), @@ -1086,11 +397,11 @@ final class HydrologyFootprintCompiler { ); } - private HydrologyColumnLayer regularLayer( + HydrologyColumnLayer regularLayer( RiverCourse course, HydraulicSegment segment, HydrologyPoint point, - LayerShape shape, + FootprintLayerShape shape, HydrologyTerrainSample terrain, double distance, int deltaX, @@ -1138,10 +449,10 @@ final class HydrologyFootprintCompiler { ); } - private HydrologyColumnLayer regularLayer( + HydrologyColumnLayer regularLayer( RiverCourse course, HydraulicSegment segment, - LayerShape shape, + FootprintLayerShape shape, HydrologyTerrainSample terrain, double distance, int worldX, @@ -1153,9 +464,9 @@ final class HydrologyFootprintCompiler { boolean receiving, HydrologyFeatureRef feature ) { - HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type()); + HydrologyPlannerSettings.ChannelShape channelShape = channelGeometry.channelShape(segment.type()); double ellipsoidScale = channel && (shape.ellipsoid() || shape.archedChannel()) - ? ellipsoidScale(shape.channelRadius(), distance, channelShape.bedRoundness()) + ? channelGeometry.ellipsoidScale(shape.channelRadius(), distance, channelShape.bedRoundness()) : 0D; int bed = resolvedBed( segment, @@ -1197,25 +508,25 @@ final class HydrologyFootprintCompiler { ); } - private RasterStencil rasterStencil(LayerShape shape) { - RasterStencilKey key = new RasterStencilKey( + FootprintRasterStencil rasterStencil(FootprintLayerShape shape) { + FootprintRasterStencilKey key = new FootprintRasterStencilKey( shape.channelRadius(), shape.shoreWidth(), shape.gradingWidth() ); - RasterStencil cached = rasterStencils.get(key); + FootprintRasterStencil cached = rasterStencils.get(key); if (cached != null) { return cached; } - RasterStencil stencil = buildRasterStencil(key); + FootprintRasterStencil stencil = buildRasterStencil(key); rasterStencils.put(key, stencil); return stencil; } - private RasterStencil buildRasterStencil(RasterStencilKey key) { + FootprintRasterStencil buildRasterStencil(FootprintRasterStencilKey key) { double totalRadius = key.channelRadius() + key.shoreWidth() + key.gradingWidth(); int blockRadius = (int) StrictMath.ceil(totalRadius); - RasterStencilBuilder builder = new RasterStencilBuilder(); + FootprintRasterStencilBuilder builder = new FootprintRasterStencilBuilder(); for (int deltaZ = -blockRadius; deltaZ <= blockRadius; deltaZ++) { for (int deltaX = -blockRadius; deltaX <= blockRadius; deltaX++) { double distance = StrictMath.hypot(deltaX, deltaZ); @@ -1234,8 +545,8 @@ final class HydrologyFootprintCompiler { return builder.build(); } - private void addOceanApron( - Long2ObjectLinkedOpenHashMap columns, + void addOceanApron( + Long2ObjectLinkedOpenHashMap columns, RiverCourse course, HydraulicSegment segment, HydrologyPoint point, @@ -1253,8 +564,8 @@ final class HydrologyFootprintCompiler { addOceanApronLayer(columns, course, segment, point, x, z, flowX, flowZ, terrain, pointFeatures); } - private void addOceanApronLayer( - Long2ObjectLinkedOpenHashMap columns, + void addOceanApronLayer( + Long2ObjectLinkedOpenHashMap columns, RiverCourse course, HydraulicSegment segment, HydrologyPoint point, @@ -1308,8 +619,8 @@ final class HydrologyFootprintCompiler { addLayer(columns, x, z, terrain, layer); } - private int gradedBed( - LayerShape shape, + int gradedBed( + FootprintLayerShape shape, HydrologyTerrainSample terrain, double distance ) { @@ -1319,16 +630,16 @@ final class HydrologyFootprintCompiler { } double bankDistance = Math.max(0D, distance - shape.channelRadius()); int targetBed = Math.addExact(shape.fluidHead(), 1); - double progress = smoothStep(Math.min(1D, bankDistance / transitionWidth)); + double progress = channelGeometry.smoothStep(Math.min(1D, bankDistance / transitionWidth)); int blendedBed = (int) StrictMath.round( targetBed + (terrain.naturalHeight() - targetBed) * progress ); return Math.min(terrain.naturalHeight(), blendedBed); } - private int resolvedBed( + int resolvedBed( HydraulicSegment segment, - LayerShape shape, + FootprintLayerShape shape, HydrologyPlannerSettings.ChannelShape channelShape, HydrologyTerrainSample terrain, double distance, @@ -1352,8 +663,8 @@ final class HydrologyFootprintCompiler { (normalized - thalwegFraction) / (1D - thalwegFraction) ); int maximumDepth = Math.max(1, shape.fluidHead() - shape.bed()); - double roughness = signedOrganicNoise( - HydrologyHash.mix(segment.id(), ORGANIC_BED_VARIATION_SALT), + double roughness = channelGeometry.signedOrganicNoise( + HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_BED_VARIATION_SALT), worldX, worldZ, channelShape.roughnessWavelength() @@ -1379,9 +690,9 @@ final class HydrologyFootprintCompiler { return Math.min(resolved, terrain.naturalHeight()); } - private int localBed( + int localBed( HydraulicSegment segment, - LayerShape shape, + FootprintLayerShape shape, HydrologyPlannerSettings.ChannelShape channelShape, double scale, int worldX, @@ -1391,8 +702,8 @@ final class HydrologyFootprintCompiler { return shape.bed(); } int lowerExtent = shape.fluidHead() - shape.bed(); - double roughness = signedOrganicNoise( - HydrologyHash.mix(segment.id(), ORGANIC_BED_VARIATION_SALT), + double roughness = channelGeometry.signedOrganicNoise( + HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_BED_VARIATION_SALT), worldX, worldZ, channelShape.roughnessWavelength() @@ -1408,8 +719,8 @@ final class HydrologyFootprintCompiler { return shape.fluidHead() - localExtent; } - private int localCeiling( - LayerShape shape, + int localCeiling( + FootprintLayerShape shape, double scale, HydraulicSegment segment, HydrologyPlannerSettings.ChannelShape channelShape, @@ -1422,8 +733,8 @@ final class HydrologyFootprintCompiler { int upperExtent = shape.ceiling() - shape.fluidHead(); double extent = upperExtent * scale; if (channelShape.ceilingRoughness() > 0D) { - double roughness = signedOrganicNoise( - HydrologyHash.mix(segment.id(), ORGANIC_CEILING_VARIATION_SALT), + double roughness = channelGeometry.signedOrganicNoise( + HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_CEILING_VARIATION_SALT), worldX, worldZ, channelShape.roughnessWavelength() @@ -1441,7 +752,7 @@ final class HydrologyFootprintCompiler { * Layers whose ceiling follows the terrain: the underground transition of a surface river, and the * chamber of a standalone sea cave, which must never break the surface above it. */ - private boolean terrainRoofedLayer(RiverCourse course, HydraulicSegment segment) { + boolean terrainRoofedLayer(RiverCourse course, HydraulicSegment segment) { if (course.type() == RiverCourseType.SEA_CAVE) { return segment.type() == HydrologyFeatureType.COASTAL_GROTTO; } @@ -1450,226 +761,23 @@ final class HydrologyFootprintCompiler { || segment.type() == HydrologyFeatureType.UNDERGROUND_DROP); } - private double ellipsoidScale(int radius, double distance, double roundness) { - double normalized = Math.min(1D, distance / Math.max(1D, radius)); - return StrictMath.pow( - Math.max(0D, 1D - StrictMath.pow(normalized, roundness)), - 1D / roundness - ); - } - - private double shapedDistance( - LayerShape shape, - HydraulicSegment segment, - int worldX, - int worldZ, - double deltaX, - double deltaZ, - int flowX, - int flowZ, - double rawDistance - ) { - if (segment.type() == HydrologyFeatureType.DEEP_POOL) { - return deepPoolDistance(shape, segment, worldX, worldZ, deltaX, deltaZ); - } - if (segment.type().isSurface() && !shape.fallingThroat()) { - return surfaceChannelDistance(shape, segment, worldX, worldZ, deltaX, deltaZ, flowX, flowZ); - } - if (rawDistance == 0D) { - return 0D; - } - double distance = rawDistance; - if (shape.fallingThroat() && (flowX != 0 || flowZ != 0)) { - double flowLength = StrictMath.hypot(flowX, flowZ); - double along = StrictMath.abs(deltaX * flowX + deltaZ * flowZ) / flowLength; - double across = StrictMath.abs(deltaX * -flowZ + deltaZ * flowX) / flowLength; - if (along > 0.75D) { - return shape.totalRadius() + 1D; - } - distance = across; - } - if (!shape.organicBoundary()) { - return distance; - } - HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type()); - if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO - || segment.type() == HydrologyFeatureType.INLAND_GROTTO) { - double orientation = HydrologyHash.unit(HydrologyHash.mix( - segment.courseId(), - ORGANIC_SHAPE_FIRST_PHASE_SALT - )) * StrictMath.PI * 2D; - double cosine = StrictMath.cos(orientation); - double sine = StrictMath.sin(orientation); - double rotatedX = deltaX * cosine + deltaZ * sine; - double rotatedZ = -deltaX * sine + deltaZ * cosine; - double aspect = channelShape.aspectMinimum() + HydrologyHash.unit(HydrologyHash.mix( - segment.courseId(), - ORGANIC_SHAPE_SECOND_PHASE_SALT - )) * channelShape.aspectRange(); - distance = StrictMath.hypot(rotatedX, rotatedZ / aspect); - } - double angle = StrictMath.atan2(deltaZ, deltaX); - double firstPhase = HydrologyHash.unit(HydrologyHash.mix( - segment.courseId(), - ORGANIC_SHAPE_FIRST_PHASE_SALT - )) * StrictMath.PI * 2D; - double secondPhase = HydrologyHash.unit(HydrologyHash.mix( - segment.courseId(), - ORGANIC_SHAPE_SECOND_PHASE_SALT - )) * StrictMath.PI * 2D; - double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase); - double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase); - double coherent = signedOrganicNoise( - segment.courseId(), - worldX, - worldZ, - channelShape.roughnessWavelength() - ); - double detail = signedOrganicNoise( - HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT), - worldX, - worldZ, - Math.max(3, channelShape.roughnessWavelength() / 2) - ); - double radialScale = channelShape.radialBase() - + (firstLobe - 0.5D) * channelShape.primaryLobeStrength() - + (secondLobe - 0.5D) * channelShape.detailLobeStrength() - + coherent * channelShape.wallRoughness() * 0.7D - + detail * channelShape.wallRoughness() * 0.3D; - radialScale = Math.max(channelShape.radialMinimum(), Math.min(channelShape.radialMaximum(), radialScale)); - return distance / radialScale; - } - - private double surfaceChannelDistance( - LayerShape shape, - HydraulicSegment segment, - int worldX, - int worldZ, - double deltaX, - double deltaZ, - int flowX, - int flowZ - ) { - double flowLength = StrictMath.hypot(flowX, flowZ); - if (flowLength == 0D) { - return deepPoolDistance(shape, segment, worldX, worldZ, deltaX, deltaZ); - } - double signedCross = (deltaX * -flowZ + deltaZ * flowX) / flowLength; - HydrologyPlannerSettings.ChannelShape channelShape = settings.geometry().surface(); - double thalweg = signedOrganicNoise( - HydrologyHash.mix(segment.courseId(), ORGANIC_BED_VARIATION_SALT), - worldX, - worldZ, - channelShape.roughnessWavelength() - ) * shape.channelRadius() * channelShape.wallRoughness(); - long bankSeed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT); - double bankNoise = signedOrganicNoise( - bankSeed, - worldX, - worldZ, - channelShape.roughnessWavelength() - ); - double sideBias = signedCross < thalweg - ? HydrologyHash.unit(HydrologyHash.mix(bankSeed, 1L)) - : HydrologyHash.unit(HydrologyHash.mix(bankSeed, 2L)); - double widthScale = 0.86D - + bankNoise * channelShape.wallRoughness() - + (sideBias - 0.5D) * 0.08D; - widthScale = Math.max(1D, Math.min(1.18D, widthScale)); - double organicDistance = StrictMath.abs(signedCross - thalweg) / widthScale; - return Math.min(StrictMath.abs(signedCross), organicDistance); - } - - private HydrologyPlannerSettings.ChannelShape channelShape(HydrologyFeatureType type) { - if (type.isGrotto() || type == HydrologyFeatureType.DEEP_POOL) { - return settings.geometry().grottos(); - } - if (type.isUnderground() || type == HydrologyFeatureType.DEEP_CHANNEL) { - return settings.geometry().underground(); - } - return settings.geometry().surface(); - } - - private double signedOrganicNoise(long seed, int x, int z, int scale) { - return organicNoise(seed, x, z, scale) * 2D - 1D; - } - - private double deepPoolDistance( - LayerShape shape, - HydraulicSegment segment, - int worldX, - int worldZ, - double deltaX, - double deltaZ - ) { - double radius = Math.max(1D, shape.channelRadius()); - long seed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_FIRST_PHASE_SALT); - double orientation = HydrologyHash.unit(seed) * StrictMath.PI * 2D; - double cosine = StrictMath.cos(orientation); - double sine = StrictMath.sin(orientation); - double rotatedX = deltaX * cosine + deltaZ * sine; - double rotatedZ = -deltaX * sine + deltaZ * cosine; - double angle = StrictMath.atan2(rotatedZ, rotatedX); - double secondPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 2L)) * StrictMath.PI * 2D; - double thirdPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 3L)) * StrictMath.PI * 2D; - double fifthPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 5L)) * StrictMath.PI * 2D; - double radialScale = 0.84D - + StrictMath.sin(angle * 2D + secondPhase) * 0.07D - + StrictMath.sin(angle * 3D + thirdPhase) * 0.05D - + StrictMath.sin(angle * 5D + fifthPhase) * 0.035D; - radialScale = Math.max(0.68D, Math.min(1D, radialScale)); - double normalizedDistance = StrictMath.hypot( - rotatedX / (radius * 0.92D), - rotatedZ / (radius * 0.7D) - ); - return normalizedDistance * radius / radialScale; - } - - private double organicNoise(long seed, int x, int z, int scale) { - int cellX = Math.floorDiv(x, scale); - int cellZ = Math.floorDiv(z, scale); - double localX = Math.floorMod(x, scale) / (double) scale; - double localZ = Math.floorMod(z, scale) / (double) scale; - double smoothX = localX * localX * (3D - 2D * localX); - double smoothZ = localZ * localZ * (3D - 2D * localZ); - double top = interpolate( - organicCorner(seed, cellX, cellZ), - organicCorner(seed, cellX + 1, cellZ), - smoothX - ); - double bottom = interpolate( - organicCorner(seed, cellX, cellZ + 1), - organicCorner(seed, cellX + 1, cellZ + 1), - smoothX - ); - return interpolate(top, bottom, smoothZ); - } - - private double organicCorner(long seed, int cellX, int cellZ) { - return HydrologyHash.unit(HydrologyHash.mix(seed, cellX, cellZ)); - } - - private double interpolate(double first, double second, double progress) { - return first + (second - first) * progress; - } - - private void addLayer( - Long2ObjectLinkedOpenHashMap columns, + void addLayer( + Long2ObjectLinkedOpenHashMap columns, int x, int z, HydrologyTerrainSample terrain, HydrologyColumnLayer layer ) { long packed = RiverFootprint.pack(x, z); - MutableColumn column = columns.get(packed); + FootprintMutableColumn column = columns.get(packed); if (column == null) { - column = new MutableColumn(x, z, terrain, settings.seaLevel()); + column = new FootprintMutableColumn(x, z, terrain, settings.seaLevel()); columns.put(packed, column); } column.add(layer); } - private HydrologyTerrainSample sampleTerrain(int x, int z) { + HydrologyTerrainSample sampleTerrain(int x, int z) { long packed = RiverFootprint.pack(x, z); HydrologyTerrainSample cached = terrainSamples.get(packed); if (cached != null) { @@ -1685,7 +793,7 @@ final class HydrologyFootprintCompiler { return sampled; } - private HydrologyTerrainSample sampleTerrainBasis(int x, int z) { + HydrologyTerrainSample sampleTerrainBasis(int x, int z) { if (naturalSampler == null) { return sampleTerrain(x, z); } @@ -1701,7 +809,7 @@ final class HydrologyFootprintCompiler { return sampled; } - private HydrologyRoutingTerrainSampler.NaturalClassification classifyNatural(int x, int z) { + HydrologyRoutingTerrainSampler.NaturalClassification classifyNatural(int x, int z) { if (naturalSampler == null) { return HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE; } @@ -1718,14 +826,14 @@ final class HydrologyFootprintCompiler { return sampled; } - private boolean naturallySubmergedSurfaceColumn( + boolean naturallySubmergedSurfaceColumn( HydraulicSegment segment, HydrologyTerrainSample terrain ) { return segment.type().isSurface() && terrain.naturalHeight() <= settings.seaLevel(); } - private boolean oceanApronEligible(HydraulicSegment segment, double distance) { + boolean oceanApronEligible(HydraulicSegment segment, double distance) { return (segment.type() == HydrologyFeatureType.MOUTH || segment.type() == HydrologyFeatureType.COASTAL_GROTTO) && distance <= apronReach(segment) + 0.25D; @@ -1736,7 +844,7 @@ final class HydrologyFootprintCompiler { * horizontal radius, so its apron reaches at least that far: every sea column the chamber touches * must carry the apron that declares the sea face an intentional opening. */ - private int apronReach(HydraulicSegment segment) { + int apronReach(HydraulicSegment segment) { int reach = settings.outlets().maximumOceanApron(); if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) { return Math.max(reach, settings.outlets().coastalGrotto().horizontalRadius()); @@ -1748,8 +856,8 @@ final class HydrologyFootprintCompiler { * The organic chamber boundary can put a sea column one block past the apron reach while the land * column beside it is still carved; the apron follows the chamber there so the face stays open. */ - private void addAdjacentSeaApron( - Long2ObjectLinkedOpenHashMap columns, + void addAdjacentSeaApron( + Long2ObjectLinkedOpenHashMap columns, RiverCourse course, HydraulicSegment segment, HydrologyPoint point, @@ -1785,7 +893,7 @@ final class HydrologyFootprintCompiler { } } - private HydrologyFeatureRef feature( + HydrologyFeatureRef feature( RiverCourse course, HydraulicSegment segment, int x, @@ -1808,7 +916,7 @@ final class HydrologyFootprintCompiler { } int featureFlowX = Integer.compare(flowX, 0); int featureFlowZ = Integer.compare(flowZ, 0); - FeatureKey key = new FeatureKey( + FootprintFeatureKey key = new FootprintFeatureKey( course.id(), segment.id(), segment.type(), @@ -1843,7 +951,7 @@ final class HydrologyFootprintCompiler { return feature; } - private int flowDelta(List centerline, int pointIndex, boolean xAxis) { + int flowDelta(List centerline, int pointIndex, boolean xAxis) { HydrologyPoint from; HydrologyPoint to; if (centerline.size() == 1) { @@ -1859,110 +967,6 @@ final class HydrologyFootprintCompiler { return Integer.compare(xAxis ? to.x() : to.z(), xAxis ? from.x() : from.z()); } - private record LayerShape( - int channelRadius, - double shoreWidth, - double gradingWidth, - int bed, - int fluidHead, - int ceiling, - boolean ellipsoid, - boolean archedChannel, - boolean roundedSurfaceBed, - boolean organicBoundary, - boolean fallingThroat - ) { - private double totalRadius() { - return channelRadius + shoreWidth + gradingWidth; - } - } - - private record CenterlineProjection( - int pointIndex, - double x, - double z, - double distance, - int flowX, - int flowZ - ) { - } - - private record ValidationCourseRaster( - long courseId, - List columns, - SurfaceFootprint surface - ) { - } - - private record FeatureKey( - long courseId, - long segmentId, - HydrologyFeatureType type, - int role, - int flowX, - int flowZ - ) { - } - - private record RasterStencilKey( - int channelRadius, - double shoreWidth, - double gradingWidth - ) { - } - - private record CourseRasterKey( - long courseId, - RiverCourseType courseType, - String profileKey, - List segments - ) { - } - - private record RasterStencil( - int[] deltaXs, - int[] deltaZs, - double[] distances - ) { - private int size() { - return deltaXs.length; - } - } - - private static final class RasterStencilBuilder { - private int[] deltaXs; - private int[] deltaZs; - private double[] distances; - private int size; - - private RasterStencilBuilder() { - this.deltaXs = new int[64]; - this.deltaZs = new int[64]; - this.distances = new double[64]; - } - - 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); - } - deltaXs[size] = deltaX; - deltaZs[size] = deltaZ; - distances[size] = distance; - size++; - } - - private RasterStencil build() { - return new RasterStencil( - Arrays.copyOf(deltaXs, size), - Arrays.copyOf(deltaZs, size), - Arrays.copyOf(distances, size) - ); - } - } - record Sampling( HydrologyTerrainSampler sampler, HydrologyGeometrySampler geometrySampler, @@ -1975,7 +979,7 @@ final class HydrologyFootprintCompiler { } final class ValidationRaster { - private final List courseRasters; + private final List courseRasters; private final Long2ObjectOpenHashMap> columnsByCourse; private final SurfaceRasterIndex surfaceRaster; private final RiverFootprint materializedSurface; @@ -1983,14 +987,14 @@ final class HydrologyFootprintCompiler { private List mergedColumns; private ValidationRaster( - List courseRasters, + List courseRasters, SurfaceRasterIndex surfaceRaster ) { this(courseRasters, surfaceRaster, null); } private ValidationRaster( - List courseRasters, + List courseRasters, SurfaceRasterIndex surfaceRaster, RiverFootprint materializedSurface ) { @@ -1999,7 +1003,7 @@ final class HydrologyFootprintCompiler { this.materializedSurface = materializedSurface; this.columnsByCourse = new Long2ObjectOpenHashMap<>(courseRasters.size()); int references = 0; - for (ValidationCourseRaster raster : courseRasters) { + for (FootprintValidationCourseRaster raster : courseRasters) { if (columnsByCourse.put(raster.courseId(), raster.columns()) != null) { throw new IllegalStateException("Duplicate hydrology validation course " + raster.courseId()); } @@ -2018,13 +1022,13 @@ final class HydrologyFootprintCompiler { if (cached != null) { return cached; } - Long2ObjectLinkedOpenHashMap merged = new Long2ObjectLinkedOpenHashMap<>(); - for (ValidationCourseRaster raster : courseRasters) { + Long2ObjectLinkedOpenHashMap merged = new Long2ObjectLinkedOpenHashMap<>(); + for (FootprintValidationCourseRaster raster : courseRasters) { for (HydrologyColumnSample sample : raster.columns()) { long packed = RiverFootprint.pack(sample.x(), sample.z()); - MutableColumn column = merged.get(packed); + FootprintMutableColumn column = merged.get(packed); if (column == null) { - merged.put(packed, new MutableColumn(sample)); + merged.put(packed, new FootprintMutableColumn(sample)); } else { column.merge(sample); } @@ -2100,7 +1104,7 @@ final class HydrologyFootprintCompiler { } } - private final class SurfaceRasterIndex implements HydrologyCaveVoxelViewFactory.PlannedSurface { + final class SurfaceRasterIndex implements HydrologyCaveVoxelViewFactory.PlannedSurface { private final Long2ObjectOpenHashMap surfaceColumns; private final Long2IntOpenHashMap validationNaturalHeights; @@ -2108,14 +1112,14 @@ final class HydrologyFootprintCompiler { List footprints, Long2ObjectOpenHashMap validationSamples ) { - Long2ObjectLinkedOpenHashMap merged = new Long2ObjectLinkedOpenHashMap<>(); + Long2ObjectLinkedOpenHashMap merged = new Long2ObjectLinkedOpenHashMap<>(); for (SurfaceFootprint footprint : footprints) { for (SurfaceLayerColumn column : footprint.columns()) { addLayer(merged, column.x(), column.z(), column.terrain(), column.layer()); } } this.surfaceColumns = new Long2ObjectOpenHashMap<>(merged.size()); - for (Long2ObjectMap.Entry entry : merged.long2ObjectEntrySet()) { + for (Long2ObjectMap.Entry entry : merged.long2ObjectEntrySet()) { surfaceColumns.put(entry.getLongKey(), entry.getValue().build()); } this.validationNaturalHeights = new Long2IntOpenHashMap(validationSamples.size()); @@ -2165,171 +1169,4 @@ final class HydrologyFootprintCompiler { return false; } } - - private static final class MutableColumn { - private final int x; - private final int z; - private final int naturalHeight; - private final int seaLevel; - private final boolean ocean; - private final String parentBiomeKey; - private HydrologyColumnLayer singleLayer; - private LinkedHashMap layers; - - private MutableColumn(int x, int z, HydrologyTerrainSample terrain, int seaLevel) { - this.x = x; - this.z = z; - this.naturalHeight = terrain.naturalHeight(); - this.seaLevel = seaLevel; - this.ocean = terrain.ocean(); - this.parentBiomeKey = terrain.parentBiomeKey(); - } - - private MutableColumn(HydrologyColumnSample sample) { - this.x = sample.x(); - this.z = sample.z(); - this.naturalHeight = sample.naturalHeight(); - this.seaLevel = sample.seaLevel(); - this.ocean = sample.ocean(); - this.parentBiomeKey = sample.parentBiomeKey(); - for (HydrologyColumnLayer layer : sample.layers()) { - add(layer); - } - } - - private void add(HydrologyColumnLayer layer) { - if (layers == null) { - if (singleLayer == null) { - singleLayer = layer; - return; - } - if (singleLayer.feature().id() == layer.feature().id()) { - singleLayer = merge(singleLayer, layer); - return; - } - layers = new LinkedHashMap<>(); - layers.put(singleLayer.feature().id(), singleLayer); - singleLayer = null; - } - HydrologyColumnLayer existing = layers.get(layer.feature().id()); - layers.put(layer.feature().id(), existing == null ? layer : merge(existing, layer)); - } - - private HydrologyColumnSample build() { - List builtLayers; - if (layers != null) { - builtLayers = resolveSurfaceHeadConflicts(new ArrayList<>(layers.values())); - } else if (singleLayer != null) { - builtLayers = List.of(singleLayer); - } else { - builtLayers = List.of(); - } - return new HydrologyColumnSample( - x, - z, - naturalHeight, - seaLevel, - ocean, - parentBiomeKey, - builtLayers - ); - } - - private List resolveSurfaceHeadConflicts(List candidates) { - HashMap selectedByCourse = new HashMap<>(); - for (HydrologyColumnLayer candidate : candidates) { - if (!conflictingSurfaceCandidate(candidate)) { - continue; - } - long courseId = candidate.feature().courseId(); - HydrologyColumnLayer selected = selectedByCourse.get(courseId); - if (selected == null || prefersSurfaceLayer(candidate, selected)) { - selectedByCourse.put(courseId, candidate); - } - } - if (selectedByCourse.isEmpty()) { - return candidates; - } - ArrayList resolved = new ArrayList<>(candidates.size()); - for (HydrologyColumnLayer candidate : candidates) { - HydrologyColumnLayer selected = selectedByCourse.get(candidate.feature().courseId()); - if (!conflictingSurfaceCandidate(candidate) - || selected == null - || candidate.fluidHeadY() == selected.fluidHeadY()) { - resolved.add(candidate); - } - } - return List.copyOf(resolved); - } - - private boolean conflictingSurfaceCandidate(HydrologyColumnLayer layer) { - return layer.feature().type().isSurface() - && layer.channel() - && layer.connectedFluid() - && layer.fluidOwned() - && !layer.fallingFluid(); - } - - private boolean prefersSurfaceLayer(HydrologyColumnLayer candidate, HydrologyColumnLayer selected) { - if (candidate.receivingPool() != selected.receivingPool()) { - return candidate.receivingPool(); - } - if (candidate.fluidHeadY() != selected.fluidHeadY()) { - return candidate.fluidHeadY() > selected.fluidHeadY(); - } - return candidate.feature().id() < selected.feature().id(); - } - - private void merge(HydrologyColumnSample sample) { - if (sample.x() != x - || sample.z() != z - || sample.naturalHeight() != naturalHeight - || sample.seaLevel() != seaLevel - || sample.ocean() != ocean - || !sample.parentBiomeKey().equals(parentBiomeKey)) { - throw new IllegalStateException("Hydrology course footprints disagree on terrain metadata at " - + x + "," + z + "."); - } - for (HydrologyColumnLayer layer : sample.layers()) { - add(layer); - } - } - - private HydrologyColumnLayer merge(HydrologyColumnLayer first, HydrologyColumnLayer second) { - if (first.equals(second)) { - return first; - } - HydrologyFeatureRef feature = first.feature().y() <= second.feature().y() - ? first.feature() - : second.feature(); - boolean drySurfaceBlend = first.feature().type().isSurface() - && second.feature().type().isSurface() - && !first.channel() - && !second.channel(); - int bedY = drySurfaceBlend - ? Math.max(first.bedY(), second.bedY()) - : Math.min(first.bedY(), second.bedY()); - return new HydrologyColumnLayer( - feature, - bedY, - Math.max(first.fluidHeadY(), second.fluidHeadY()), - Math.max(first.ceilingY(), second.ceilingY()), - first.channel() || second.channel(), - first.shore() || second.shore(), - first.grading() || second.grading(), - first.connectedFluid() || second.connectedFluid(), - first.fallingFluid() || second.fallingFluid(), - first.receivingPool() || second.receivingPool(), - first.terrainOwned() || second.terrainOwned(), - first.fluidOwned() || second.fluidOwned(), - first.oceanApron() || second.oceanApron(), - first.profileKey(), - first.surfaceBiomeKey(), - first.mouthBiomeKey(), - first.shoreBiomeKey(), - first.bankBiomeKey(), - first.floodedCaveBiomeKey() - ); - } - } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintRasterizer.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintRasterizer.java new file mode 100644 index 000000000..9b82a251f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintRasterizer.java @@ -0,0 +1,565 @@ +package art.arcane.iris.engine.hydrology; + +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; + +import java.util.ArrayList; +import java.util.List; + +final class HydrologyFootprintRasterizer { + private final HydrologyFootprintCompiler compiler; + + HydrologyFootprintRasterizer(HydrologyFootprintCompiler compiler) { + this.compiler = compiler; + } + + void rasterizeSegment( + Long2ObjectLinkedOpenHashMap columns, + RiverCourse course, + HydraulicSegment segment, + boolean firstSegment, + boolean clipStart, + boolean clipEnd, + boolean validationOnly, + HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface + ) { + List centerline = continuousCenterline(segment); + if (segment.fallingFluid() && centerline.size() > 1) { + HydrologyPoint throat = centerline.getFirst(); + int fluidHead = segment.upstreamHeadY(); + FootprintLayerShape throatShape = compiler.channelGeometry.shape( + course, + segment, + throat, + segment.downstreamHeadY() - 1, + fluidHead, + true, + false + ); + rasterizePoint( + columns, + course, + segment, + throat, + 0, + throatShape, + compiler.flowDelta(centerline, 0, true), + compiler.flowDelta(centerline, 0, false), + firstSegment, + true, + false, + false, + validationOnly, + plannedSurface + ); + rasterizeSweptSegment( + columns, + course, + segment, + List.copyOf(centerline.subList(1, centerline.size())), + false, + true, + clipEnd, + validationOnly, + plannedSurface + ); + return; + } + if (sweptChannel(segment, centerline)) { + rasterizeSweptSegment( + columns, + course, + segment, + centerline, + firstSegment, + clipStart, + clipEnd, + validationOnly, + plannedSurface + ); + return; + } + for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) { + HydrologyPoint point = centerline.get(pointIndex); + boolean oceanConnection = (segment.type() == HydrologyFeatureType.MOUTH + || segment.type() == HydrologyFeatureType.COASTAL_GROTTO) + && pointIndex == centerline.size() - 1; + boolean falling = segment.fallingFluid() && pointIndex == 0; + boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1; + int fluidHead = falling ? segment.upstreamHeadY() : point.y(); + int bed = falling + ? segment.downstreamHeadY() - 1 + : fluidHead - segment.depth(); + FootprintLayerShape shape = compiler.channelGeometry.shape(course, segment, point, bed, fluidHead, falling, receiving); + int flowX = compiler.flowDelta(centerline, pointIndex, true); + int flowZ = compiler.flowDelta(centerline, pointIndex, false); + rasterizePoint( + columns, + course, + segment, + point, + pointIndex, + shape, + flowX, + flowZ, + firstSegment && pointIndex == 0, + falling, + receiving, + oceanConnection, + validationOnly, + plannedSurface + ); + } + } + + boolean sweptChannel( + HydraulicSegment segment, + List centerline + ) { + if (centerline.size() < 2 || segment.fallingFluid()) { + return false; + } + return segment.type().isSurface() + || segment.type() == HydrologyFeatureType.UNDERGROUND_POOL + || segment.type() == HydrologyFeatureType.UNDERGROUND_DROP + || segment.type() == HydrologyFeatureType.SINKHOLE + || segment.type() == HydrologyFeatureType.DEEP_CHANNEL; + } + + void rasterizeSweptSegment( + Long2ObjectLinkedOpenHashMap columns, + RiverCourse course, + HydraulicSegment segment, + List centerline, + boolean firstSegment, + boolean clipStart, + boolean clipEnd, + boolean validationOnly, + HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface + ) { + FootprintLayerShape[] shapes = new FootprintLayerShape[centerline.size()]; + int maximumRadius = 1; + int minimumX = Integer.MAX_VALUE; + int maximumX = Integer.MIN_VALUE; + int minimumZ = Integer.MAX_VALUE; + int maximumZ = Integer.MIN_VALUE; + for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) { + HydrologyPoint point = centerline.get(pointIndex); + int fluidHead = point.y(); + boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1; + FootprintLayerShape shape = compiler.channelGeometry.shape( + course, + segment, + point, + fluidHead - segment.depth(), + fluidHead, + false, + receiving + ); + shapes[pointIndex] = shape; + int rasterRadius = segment.type().isSurface() + ? (int) StrictMath.ceil(shape.totalRadius()) + : shape.channelRadius(); + maximumRadius = Math.max(maximumRadius, rasterRadius); + minimumX = Math.min(minimumX, point.x()); + maximumX = Math.max(maximumX, point.x()); + minimumZ = Math.min(minimumZ, point.z()); + maximumZ = Math.max(maximumZ, point.z()); + } + minimumX = Math.subtractExact(minimumX, maximumRadius); + maximumX = Math.addExact(maximumX, maximumRadius); + minimumZ = Math.subtractExact(minimumZ, maximumRadius); + maximumZ = Math.addExact(maximumZ, maximumRadius); + HydrologyFeatureRef[][] pointFeatures = new HydrologyFeatureRef[centerline.size()][HydrologyFootprintCompiler.FEATURE_ROLE_COUNT]; + for (int z = minimumZ; z <= maximumZ; z++) { + for (int x = minimumX; x <= maximumX; x++) { + if (!withinLongitudinalBounds(centerline, x, z, clipStart, clipEnd)) { + continue; + } + FootprintCenterlineProjection projection = projectCenterline(centerline, x, z); + int pointIndex = projection.pointIndex(); + HydrologyPoint point = centerline.get(pointIndex); + boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1; + FootprintLayerShape shape = shapes[pointIndex]; + int deltaX = x - point.x(); + int deltaZ = z - point.z(); + double rawDistance = projection.distance(); + double rasterRadius = segment.type().isSurface() + ? shape.totalRadius() + : shape.channelRadius(); + if (rawDistance > rasterRadius + 0.25D) { + continue; + } + int flowX = projection.flowX(); + int flowZ = projection.flowZ(); + double channelDistance = compiler.channelGeometry.shapedDistance( + shape, + segment, + x, + z, + x - projection.x(), + z - projection.z(), + flowX, + flowZ, + rawDistance + ); + boolean channel = channelDistance <= shape.channelRadius() + 0.25D; + if (!segment.type().isSurface() && !channel) { + continue; + } + boolean shore = !channel + && channelDistance <= shape.channelRadius() + shape.shoreWidth() + 0.25D; + boolean grading = !channel && !shore && rawDistance <= shape.totalRadius() + 0.25D; + if (!channel && !shore && !grading) { + continue; + } + double distance = grading ? rawDistance : channelDistance; + HydrologyRoutingTerrainSampler.NaturalClassification classification = compiler.classifyNatural(x, z); + boolean oceanConnection = (segment.type() == HydrologyFeatureType.MOUTH + || segment.type() == HydrologyFeatureType.COASTAL_GROTTO) + && pointIndex == centerline.size() - 1; + boolean apronEligible = compiler.oceanApronEligible(segment, distance); + if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN + && !apronEligible) { + continue; + } + if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND + && oceanConnection) { + continue; + } + boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN + && !oceanConnection + && !channel + && segment.type() != HydrologyFeatureType.WATERFALL + && segment.type() != HydrologyFeatureType.CASCADE; + HydrologyTerrainSample terrain = exactSlope ? compiler.sampleTerrain(x, z) : compiler.sampleTerrainBasis(x, z); + if (terrain == null) { + continue; + } + boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN + || terrain.ocean() + || compiler.naturallySubmergedSurfaceColumn(segment, terrain); + if (oceanConnection || naturalOcean) { + if (naturalOcean && apronEligible) { + compiler.addOceanApron(columns, course, segment, point, x, z, distance, flowX, flowZ, + terrain, pointFeatures[pointIndex]); + } + continue; + } + boolean source = firstSegment && pointIndex == 0; + if (validationOnly && segment.type().isSurface()) { + compiler.feature(course, segment, x, shape.fluidHead(), z, flowX, flowZ, + source && deltaX == 0 && deltaZ == 0, channel, shore, grading, + false, false, pointFeatures[pointIndex]); + continue; + } + HydrologyColumnLayer layer = compiler.regularLayer( + course, + segment, + point, + shape, + terrain, + distance, + deltaX, + deltaZ, + flowX, + flowZ, + source, + channel, + shore, + grading, + false, + receiving, + pointFeatures[pointIndex] + ); + layer = compiler.terrainContainedCaveLayer(course, segment, terrain, layer, x, z, plannedSurface); + if (layer == null) { + continue; + } + compiler.addLayer(columns, x, z, terrain, layer); + } + } + } + + FootprintCenterlineProjection projectCenterline(List centerline, int x, int z) { + if (centerline.size() == 1) { + HydrologyPoint point = centerline.getFirst(); + return new FootprintCenterlineProjection( + 0, + point.x(), + point.z(), + StrictMath.hypot(x - point.x(), z - point.z()), + 1, + 0 + ); + } + int selectedPoint = 0; + double selectedX = centerline.getFirst().x(); + double selectedZ = centerline.getFirst().z(); + double selectedDistanceSquared = Double.POSITIVE_INFINITY; + int selectedFlowX = 1; + int selectedFlowZ = 0; + for (int segmentIndex = 0; segmentIndex < centerline.size() - 1; segmentIndex++) { + HydrologyPoint start = centerline.get(segmentIndex); + HydrologyPoint end = centerline.get(segmentIndex + 1); + double flowX = end.x() - start.x(); + double flowZ = end.z() - start.z(); + double lengthSquared = flowX * flowX + flowZ * flowZ; + if (lengthSquared == 0D) { + continue; + } + double progress = ((x - start.x()) * flowX + (z - start.z()) * flowZ) / lengthSquared; + progress = Math.max(0D, Math.min(1D, progress)); + double projectedX = start.x() + flowX * progress; + double projectedZ = start.z() + flowZ * progress; + double deltaX = x - projectedX; + double deltaZ = z - projectedZ; + double distanceSquared = deltaX * deltaX + deltaZ * deltaZ; + if (distanceSquared >= selectedDistanceSquared) { + continue; + } + selectedDistanceSquared = distanceSquared; + selectedX = projectedX; + selectedZ = projectedZ; + selectedPoint = progress < 0.5D ? segmentIndex : segmentIndex + 1; + selectedFlowX = end.x() - start.x(); + selectedFlowZ = end.z() - start.z(); + } + int tangentReach = Math.max(2, compiler.settings.routing().refinementSpacing() * 2); + HydrologyPoint tangentStart = centerline.get(Math.max(0, selectedPoint - tangentReach)); + HydrologyPoint tangentEnd = centerline.get(Math.min(centerline.size() - 1, selectedPoint + tangentReach)); + int tangentX = tangentEnd.x() - tangentStart.x(); + int tangentZ = tangentEnd.z() - tangentStart.z(); + if (tangentX != 0 || tangentZ != 0) { + selectedFlowX = tangentX; + selectedFlowZ = tangentZ; + } + return new FootprintCenterlineProjection( + selectedPoint, + selectedX, + selectedZ, + StrictMath.sqrt(selectedDistanceSquared), + selectedFlowX, + selectedFlowZ + ); + } + + boolean withinLongitudinalBounds( + List centerline, + int x, + int z, + boolean clipStart, + boolean clipEnd + ) { + if (centerline.size() < 2) { + return true; + } + HydrologyPoint start = centerline.getFirst(); + HydrologyPoint afterStart = centerline.get(1); + long startDot = (long) (x - start.x()) * (afterStart.x() - start.x()) + + (long) (z - start.z()) * (afterStart.z() - start.z()); + if (clipStart && startDot < 0L) { + return false; + } + if (!clipEnd) { + return true; + } + HydrologyPoint end = centerline.getLast(); + HydrologyPoint beforeEnd = centerline.get(centerline.size() - 2); + long endDot = (long) (x - end.x()) * (end.x() - beforeEnd.x()) + + (long) (z - end.z()) * (end.z() - beforeEnd.z()); + return endDot <= 0L; + } + + boolean segmentsJoin(HydraulicSegment upstream, HydraulicSegment downstream) { + HydrologyPoint upstreamEnd = upstream.centerline().getLast(); + HydrologyPoint downstreamStart = downstream.centerline().getFirst(); + return upstreamEnd.x() == downstreamStart.x() + && upstreamEnd.y() == downstreamStart.y() + && upstreamEnd.z() == downstreamStart.z(); + } + + List continuousCenterline(HydraulicSegment segment) { + List configured = segment.centerline(); + if (configured.size() == 1) { + return configured; + } + ArrayList continuous = new ArrayList<>(); + for (int pairIndex = 0; pairIndex < configured.size() - 1; pairIndex++) { + HydrologyPoint start = configured.get(pairIndex); + HydrologyPoint end = configured.get(pairIndex + 1); + int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z())); + if (steps == 0) { + if (continuous.isEmpty()) { + continuous.add(start); + } + continue; + } + int firstStep = continuous.isEmpty() ? 0 : 1; + for (int step = firstStep; step <= steps; step++) { + double progress = step / (double) steps; + int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress); + int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress); + int y = segment.fallingFluid() && !continuous.isEmpty() + ? segment.downstreamHeadY() + : (int) StrictMath.round(start.y() + (end.y() - start.y()) * progress); + HydrologyPoint previous = continuous.isEmpty() ? null : continuous.getLast(); + if (previous == null || previous.x() != x || previous.z() != z) { + continuous.add(new HydrologyPoint(x, y, z)); + } + } + } + return List.copyOf(continuous); + } + + void rasterizePoint( + Long2ObjectLinkedOpenHashMap columns, + RiverCourse course, + HydraulicSegment segment, + HydrologyPoint point, + int pointIndex, + FootprintLayerShape shape, + int flowX, + int flowZ, + boolean source, + boolean falling, + boolean receiving, + boolean oceanConnection, + boolean validationOnly, + HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface + ) { + FootprintRasterStencil stencil = compiler.rasterStencil(shape); + HydrologyFeatureRef[] pointFeatures = new HydrologyFeatureRef[HydrologyFootprintCompiler.FEATURE_ROLE_COUNT]; + boolean underground = segment.type().isUnderground() || segment.type().isDeepFluid(); + for (int offsetIndex = 0; offsetIndex < stencil.size(); offsetIndex++) { + int deltaX = stencil.deltaXs()[offsetIndex]; + int deltaZ = stencil.deltaZs()[offsetIndex]; + int x = point.x() + deltaX; + int z = point.z() + deltaZ; + double channelDistance = compiler.channelGeometry.shapedDistance( + shape, + segment, + x, + z, + deltaX, + deltaZ, + flowX, + flowZ, + stencil.distances()[offsetIndex] + ); + boolean channel = channelDistance <= shape.channelRadius() + 0.25D; + if (underground && !channel) { + continue; + } + boolean shore = !channel + && stencil.distances()[offsetIndex] <= shape.channelRadius() + shape.shoreWidth() + 0.25D; + boolean grading = !channel + && !shore + && stencil.distances()[offsetIndex] <= shape.totalRadius() + 0.25D; + double distance = grading ? stencil.distances()[offsetIndex] : channelDistance; + HydrologyRoutingTerrainSampler.NaturalClassification classification = compiler.classifyNatural(x, z); + boolean apronEligible = compiler.oceanApronEligible(segment, distance); + if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN + && !apronEligible) { + continue; + } + if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND + && oceanConnection) { + continue; + } + boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN + && !oceanConnection + && !channel + && segment.type() != HydrologyFeatureType.WATERFALL + && segment.type() != HydrologyFeatureType.CASCADE; + HydrologyTerrainSample terrain = exactSlope + ? compiler.sampleTerrain(x, z) + : compiler.sampleTerrainBasis(x, z); + if (terrain == null) { + continue; + } + boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN + || terrain.ocean() + || compiler.naturallySubmergedSurfaceColumn(segment, terrain); + if (oceanConnection) { + if (naturalOcean) { + compiler.addOceanApron( + columns, + course, + segment, + point, + x, + z, + distance, + flowX, + flowZ, + terrain, + pointFeatures + ); + } + continue; + } + if (naturalOcean) { + compiler.addOceanApron( + columns, + course, + segment, + point, + x, + z, + distance, + flowX, + flowZ, + terrain, + pointFeatures + ); + continue; + } + if (validationOnly && !underground) { + compiler.feature( + course, + segment, + x, + shape.fluidHead(), + z, + flowX, + flowZ, + source && deltaX == 0 && deltaZ == 0, + channel, + shore, + grading, + channel && falling, + channel && receiving, + pointFeatures + ); + continue; + } + HydrologyColumnLayer layer = compiler.regularLayer( + course, + segment, + point, + shape, + terrain, + distance, + deltaX, + deltaZ, + flowX, + flowZ, + source, + channel, + shore, + grading, + falling, + receiving, + pointFeatures + ); + layer = compiler.terrainContainedCaveLayer(course, segment, terrain, layer, x, z, plannedSurface); + if (layer == null) { + continue; + } + compiler.addLayer(columns, x, z, terrain, layer); + if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) { + compiler.addAdjacentSeaApron(columns, course, segment, point, x, z, flowX, flowZ, pointFeatures); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/SourceSelection.java b/core/src/main/java/art/arcane/iris/engine/hydrology/SourceSelection.java index 8a9751a36..53fd811b6 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/SourceSelection.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/SourceSelection.java @@ -9,13 +9,13 @@ final class SourceSelection { final boolean surface; final List candidates; final SourceAdmissionSelection admission; - final int guaranteedMinimum; + private final int guaranteedMinimum; final int maximumOptionalRejections; final int targetCount; final ArrayList selectedCandidateIndices; final boolean[] selectedCandidates; - final boolean[] attemptedCandidates; - int rejectedOptionalCandidates; + private final boolean[] attemptedCandidates; + private int rejectedOptionalCandidates; SourceSelection( boolean surface, @@ -130,7 +130,7 @@ final class SourceSelection { return changed; } - Set acceptedSourceNodeIds(List acceptedCourses) { + private Set acceptedSourceNodeIds(List acceptedCourses) { RiverCourseType expectedType = surface ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND; HashSet accepted = new HashSet<>(); for (RiverCourse course : acceptedCourses) { @@ -141,7 +141,7 @@ final class SourceSelection { return accepted; } - int nextRequiredCandidate() { + private int nextRequiredCandidate() { for (int candidateIndex = 0; candidateIndex < candidates.size(); candidateIndex++) { SourceCandidate candidate = candidates.get(candidateIndex); if (candidate.required() @@ -154,7 +154,7 @@ final class SourceSelection { return -1; } - int nextAdmittedCandidate() { + private int nextAdmittedCandidate() { if (rejectedOptionalCandidates >= maximumOptionalRejections) { return -1; } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveBoundaryResult.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveBoundaryResult.java new file mode 100644 index 000000000..8cc36cf57 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveBoundaryResult.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +record CaveBoundaryResult(Set sealGuards, HydrologyCaveRejection rejection) { + static CaveBoundaryResult accepted(Set sealGuards) { + return new CaveBoundaryResult( + Collections.unmodifiableSet(new LinkedHashSet<>(sealGuards)), + HydrologyCaveRejection.NONE + ); + } + + static CaveBoundaryResult rejected(HydrologyCaveRejection rejection) { + return new CaveBoundaryResult(Set.of(), rejection); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveCachedValidation.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveCachedValidation.java new file mode 100644 index 000000000..fe39b1d37 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveCachedValidation.java @@ -0,0 +1,14 @@ +package art.arcane.iris.engine.hydrology.cave; + +record CaveCachedValidation( + HydrologyCavePlan plan, + CaveViewObservations viewObservations, + CaveSurfaceObservations surfaceObservations +) { + long retainedPositions() { + return Math.max(1L, (long) plan.actions().size() + + plan.baselinePreconditions().size() + + viewObservations.size() + + surfaceObservations.observations().size()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveClaimGroup.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveClaimGroup.java new file mode 100644 index 000000000..e0b30f82d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveClaimGroup.java @@ -0,0 +1,9 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.Map; + +record CaveClaimGroup( + HydrologyCaveCandidate candidate, + Map actions +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveComponentResult.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveComponentResult.java new file mode 100644 index 000000000..2f566d433 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveComponentResult.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +record CaveComponentResult(Set positions, HydrologyCaveRejection rejection) { + static CaveComponentResult accepted(Set positions) { + return new CaveComponentResult( + Collections.unmodifiableSet(new LinkedHashSet<>(positions)), + HydrologyCaveRejection.NONE + ); + } + + static CaveComponentResult rejected(HydrologyCaveRejection rejection) { + return new CaveComponentResult(Set.of(), rejection); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveGrottoResult.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveGrottoResult.java new file mode 100644 index 000000000..0ff858204 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveGrottoResult.java @@ -0,0 +1,18 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +record CaveGrottoResult(Set positions, HydrologyCaveRejection rejection) { + static CaveGrottoResult accepted(Set positions) { + return new CaveGrottoResult( + Collections.unmodifiableSet(new LinkedHashSet<>(positions)), + HydrologyCaveRejection.NONE + ); + } + + static CaveGrottoResult rejected(HydrologyCaveRejection rejection) { + return new CaveGrottoResult(Set.of(), rejection); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveIndependentDecision.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveIndependentDecision.java new file mode 100644 index 000000000..8558aa951 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveIndependentDecision.java @@ -0,0 +1,16 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.List; + +record CaveIndependentDecision( + HydrologyCaveRejection rejection, + List guards +) { + private static CaveIndependentDecision accepted(List guards) { + return new CaveIndependentDecision(HydrologyCaveRejection.NONE, List.copyOf(guards)); + } + + private static CaveIndependentDecision rejected(HydrologyCaveRejection rejection) { + return new CaveIndependentDecision(rejection, List.of()); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CavePathResult.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CavePathResult.java new file mode 100644 index 000000000..77148114d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CavePathResult.java @@ -0,0 +1,13 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.List; + +record CavePathResult(List positions, HydrologyCaveRejection rejection) { + static CavePathResult accepted(List positions) { + return new CavePathResult(List.copyOf(positions), HydrologyCaveRejection.NONE); + } + + static CavePathResult rejected(HydrologyCaveRejection rejection) { + return new CavePathResult(List.of(), rejection); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveSurfaceObservations.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveSurfaceObservations.java new file mode 100644 index 000000000..cde81b26d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveSurfaceObservations.java @@ -0,0 +1,40 @@ +package art.arcane.iris.engine.hydrology.cave; + +import art.arcane.iris.engine.hydrology.HydrologyObservedPlannedSurface; + +import java.util.ArrayList; +import java.util.List; + +record CaveSurfaceObservations( + List observations +) { + static CaveSurfaceObservations empty() { + return new CaveSurfaceObservations(List.of()); + } + + static CaveSurfaceObservations capture( + CaveViewObservations viewObservations, + HydrologyObservedPlannedSurface plannedSurface + ) { + ArrayList observations = new ArrayList<>(); + for (int index = 0; index < viewObservations.size(); index++) { + CavePosition position = viewObservations.positions()[index]; + observations.addAll(plannedSurface.observationsAt(position.x(), position.z())); + } + return new CaveSurfaceObservations(List.copyOf(observations)); + } + + boolean matches(HydrologyObservedPlannedSurface plannedSurface) { + for (HydrologyObservedPlannedSurface.Observation observation : observations) { + int resolvedHeight = plannedSurface.resolve( + observation.x(), + observation.z(), + observation.naturalHeight() + ); + if (resolvedHeight != observation.resolvedHeight()) { + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveValidationBatch.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveValidationBatch.java new file mode 100644 index 000000000..5d3813bda --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveValidationBatch.java @@ -0,0 +1,11 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.List; +import java.util.Map; + +record CaveValidationBatch( + List plans, + Map actions, + Map baselinePreconditions +) { +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveViewObservations.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveViewObservations.java new file mode 100644 index 000000000..116c71b13 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/CaveViewObservations.java @@ -0,0 +1,30 @@ +package art.arcane.iris.engine.hydrology.cave; + +record CaveViewObservations( + CavePosition[] positions, + byte[] operations, + byte[] values, + int size +) { + static CaveViewObservations empty() { + return new CaveViewObservations(new CavePosition[0], new byte[0], new byte[0], 0); + } + + boolean matches(CaveVoxelView view) { + for (int index = 0; index < size; index++) { + CavePosition position = positions[index]; + int actual = switch (operations[index]) { + case RecordingCaveVoxelView.IN_WORLD -> view.isInWorld(position) ? 1 : 0; + case RecordingCaveVoxelView.VOXEL -> view.voxelAt(position).ordinal(); + case RecordingCaveVoxelView.OPEN_TO_SURFACE -> view.isOpenToSurface(position) ? 1 : 0; + case RecordingCaveVoxelView.ABOVE_TERRAIN_SURFACE -> + view.isAboveTerrainSurface(position) ? 1 : 0; + default -> throw new IllegalStateException("Unknown cave view operation."); + }; + if (actual != Byte.toUnsignedInt(values[index])) { + return false; + } + } + return true; + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveChamberPlanner.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveChamberPlanner.java new file mode 100644 index 000000000..5eb7eb854 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveChamberPlanner.java @@ -0,0 +1,809 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +final class HydrologyCaveChamberPlanner { + private final HydrologyCaveContainmentPlanner planner; + + HydrologyCaveChamberPlanner(HydrologyCaveContainmentPlanner planner) { + this.planner = planner; + } + + HydrologyCavePlan planGrottoOrClosedComponent( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + CaveVoxel targetVoxel = planner.voxelAt(view, source.target()); + if (planner.isFluidReachable(targetVoxel, settings)) { + return planClosedComponent(view, source, settings, throat); + } + if (targetVoxel == CaveVoxel.LAVA) { + return planner.rejected(source, HydrologyCaveRejection.LAVA_CONTACT); + } + if (targetVoxel == CaveVoxel.INCOMPATIBLE_FLUID) { + return planner.rejected(source, HydrologyCaveRejection.INCOMPATIBLE_FLUID); + } + return planGeneratedGrotto(view, source, settings, throat); + } + + HydrologyCavePlan planWaterfallPool( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + if (!view.isOpenToSurface(source.target())) { + return planGrottoOrClosedComponent(view, source, settings, throat); + } + HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat); + if (dryThroatRejection != HydrologyCaveRejection.NONE) { + return planner.rejected(source, dryThroatRejection); + } + HydrologyCaveRejection shaftRejection = validateWaterfallShaft(view, source, settings, throat); + if (shaftRejection != HydrologyCaveRejection.NONE) { + return planner.rejected(source, shaftRejection); + } + CaveVoxel targetVoxel = planner.voxelAt(view, source.target()); + if (!planner.isFluidReachable(targetVoxel, settings)) { + return planner.rejected(source, planner.rejectionForTarget(targetVoxel, settings)); + } + + Map actions = new LinkedHashMap<>(); + addThroatActions(actions, throat, source); + addSealGuards(view, source, actions); + return planner.accepted(view, source, actions); + } + + HydrologyCavePlan planClosedComponent( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat); + if (dryThroatRejection != HydrologyCaveRejection.NONE) { + return planner.rejected(source, dryThroatRejection); + } + HydrologyCaveRejection waterfallRejection = validateWaterfallShaft(view, source, settings, throat); + if (waterfallRejection != HydrologyCaveRejection.NONE) { + return planner.rejected(source, waterfallRejection); + } + + CaveVoxel targetVoxel = planner.voxelAt(view, source.target()); + if (!planner.isFluidReachable(targetVoxel, settings)) { + return planner.rejected(source, planner.rejectionForTarget(targetVoxel, settings)); + } + + CaveComponentResult component = resolveClosedComponent(view, source, settings, throat); + if (component.rejection() != HydrologyCaveRejection.NONE) { + return planner.rejected(source, component.rejection()); + } + + Map actions = new LinkedHashMap<>(); + addThroatActions(actions, throat, source); + for (CavePosition position : component.positions()) { + actions.put(position, HydrologyCaveAction.WET_SOURCE); + } + addSealGuards(view, source, actions); + return planner.accepted(view, source, actions); + } + + HydrologyCaveRejection validateDryThroatContacts( + CaveVoxelView view, + HydrologyCaveSource source, + List throat + ) { + Set throatPositions = Set.copyOf(throat); + for (CavePosition position : throat) { + if (position.y() <= source.waterHeadY()) { + continue; + } + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) { + continue; + } + if (!view.isInWorld(neighbor)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + CaveVoxel voxel = planner.voxelAt(view, neighbor); + if (voxel == CaveVoxel.LAVA) { + return HydrologyCaveRejection.LAVA_CONTACT; + } + if (voxel == CaveVoxel.COMPATIBLE_FLUID) { + return HydrologyCaveRejection.EXISTING_FLUID; + } + if (voxel == CaveVoxel.INCOMPATIBLE_FLUID) { + return HydrologyCaveRejection.INCOMPATIBLE_FLUID; + } + } + } + return HydrologyCaveRejection.NONE; + } + + HydrologyCaveRejection validateWaterfallShaft( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + if (source.mode() != HydrologyCaveMode.WATERFALL_POOL) { + return HydrologyCaveRejection.NONE; + } + + Set throatPositions = Set.copyOf(throat); + for (CavePosition position : throat) { + if (position.y() <= source.waterHeadY()) { + continue; + } + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) { + continue; + } + if (!view.isInWorld(neighbor)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return boundsRejection; + } + CaveVoxel voxel = planner.voxelAt(view, neighbor); + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return hazard; + } + if (voxel != CaveVoxel.SOLID) { + return HydrologyCaveRejection.WATERFALL_SHAFT_OPEN; + } + } + } + return HydrologyCaveRejection.NONE; + } + + HydrologyCavePlan planGeneratedGrotto( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + CaveGrottoResult grotto = buildGrotto(source, settings); + if (grotto.rejection() != HydrologyCaveRejection.NONE) { + return planner.rejected(source, grotto.rejection()); + } + Set chamber = grotto.positions(); + + Set carve = new LinkedHashSet<>(chamber.size() + throat.size()); + carve.addAll(chamber); + carve.addAll(throat); + HydrologyCaveRejection carveRejection = validateGeneratedCarve(view, source, settings, carve); + if (carveRejection != HydrologyCaveRejection.NONE) { + return planner.rejected(source, carveRejection); + } + + CaveBoundaryResult boundary = validateGeneratedBoundary(view, source, settings, carve); + if (boundary.rejection() != HydrologyCaveRejection.NONE) { + return planner.rejected(source, boundary.rejection()); + } + + Map actions = new LinkedHashMap<>(); + addChamberActions(actions, chamber, source.waterHeadY()); + addThroatActions(actions, throat, source); + for (CavePosition position : boundary.sealGuards()) { + actions.put(position, HydrologyCaveAction.SEAL_GUARD); + } + return planner.accepted(view, source, actions); + } + + HydrologyCavePlan planDeepPool( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + CaveGrottoResult grotto = buildGrotto(source, settings); + if (grotto.rejection() != HydrologyCaveRejection.NONE) { + return planner.rejected(source, grotto.rejection()); + } + Set chamber = grotto.positions(); + Set carve = new LinkedHashSet<>(chamber.size() + throat.size()); + carve.addAll(chamber); + carve.addAll(throat); + + HydrologyCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve); + if (carveRejection != HydrologyCaveRejection.NONE) { + return planner.rejected(source, carveRejection); + } + CaveBoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve); + if (boundary.rejection() != HydrologyCaveRejection.NONE) { + return planner.rejected(source, boundary.rejection()); + } + + Map actions = new LinkedHashMap<>(); + addChamberActions(actions, chamber, source.waterHeadY()); + addThroatActions(actions, throat, source); + for (CavePosition position : boundary.sealGuards()) { + actions.put(position, HydrologyCaveAction.SEAL_GUARD); + } + return planner.accepted(view, source, actions); + } + + HydrologyCaveRejection validateDeepPoolCarve( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + Set carve + ) { + for (CavePosition position : carve) { + if (!view.isInWorld(position)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return boundsRejection; + } + CaveVoxel voxel = planner.voxelAt(view, position); + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return hazard; + } + if (voxel == CaveVoxel.SOLID) { + continue; + } + if (position.y() > source.waterHeadY() + && voxel == CaveVoxel.CAVE_AIR + && !view.isOpenToSurface(position)) { + continue; + } + return HydrologyCaveRejection.GROTTO_INTERSECTION; + } + return HydrologyCaveRejection.NONE; + } + + CaveBoundaryResult validateDeepPoolBoundary( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + Set carve + ) { + Set guards = new LinkedHashSet<>(); + for (CavePosition position : carve) { + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + if (carve.contains(neighbor)) { + continue; + } + if (!view.isInWorld(neighbor)) { + return CaveBoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY); + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return CaveBoundaryResult.rejected(boundsRejection); + } + CaveVoxel voxel = planner.voxelAt(view, neighbor); + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return CaveBoundaryResult.rejected(hazard); + } + if (voxel == CaveVoxel.SOLID) { + guards.add(neighbor); + continue; + } + if (neighbor.y() > source.waterHeadY() + && voxel == CaveVoxel.CAVE_AIR + && !view.isOpenToSurface(neighbor)) { + continue; + } + return CaveBoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN); + } + } + return CaveBoundaryResult.accepted(guards); + } + + CaveComponentResult resolveClosedComponent( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat + ) { + Queue queue = new ArrayDeque<>(); + Set queued = new LinkedHashSet<>(); + int visitedCount = 0; + + queue.add(source.target()); + queued.add(source.target()); + HydrologyCaveRejection seedRejection = addThroatContacts(view, source, settings, throat, queue, queued); + if (seedRejection != HydrologyCaveRejection.NONE) { + return CaveComponentResult.rejected(seedRejection); + } + + while (!queue.isEmpty()) { + CavePosition position = queue.remove(); + HydrologyCaveRejection positionRejection = validateReachablePosition(view, source, settings, position); + if (positionRejection != HydrologyCaveRejection.NONE) { + return CaveComponentResult.rejected(positionRejection); + } + if (++visitedCount > settings.maxFloodVolume()) { + return CaveComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); + } + + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + HydrologyCaveRejection neighborRejection = inspectReachableNeighbor( + view, + source, + settings, + neighbor, + queue, + queued + ); + if (neighborRejection != HydrologyCaveRejection.NONE) { + return CaveComponentResult.rejected(neighborRejection); + } + } + } + + return CaveComponentResult.accepted(queued); + } + + HydrologyCaveRejection addThroatContacts( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List throat, + Queue queue, + Set queued + ) { + for (CavePosition position : throat) { + if (position.y() > source.waterHeadY()) { + continue; + } + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + HydrologyCaveRejection rejection = inspectReachableNeighbor( + view, + source, + settings, + neighbor, + queue, + queued + ); + if (rejection != HydrologyCaveRejection.NONE) { + return rejection; + } + } + } + return HydrologyCaveRejection.NONE; + } + + HydrologyCaveRejection inspectReachableNeighbor( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + CavePosition position, + Queue queue, + Set queued + ) { + if (position.y() > source.waterHeadY()) { + return inspectAboveHeadNeighbor(view, source, position); + } + if (!view.isInWorld(position)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + + CaveVoxel voxel = planner.voxelAt(view, position); + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return hazard; + } + if (!planner.isFluidReachable(voxel, settings)) { + return HydrologyCaveRejection.NONE; + } + + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return boundsRejection; + } + if (queued.add(position)) { + queue.add(position); + } + return HydrologyCaveRejection.NONE; + } + + HydrologyCaveRejection inspectAboveHeadNeighbor( + CaveVoxelView view, + HydrologyCaveSource source, + CavePosition position + ) { + if (isInletOpening(source, position) || !view.isInWorld(position)) { + return HydrologyCaveRejection.NONE; + } + CaveVoxel voxel = planner.voxelAt(view, position); + return switch (voxel) { + case LAVA -> HydrologyCaveRejection.LAVA_CONTACT; + case COMPATIBLE_FLUID -> HydrologyCaveRejection.EXISTING_FLUID; + case INCOMPATIBLE_FLUID -> HydrologyCaveRejection.INCOMPATIBLE_FLUID; + default -> HydrologyCaveRejection.NONE; + }; + } + + HydrologyCaveRejection validateReachablePosition( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + CavePosition position + ) { + if (!view.isInWorld(position)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return boundsRejection; + } + if (view.isOpenToSurface(position)) { + return HydrologyCaveRejection.OPEN_SURFACE; + } + CaveVoxel voxel = planner.voxelAt(view, position); + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return hazard; + } + return planner.isFluidReachable(voxel, settings) + ? HydrologyCaveRejection.NONE + : HydrologyCaveRejection.NO_CAVE_TARGET; + } + + CavePathResult buildThroat( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings + ) { + CavePosition entry = source.entry(); + CavePosition target = source.target(); + int deltaX = target.x() - entry.x(); + int deltaY = target.y() - entry.y(); + int deltaZ = target.z() - entry.z(); + int movesX = Math.abs(deltaX); + int movesY = Math.abs(deltaY); + int movesZ = Math.abs(deltaZ); + int length = movesX + movesY + movesZ; + if (length > settings.maxThroatLength()) { + return CavePathResult.rejected(HydrologyCaveRejection.THROAT_LIMIT); + } + + int stepX = Integer.signum(deltaX); + int stepY = Integer.signum(deltaY); + int stepZ = Integer.signum(deltaZ); + int usedX = 0; + int usedY = 0; + int usedZ = 0; + CavePosition current = entry; + List positions = new ArrayList<>(length + 1); + + while (true) { + HydrologyCaveRejection positionRejection = validateThroatPosition(view, source, settings, current); + if (positionRejection != HydrologyCaveRejection.NONE) { + return CavePathResult.rejected(positionRejection); + } + positions.add(current); + if (current.equals(target)) { + return expandThroat(view, source, settings, positions); + } + + int axis = selectNextAxis(source.sourceId(), movesX, movesY, movesZ, usedX, usedY, usedZ); + if (axis == 0) { + current = current.offset(stepX, 0, 0); + usedX++; + } else if (axis == 1) { + current = current.offset(0, stepY, 0); + usedY++; + } else { + current = current.offset(0, 0, stepZ); + usedZ++; + } + } + } + + CavePathResult expandThroat( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + List centerline + ) { + int radius = settings.throatRadius(); + int extent = radius - 1; + int radiusSquared = radius * radius; + Set expanded = new LinkedHashSet<>(); + for (CavePosition center : centerline) { + for (int dx = -extent; dx <= extent; dx++) { + for (int dy = -extent; dy <= extent; dy++) { + for (int dz = -extent; dz <= extent; dz++) { + if ((dx * dx) + (dy * dy) + (dz * dz) >= radiusSquared) { + continue; + } + CavePosition position = center.offset(dx, dy, dz); + if (position.y() > source.entry().y()) { + continue; + } + HydrologyCaveRejection rejection = validateThroatPosition(view, source, settings, position); + if (rejection != HydrologyCaveRejection.NONE) { + return CavePathResult.rejected(rejection); + } + expanded.add(position); + if (expanded.size() > settings.maxFloodVolume()) { + return CavePathResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); + } + } + } + } + } + return CavePathResult.accepted(List.copyOf(expanded)); + } + + int selectNextAxis( + long sourceId, + int movesX, + int movesY, + int movesZ, + int usedX, + int usedY, + int usedZ + ) { + double scoreX = nextAxisScore(movesX, usedX); + double scoreY = nextAxisScore(movesY, usedY); + double scoreZ = nextAxisScore(movesZ, usedZ); + double minimum = Math.min(scoreX, Math.min(scoreY, scoreZ)); + int tieOffset = Math.floorMod(sourceId, 3); + for (int offset = 0; offset < 3; offset++) { + int axis = (tieOffset + offset) % 3; + double score = axis == 0 ? scoreX : axis == 1 ? scoreY : scoreZ; + if (score == minimum) { + return axis; + } + } + throw new IllegalStateException("No remaining throat axis"); + } + + double nextAxisScore(int moves, int used) { + if (used >= moves) { + return Double.POSITIVE_INFINITY; + } + return ((2D * used) + 1D) / moves; + } + + HydrologyCaveRejection validateThroatPosition( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + CavePosition position + ) { + if (!view.isInWorld(position)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return boundsRejection; + } + return planner.rejectionForHazard(planner.voxelAt(view, position), settings); + } + + CaveGrottoResult buildGrotto(HydrologyCaveSource source, HydrologyCavePlannerSettings settings) { + int horizontalRadius = settings.grottoHorizontalRadius(); + int verticalRadius = settings.grottoVerticalRadius(); + Set candidates = new LinkedHashSet<>(); + + for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) { + for (int dy = -verticalRadius; dy <= verticalRadius; dy++) { + for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) { + if (settings.grottoShape().contains(source, settings, dx, dy, dz)) { + candidates.add(source.target().offset(dx, dy, dz)); + if (candidates.size() > settings.maxFloodVolume()) { + return CaveGrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); + } + } + } + } + } + candidates.add(source.target()); + for (int offset = 1; offset <= settings.dryHeadroom(); offset++) { + CavePosition headroom = new CavePosition( + source.target().x(), source.waterHeadY() + offset, source.target().z()); + if (Math.abs(headroom.y() - source.target().y()) > verticalRadius) { + return CaveGrottoResult.rejected(HydrologyCaveRejection.DRY_HEADROOM_LIMIT); + } + candidates.add(headroom); + if (candidates.size() > settings.maxFloodVolume()) { + return CaveGrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); + } + } + return CaveGrottoResult.accepted(connectedGrotto(source.target(), candidates)); + } + + Set connectedGrotto(CavePosition target, Set candidates) { + Queue queue = new ArrayDeque<>(); + Set connected = new LinkedHashSet<>(); + queue.add(target); + connected.add(target); + while (!queue.isEmpty()) { + CavePosition position = queue.remove(); + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + if (candidates.contains(neighbor) && connected.add(neighbor)) { + queue.add(neighbor); + } + } + } + return connected; + } + + HydrologyCaveRejection validateGeneratedCarve( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + Set carve + ) { + for (CavePosition position : carve) { + if (!view.isInWorld(position)) { + return HydrologyCaveRejection.WORLD_BOUNDARY; + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, position); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return boundsRejection; + } + CaveVoxel voxel = planner.voxelAt(view, position); + if (isGeneratedInletCarve(view, source, settings, position, voxel)) { + continue; + } + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return hazard; + } + if (voxel != CaveVoxel.SOLID) { + return HydrologyCaveRejection.GROTTO_INTERSECTION; + } + } + return HydrologyCaveRejection.NONE; + } + + CaveBoundaryResult validateGeneratedBoundary( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + Set carve + ) { + Set guards = new LinkedHashSet<>(); + for (CavePosition position : carve) { + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + if (carve.contains(neighbor)) { + continue; + } + if (isInletOpening(source, neighbor) + || isGeneratedInletOpening(view, source, settings, neighbor)) { + continue; + } + if (!view.isInWorld(neighbor)) { + return CaveBoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY); + } + HydrologyCaveRejection boundsRejection = planner.validateBounds(source, settings, neighbor); + if (boundsRejection != HydrologyCaveRejection.NONE) { + return CaveBoundaryResult.rejected(boundsRejection); + } + CaveVoxel voxel = planner.voxelAt(view, neighbor); + HydrologyCaveRejection hazard = planner.rejectionForHazard(voxel, settings); + if (hazard != HydrologyCaveRejection.NONE) { + return CaveBoundaryResult.rejected(hazard); + } + if (voxel != CaveVoxel.SOLID) { + return CaveBoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN); + } + guards.add(neighbor); + } + } + return CaveBoundaryResult.accepted(guards); + } + + void addChamberActions( + Map actions, + Collection positions, + int waterHeadY + ) { + for (CavePosition position : positions) { + HydrologyCaveAction action = position.y() <= waterHeadY + ? HydrologyCaveAction.WET_SOURCE + : HydrologyCaveAction.DRY_AIR; + actions.put(position, action); + } + } + + void addThroatActions( + Map actions, + Collection throat, + HydrologyCaveSource source + ) { + for (CavePosition position : throat) { + HydrologyCaveAction action; + if (position.y() <= source.waterHeadY()) { + action = HydrologyCaveAction.WET_SOURCE; + } else if (source.mode() == HydrologyCaveMode.WATERFALL_POOL + || source.mode() == HydrologyCaveMode.GENERATED_GROTTO) { + action = HydrologyCaveAction.FALLING_FLUID; + } else { + action = HydrologyCaveAction.DRY_AIR; + } + actions.put(position, action); + } + } + + void addSealGuards( + CaveVoxelView view, + HydrologyCaveSource source, + Map actions + ) { + Set guards = new LinkedHashSet<>(); + for (CavePosition position : List.copyOf(actions.keySet())) { + for (CavePosition direction : HydrologyCaveContainmentPlanner.DIRECTIONS) { + CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); + if (actions.containsKey(neighbor) + || isInletOpening(source, neighbor) + || !view.isInWorld(neighbor)) { + continue; + } + if (planner.voxelAt(view, neighbor) == CaveVoxel.SOLID) { + guards.add(neighbor); + } + } + } + for (CavePosition guard : guards) { + actions.put(guard, HydrologyCaveAction.SEAL_GUARD); + } + } + + boolean isInletOpening(HydrologyCaveSource source, CavePosition position) { + return position.equals(source.entry().offset(0, 1, 0)); + } + + boolean isGeneratedInletCarve( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + CavePosition position, + CaveVoxel voxel + ) { + if (voxel != CaveVoxel.CAVE_AIR && voxel != CaveVoxel.COMPATIBLE_FLUID) { + return false; + } + int extent = Math.max(0, settings.throatRadius() - 1); + long deltaX = (long) position.x() - source.entry().x(); + long deltaZ = (long) position.z() - source.entry().z(); + return position.y() >= source.entry().y() - extent + && position.y() <= source.entry().y() + && deltaX * deltaX + deltaZ * deltaZ <= (long) extent * extent + && view.isOpenToSurface(position); + } + + boolean isGeneratedInletOpening( + CaveVoxelView view, + HydrologyCaveSource source, + HydrologyCavePlannerSettings settings, + CavePosition position + ) { + int radius = Math.max(1, settings.throatRadius()); + long deltaX = (long) position.x() - source.entry().x(); + long deltaZ = (long) position.z() - source.entry().z(); + return position.y() >= source.entry().y() + && position.y() <= source.entry().y() + 1 + && deltaX * deltaX + deltaZ * deltaZ < (long) radius * radius + && view.isOpenToSurface(position); + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveContainmentPlanner.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveContainmentPlanner.java index e1d067d21..139677dd4 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveContainmentPlanner.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/HydrologyCaveContainmentPlanner.java @@ -2,27 +2,23 @@ package art.arcane.iris.engine.hydrology.cave; import art.arcane.iris.engine.hydrology.HydrologyObservedPlannedSurface; -import java.util.ArrayDeque; 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.IdentityHashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.OptionalLong; -import java.util.Queue; import java.util.Set; import java.util.function.BiPredicate; public final class HydrologyCaveContainmentPlanner { - private static final int MAXIMUM_INITIAL_GUARD_INDEX_SIZE = 16_384; - private static final List DIRECTIONS = List.of( + static final int MAXIMUM_INITIAL_GUARD_INDEX_SIZE = 16_384; + + static final List DIRECTIONS = List.of( new CavePosition(1, 0, 0), new CavePosition(-1, 0, 0), new CavePosition(0, 1, 0), @@ -30,8 +26,14 @@ public final class HydrologyCaveContainmentPlanner { new CavePosition(0, 0, 1), new CavePosition(0, 0, -1) ); - private static final Comparator SOURCE_PRIORITY = + + static final Comparator SOURCE_PRIORITY = HydrologyCaveConflictPolicy.sourcePriority(); + final HydrologyCaveChamberPlanner chambers; + + public HydrologyCaveContainmentPlanner() { + this.chambers = new HydrologyCaveChamberPlanner(this); + } public HydrologyCavePlan plan( CaveVoxelView view, @@ -47,27 +49,27 @@ public final class HydrologyCaveContainmentPlanner { return rejected(source, sourceRejection); } - PathResult throat = buildThroat(view, source, settings); + CavePathResult throat = chambers.buildThroat(view, source, settings); if (throat.rejection() != HydrologyCaveRejection.NONE) { return rejected(source, throat.rejection()); } return switch (source.mode()) { - case CLOSED_COMPONENT -> planClosedComponent(view, source, settings, throat.positions()); - case GENERATED_GROTTO -> planGeneratedGrotto(view, source, settings, throat.positions()); - case GROTTO_OR_CLOSED_COMPONENT -> planGrottoOrClosedComponent( + case CLOSED_COMPONENT -> chambers.planClosedComponent(view, source, settings, throat.positions()); + case GENERATED_GROTTO -> chambers.planGeneratedGrotto(view, source, settings, throat.positions()); + case GROTTO_OR_CLOSED_COMPONENT -> chambers.planGrottoOrClosedComponent( view, source, settings, throat.positions() ); - case WATERFALL_POOL -> planWaterfallPool( + case WATERFALL_POOL -> chambers.planWaterfallPool( view, source, settings, throat.positions() ); - case DEEP_POOL -> planDeepPool(view, source, settings, throat.positions()); + case DEEP_POOL -> chambers.planDeepPool(view, source, settings, throat.positions()); }; } @@ -133,7 +135,7 @@ public final class HydrologyCaveContainmentPlanner { ValidationCache validationCache, HydrologyObservedPlannedSurface plannedSurface ) { - ValidationBatch batch = validateBatch( + CaveValidationBatch batch = validateBatch( view, candidates, validationCache, @@ -203,7 +205,7 @@ public final class HydrologyCaveContainmentPlanner { ).plans(); } - private ValidationBatch validateBatch( + CaveValidationBatch validateBatch( CaveVoxelView view, Collection candidates, ValidationCache validationCache, @@ -252,26 +254,19 @@ public final class HydrologyCaveContainmentPlanner { claims.putIfAbsent(position, claimGroup)); } - return new ValidationBatch( + return new CaveValidationBatch( List.copyOf(plans), combinedActions, combinedPreconditions ); } - private record ValidationBatch( - List plans, - Map actions, - Map baselinePreconditions - ) { - } - public static final class ValidationCache { private static final int DEFAULT_MAXIMUM_ENTRIES = 256; private static final long DEFAULT_MAXIMUM_RETAINED_POSITIONS = 262_144L; - private final IdentityHashMap validations; - private final HashMap equivalentValidations; + private final IdentityHashMap validations; + private final HashMap equivalentValidations; private final int maximumEntries; private final long maximumRetainedPositions; private long retainedPositions; @@ -302,7 +297,7 @@ public final class HydrologyCaveContainmentPlanner { HydrologyObservedPlannedSurface plannedSurface, boolean exposureValidated ) { - CachedValidation cached = validations.get(candidate); + CaveCachedValidation cached = validations.get(candidate); if (cached == null) { cached = equivalentValidations.get(candidate); if (cached != null) { @@ -323,14 +318,14 @@ public final class HydrologyCaveContainmentPlanner { boolean observeColumnsOnly = plannedSurface != null; RecordingCaveVoxelView recording = new RecordingCaveVoxelView(view, observeColumnsOnly); HydrologyCavePlan plan = planner.validate(recording, candidate, exposureValidated); - ViewObservations viewObservations = recording.snapshot(); - SurfaceObservations surfaceObservations = plannedSurface == null - ? SurfaceObservations.empty() - : SurfaceObservations.capture(viewObservations, plannedSurface); + CaveViewObservations viewObservations = recording.snapshot(); + CaveSurfaceObservations surfaceObservations = plannedSurface == null + ? CaveSurfaceObservations.empty() + : CaveSurfaceObservations.capture(viewObservations, plannedSurface); if (observeColumnsOnly) { - viewObservations = ViewObservations.empty(); + viewObservations = CaveViewObservations.empty(); } - CachedValidation validation = new CachedValidation(plan, viewObservations, surfaceObservations); + CaveCachedValidation validation = new CaveCachedValidation(plan, viewObservations, surfaceObservations); retain(candidate, validation); return plan; } @@ -345,7 +340,7 @@ public final class HydrologyCaveContainmentPlanner { misses = 0L; } - private void retain(HydrologyCaveCandidate candidate, CachedValidation validation) { + private void retain(HydrologyCaveCandidate candidate, CaveCachedValidation validation) { long weight = validation.retainedPositions(); if (weight > maximumRetainedPositions) { return; @@ -359,12 +354,12 @@ public final class HydrologyCaveContainmentPlanner { retainedPositions += weight; } - private void remove(CachedValidation cached) { + private void remove(CaveCachedValidation cached) { validations.entrySet().removeIf( - (Map.Entry entry) -> entry.getValue() == cached + (Map.Entry entry) -> entry.getValue() == cached ); equivalentValidations.entrySet().removeIf( - (Map.Entry entry) -> entry.getValue() == cached + (Map.Entry entry) -> entry.getValue() == cached ); retainedPositions -= cached.retainedPositions(); } @@ -380,181 +375,11 @@ public final class HydrologyCaveContainmentPlanner { } } - private static final class RecordingCaveVoxelView implements CaveVoxelView { - private static final byte IN_WORLD = 0; - private static final byte VOXEL = 1; - private static final byte OPEN_TO_SURFACE = 2; - private static final byte ABOVE_TERRAIN_SURFACE = 3; - - private final CaveVoxelView delegate; - private final boolean observeColumnsOnly; - private CavePosition[] positions; - private byte[] operations; - private byte[] values; - private CavePositionIndex[] observed; - private int size; - - private RecordingCaveVoxelView(CaveVoxelView delegate, boolean observeColumnsOnly) { - this.delegate = delegate; - this.observeColumnsOnly = observeColumnsOnly; - this.positions = new CavePosition[1024]; - this.operations = observeColumnsOnly ? new byte[0] : new byte[1024]; - this.values = observeColumnsOnly ? new byte[0] : new byte[1024]; - this.observed = observeColumnsOnly - ? new CavePositionIndex[]{new CavePositionIndex()} - : new CavePositionIndex[]{ - new CavePositionIndex(), - new CavePositionIndex(), - new CavePositionIndex(), - new CavePositionIndex() - }; - } - - @Override - public boolean isInWorld(CavePosition position) { - boolean result = delegate.isInWorld(position); - add(position, IN_WORLD, result ? 1 : 0); - return result; - } - - @Override - public CaveVoxel voxelAt(CavePosition position) { - CaveVoxel result = delegate.voxelAt(position); - add(position, VOXEL, result.ordinal()); - return result; - } - - @Override - public boolean isOpenToSurface(CavePosition position) { - boolean result = delegate.isOpenToSurface(position); - add(position, OPEN_TO_SURFACE, result ? 1 : 0); - return result; - } - - @Override - public boolean isAboveTerrainSurface(CavePosition position) { - boolean result = delegate.isAboveTerrainSurface(position); - add(position, ABOVE_TERRAIN_SURFACE, result ? 1 : 0); - return result; - } - - private void add(CavePosition position, byte operation, int value) { - int observedIndex = observeColumnsOnly ? 0 : operation; - int observedY = observeColumnsOnly ? 0 : position.y(); - if (!observed[observedIndex].add(position.x(), observedY, position.z())) { - return; - } - if (size == positions.length) { - int expandedSize = Math.multiplyExact(size, 2); - positions = Arrays.copyOf(positions, expandedSize); - if (!observeColumnsOnly) { - operations = Arrays.copyOf(operations, expandedSize); - values = Arrays.copyOf(values, expandedSize); - } - } - positions[size] = position; - if (!observeColumnsOnly) { - operations[size] = operation; - values[size] = (byte) value; - } - size++; - } - - private ViewObservations snapshot() { - observed = null; - if (positions.length == 1024) { - return new ViewObservations( - Arrays.copyOf(positions, size), - observeColumnsOnly ? operations : Arrays.copyOf(operations, size), - observeColumnsOnly ? values : Arrays.copyOf(values, size), - size - ); - } - return new ViewObservations(positions, operations, values, size); - } - } - - private record CachedValidation( - HydrologyCavePlan plan, - ViewObservations viewObservations, - SurfaceObservations surfaceObservations - ) { - private long retainedPositions() { - return Math.max(1L, (long) plan.actions().size() - + plan.baselinePreconditions().size() - + viewObservations.size() - + surfaceObservations.observations().size()); - } - } - - private record SurfaceObservations( - List observations - ) { - private static SurfaceObservations empty() { - return new SurfaceObservations(List.of()); - } - - private static SurfaceObservations capture( - ViewObservations viewObservations, - HydrologyObservedPlannedSurface plannedSurface - ) { - ArrayList observations = new ArrayList<>(); - for (int index = 0; index < viewObservations.size(); index++) { - CavePosition position = viewObservations.positions()[index]; - observations.addAll(plannedSurface.observationsAt(position.x(), position.z())); - } - return new SurfaceObservations(List.copyOf(observations)); - } - - private boolean matches(HydrologyObservedPlannedSurface plannedSurface) { - for (HydrologyObservedPlannedSurface.Observation observation : observations) { - int resolvedHeight = plannedSurface.resolve( - observation.x(), - observation.z(), - observation.naturalHeight() - ); - if (resolvedHeight != observation.resolvedHeight()) { - return false; - } - } - return true; - } - } - - private record ViewObservations( - CavePosition[] positions, - byte[] operations, - byte[] values, - int size - ) { - private static ViewObservations empty() { - return new ViewObservations(new CavePosition[0], new byte[0], new byte[0], 0); - } - - private boolean matches(CaveVoxelView view) { - for (int index = 0; index < size; index++) { - CavePosition position = positions[index]; - int actual = switch (operations[index]) { - case RecordingCaveVoxelView.IN_WORLD -> view.isInWorld(position) ? 1 : 0; - case RecordingCaveVoxelView.VOXEL -> view.voxelAt(position).ordinal(); - case RecordingCaveVoxelView.OPEN_TO_SURFACE -> view.isOpenToSurface(position) ? 1 : 0; - case RecordingCaveVoxelView.ABOVE_TERRAIN_SURFACE -> - view.isAboveTerrainSurface(position) ? 1 : 0; - default -> throw new IllegalStateException("Unknown cave view operation."); - }; - if (actual != Byte.toUnsignedInt(values[index])) { - return false; - } - } - return true; - } - } - public HydrologyCavePlan validate(CaveVoxelView view, HydrologyCaveCandidate candidate) { return validate(view, candidate, false); } - private HydrologyCavePlan validate( + HydrologyCavePlan validate( CaveVoxelView view, HydrologyCaveCandidate candidate, boolean exposureValidated @@ -619,7 +444,7 @@ public final class HydrologyCaveContainmentPlanner { return accepted(source, actions, preconditions); } - private boolean exceedsFloodVolume( + boolean exceedsFloodVolume( Map actions, int maximumFloodVolume ) { @@ -635,7 +460,7 @@ public final class HydrologyCaveContainmentPlanner { return false; } - private boolean eligibleForTerrainSurfaceCheck( + boolean eligibleForTerrainSurfaceCheck( CaveVoxelView view, HydrologyCaveSource source, HydrologyCavePlannerSettings settings, @@ -645,7 +470,7 @@ public final class HydrologyCaveContainmentPlanner { && validateBounds(source, settings, position) == HydrologyCaveRejection.NONE; } - private boolean isPlannedVolumeAboveTerrainSurface( + boolean isPlannedVolumeAboveTerrainSurface( CaveVoxelView view, HydrologyCaveCandidate candidate, CavePositionIndex actionIndex, @@ -679,7 +504,7 @@ public final class HydrologyCaveContainmentPlanner { return false; } - private HydrologyCaveRejection validatePlannedPosition( + HydrologyCaveRejection validatePlannedPosition( CaveVoxelView view, HydrologyCaveSource source, HydrologyCavePlannerSettings settings, @@ -706,7 +531,7 @@ public final class HydrologyCaveContainmentPlanner { return HydrologyCaveRejection.NONE; } - private HydrologyCaveRejection addPlannedBoundaryGuards( + HydrologyCaveRejection addPlannedBoundaryGuards( CaveVoxelView view, HydrologyCaveSource source, HydrologyCavePlannerSettings settings, @@ -782,7 +607,7 @@ public final class HydrologyCaveContainmentPlanner { return HydrologyCaveRejection.NONE; } - private HydrologyCaveRejection validateIntentionalOpening( + HydrologyCaveRejection validateIntentionalOpening( CaveVoxelView view, HydrologyCaveSource source, HydrologyCavePlannerSettings settings, @@ -808,7 +633,7 @@ public final class HydrologyCaveContainmentPlanner { return HydrologyCaveRejection.NONE; } - private OptionalLong findConflictingSourceId( + OptionalLong findConflictingSourceId( HydrologyCaveCandidate candidate, Map actions, Map claimedBy, @@ -831,797 +656,7 @@ public final class HydrologyCaveContainmentPlanner { return winner == null ? OptionalLong.empty() : OptionalLong.of(winner.sourceId()); } - private HydrologyCavePlan planGrottoOrClosedComponent( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - CaveVoxel targetVoxel = voxelAt(view, source.target()); - if (isFluidReachable(targetVoxel, settings)) { - return planClosedComponent(view, source, settings, throat); - } - if (targetVoxel == CaveVoxel.LAVA) { - return rejected(source, HydrologyCaveRejection.LAVA_CONTACT); - } - if (targetVoxel == CaveVoxel.INCOMPATIBLE_FLUID) { - return rejected(source, HydrologyCaveRejection.INCOMPATIBLE_FLUID); - } - return planGeneratedGrotto(view, source, settings, throat); - } - - private HydrologyCavePlan planWaterfallPool( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - if (!view.isOpenToSurface(source.target())) { - return planGrottoOrClosedComponent(view, source, settings, throat); - } - HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat); - if (dryThroatRejection != HydrologyCaveRejection.NONE) { - return rejected(source, dryThroatRejection); - } - HydrologyCaveRejection shaftRejection = validateWaterfallShaft(view, source, settings, throat); - if (shaftRejection != HydrologyCaveRejection.NONE) { - return rejected(source, shaftRejection); - } - CaveVoxel targetVoxel = voxelAt(view, source.target()); - if (!isFluidReachable(targetVoxel, settings)) { - return rejected(source, rejectionForTarget(targetVoxel, settings)); - } - - Map actions = new LinkedHashMap<>(); - addThroatActions(actions, throat, source); - addSealGuards(view, source, actions); - return accepted(view, source, actions); - } - - private HydrologyCavePlan planClosedComponent( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - HydrologyCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat); - if (dryThroatRejection != HydrologyCaveRejection.NONE) { - return rejected(source, dryThroatRejection); - } - HydrologyCaveRejection waterfallRejection = validateWaterfallShaft(view, source, settings, throat); - if (waterfallRejection != HydrologyCaveRejection.NONE) { - return rejected(source, waterfallRejection); - } - - CaveVoxel targetVoxel = voxelAt(view, source.target()); - if (!isFluidReachable(targetVoxel, settings)) { - return rejected(source, rejectionForTarget(targetVoxel, settings)); - } - - ComponentResult component = resolveClosedComponent(view, source, settings, throat); - if (component.rejection() != HydrologyCaveRejection.NONE) { - return rejected(source, component.rejection()); - } - - Map actions = new LinkedHashMap<>(); - addThroatActions(actions, throat, source); - for (CavePosition position : component.positions()) { - actions.put(position, HydrologyCaveAction.WET_SOURCE); - } - addSealGuards(view, source, actions); - return accepted(view, source, actions); - } - - private HydrologyCaveRejection validateDryThroatContacts( - CaveVoxelView view, - HydrologyCaveSource source, - List throat - ) { - Set throatPositions = Set.copyOf(throat); - for (CavePosition position : throat) { - if (position.y() <= source.waterHeadY()) { - continue; - } - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - CaveVoxel voxel = voxelAt(view, neighbor); - if (voxel == CaveVoxel.LAVA) { - return HydrologyCaveRejection.LAVA_CONTACT; - } - if (voxel == CaveVoxel.COMPATIBLE_FLUID) { - return HydrologyCaveRejection.EXISTING_FLUID; - } - if (voxel == CaveVoxel.INCOMPATIBLE_FLUID) { - return HydrologyCaveRejection.INCOMPATIBLE_FLUID; - } - } - } - return HydrologyCaveRejection.NONE; - } - - private HydrologyCaveRejection validateWaterfallShaft( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - if (source.mode() != HydrologyCaveMode.WATERFALL_POOL) { - return HydrologyCaveRejection.NONE; - } - - Set throatPositions = Set.copyOf(throat); - for (CavePosition position : throat) { - if (position.y() <= source.waterHeadY()) { - continue; - } - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, neighbor); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return boundsRejection; - } - CaveVoxel voxel = voxelAt(view, neighbor); - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return hazard; - } - if (voxel != CaveVoxel.SOLID) { - return HydrologyCaveRejection.WATERFALL_SHAFT_OPEN; - } - } - } - return HydrologyCaveRejection.NONE; - } - - private HydrologyCavePlan planGeneratedGrotto( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - GrottoResult grotto = buildGrotto(source, settings); - if (grotto.rejection() != HydrologyCaveRejection.NONE) { - return rejected(source, grotto.rejection()); - } - Set chamber = grotto.positions(); - - Set carve = new LinkedHashSet<>(chamber.size() + throat.size()); - carve.addAll(chamber); - carve.addAll(throat); - HydrologyCaveRejection carveRejection = validateGeneratedCarve(view, source, settings, carve); - if (carveRejection != HydrologyCaveRejection.NONE) { - return rejected(source, carveRejection); - } - - BoundaryResult boundary = validateGeneratedBoundary(view, source, settings, carve); - if (boundary.rejection() != HydrologyCaveRejection.NONE) { - return rejected(source, boundary.rejection()); - } - - Map actions = new LinkedHashMap<>(); - addChamberActions(actions, chamber, source.waterHeadY()); - addThroatActions(actions, throat, source); - for (CavePosition position : boundary.sealGuards()) { - actions.put(position, HydrologyCaveAction.SEAL_GUARD); - } - return accepted(view, source, actions); - } - - private HydrologyCavePlan planDeepPool( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - GrottoResult grotto = buildGrotto(source, settings); - if (grotto.rejection() != HydrologyCaveRejection.NONE) { - return rejected(source, grotto.rejection()); - } - Set chamber = grotto.positions(); - Set carve = new LinkedHashSet<>(chamber.size() + throat.size()); - carve.addAll(chamber); - carve.addAll(throat); - - HydrologyCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve); - if (carveRejection != HydrologyCaveRejection.NONE) { - return rejected(source, carveRejection); - } - BoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve); - if (boundary.rejection() != HydrologyCaveRejection.NONE) { - return rejected(source, boundary.rejection()); - } - - Map actions = new LinkedHashMap<>(); - addChamberActions(actions, chamber, source.waterHeadY()); - addThroatActions(actions, throat, source); - for (CavePosition position : boundary.sealGuards()) { - actions.put(position, HydrologyCaveAction.SEAL_GUARD); - } - return accepted(view, source, actions); - } - - private HydrologyCaveRejection validateDeepPoolCarve( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - Set carve - ) { - for (CavePosition position : carve) { - if (!view.isInWorld(position)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return boundsRejection; - } - CaveVoxel voxel = voxelAt(view, position); - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return hazard; - } - if (voxel == CaveVoxel.SOLID) { - continue; - } - if (position.y() > source.waterHeadY() - && voxel == CaveVoxel.CAVE_AIR - && !view.isOpenToSurface(position)) { - continue; - } - return HydrologyCaveRejection.GROTTO_INTERSECTION; - } - return HydrologyCaveRejection.NONE; - } - - private BoundaryResult validateDeepPoolBoundary( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - Set carve - ) { - Set guards = new LinkedHashSet<>(); - for (CavePosition position : carve) { - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (carve.contains(neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return BoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY); - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, neighbor); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return BoundaryResult.rejected(boundsRejection); - } - CaveVoxel voxel = voxelAt(view, neighbor); - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return BoundaryResult.rejected(hazard); - } - if (voxel == CaveVoxel.SOLID) { - guards.add(neighbor); - continue; - } - if (neighbor.y() > source.waterHeadY() - && voxel == CaveVoxel.CAVE_AIR - && !view.isOpenToSurface(neighbor)) { - continue; - } - return BoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN); - } - } - return BoundaryResult.accepted(guards); - } - - private ComponentResult resolveClosedComponent( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat - ) { - Queue queue = new ArrayDeque<>(); - Set queued = new LinkedHashSet<>(); - int visitedCount = 0; - - queue.add(source.target()); - queued.add(source.target()); - HydrologyCaveRejection seedRejection = addThroatContacts(view, source, settings, throat, queue, queued); - if (seedRejection != HydrologyCaveRejection.NONE) { - return ComponentResult.rejected(seedRejection); - } - - while (!queue.isEmpty()) { - CavePosition position = queue.remove(); - HydrologyCaveRejection positionRejection = validateReachablePosition(view, source, settings, position); - if (positionRejection != HydrologyCaveRejection.NONE) { - return ComponentResult.rejected(positionRejection); - } - if (++visitedCount > settings.maxFloodVolume()) { - return ComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); - } - - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - HydrologyCaveRejection neighborRejection = inspectReachableNeighbor( - view, - source, - settings, - neighbor, - queue, - queued - ); - if (neighborRejection != HydrologyCaveRejection.NONE) { - return ComponentResult.rejected(neighborRejection); - } - } - } - - return ComponentResult.accepted(queued); - } - - private HydrologyCaveRejection addThroatContacts( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List throat, - Queue queue, - Set queued - ) { - for (CavePosition position : throat) { - if (position.y() > source.waterHeadY()) { - continue; - } - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - HydrologyCaveRejection rejection = inspectReachableNeighbor( - view, - source, - settings, - neighbor, - queue, - queued - ); - if (rejection != HydrologyCaveRejection.NONE) { - return rejection; - } - } - } - return HydrologyCaveRejection.NONE; - } - - private HydrologyCaveRejection inspectReachableNeighbor( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - CavePosition position, - Queue queue, - Set queued - ) { - if (position.y() > source.waterHeadY()) { - return inspectAboveHeadNeighbor(view, source, position); - } - if (!view.isInWorld(position)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - - CaveVoxel voxel = voxelAt(view, position); - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return hazard; - } - if (!isFluidReachable(voxel, settings)) { - return HydrologyCaveRejection.NONE; - } - - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return boundsRejection; - } - if (queued.add(position)) { - queue.add(position); - } - return HydrologyCaveRejection.NONE; - } - - private HydrologyCaveRejection inspectAboveHeadNeighbor( - CaveVoxelView view, - HydrologyCaveSource source, - CavePosition position - ) { - if (isInletOpening(source, position) || !view.isInWorld(position)) { - return HydrologyCaveRejection.NONE; - } - CaveVoxel voxel = voxelAt(view, position); - return switch (voxel) { - case LAVA -> HydrologyCaveRejection.LAVA_CONTACT; - case COMPATIBLE_FLUID -> HydrologyCaveRejection.EXISTING_FLUID; - case INCOMPATIBLE_FLUID -> HydrologyCaveRejection.INCOMPATIBLE_FLUID; - default -> HydrologyCaveRejection.NONE; - }; - } - - private HydrologyCaveRejection validateReachablePosition( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - CavePosition position - ) { - if (!view.isInWorld(position)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return boundsRejection; - } - if (view.isOpenToSurface(position)) { - return HydrologyCaveRejection.OPEN_SURFACE; - } - CaveVoxel voxel = voxelAt(view, position); - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return hazard; - } - return isFluidReachable(voxel, settings) - ? HydrologyCaveRejection.NONE - : HydrologyCaveRejection.NO_CAVE_TARGET; - } - - private PathResult buildThroat( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings - ) { - CavePosition entry = source.entry(); - CavePosition target = source.target(); - int deltaX = target.x() - entry.x(); - int deltaY = target.y() - entry.y(); - int deltaZ = target.z() - entry.z(); - int movesX = Math.abs(deltaX); - int movesY = Math.abs(deltaY); - int movesZ = Math.abs(deltaZ); - int length = movesX + movesY + movesZ; - if (length > settings.maxThroatLength()) { - return PathResult.rejected(HydrologyCaveRejection.THROAT_LIMIT); - } - - int stepX = Integer.signum(deltaX); - int stepY = Integer.signum(deltaY); - int stepZ = Integer.signum(deltaZ); - int usedX = 0; - int usedY = 0; - int usedZ = 0; - CavePosition current = entry; - List positions = new ArrayList<>(length + 1); - - while (true) { - HydrologyCaveRejection positionRejection = validateThroatPosition(view, source, settings, current); - if (positionRejection != HydrologyCaveRejection.NONE) { - return PathResult.rejected(positionRejection); - } - positions.add(current); - if (current.equals(target)) { - return expandThroat(view, source, settings, positions); - } - - int axis = selectNextAxis(source.sourceId(), movesX, movesY, movesZ, usedX, usedY, usedZ); - if (axis == 0) { - current = current.offset(stepX, 0, 0); - usedX++; - } else if (axis == 1) { - current = current.offset(0, stepY, 0); - usedY++; - } else { - current = current.offset(0, 0, stepZ); - usedZ++; - } - } - } - - private PathResult expandThroat( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - List centerline - ) { - int radius = settings.throatRadius(); - int extent = radius - 1; - int radiusSquared = radius * radius; - Set expanded = new LinkedHashSet<>(); - for (CavePosition center : centerline) { - for (int dx = -extent; dx <= extent; dx++) { - for (int dy = -extent; dy <= extent; dy++) { - for (int dz = -extent; dz <= extent; dz++) { - if ((dx * dx) + (dy * dy) + (dz * dz) >= radiusSquared) { - continue; - } - CavePosition position = center.offset(dx, dy, dz); - if (position.y() > source.entry().y()) { - continue; - } - HydrologyCaveRejection rejection = validateThroatPosition(view, source, settings, position); - if (rejection != HydrologyCaveRejection.NONE) { - return PathResult.rejected(rejection); - } - expanded.add(position); - if (expanded.size() > settings.maxFloodVolume()) { - return PathResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); - } - } - } - } - } - return PathResult.accepted(List.copyOf(expanded)); - } - - private int selectNextAxis( - long sourceId, - int movesX, - int movesY, - int movesZ, - int usedX, - int usedY, - int usedZ - ) { - double scoreX = nextAxisScore(movesX, usedX); - double scoreY = nextAxisScore(movesY, usedY); - double scoreZ = nextAxisScore(movesZ, usedZ); - double minimum = Math.min(scoreX, Math.min(scoreY, scoreZ)); - int tieOffset = Math.floorMod(sourceId, 3); - for (int offset = 0; offset < 3; offset++) { - int axis = (tieOffset + offset) % 3; - double score = axis == 0 ? scoreX : axis == 1 ? scoreY : scoreZ; - if (score == minimum) { - return axis; - } - } - throw new IllegalStateException("No remaining throat axis"); - } - - private double nextAxisScore(int moves, int used) { - if (used >= moves) { - return Double.POSITIVE_INFINITY; - } - return ((2D * used) + 1D) / moves; - } - - private HydrologyCaveRejection validateThroatPosition( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - CavePosition position - ) { - if (!view.isInWorld(position)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return boundsRejection; - } - return rejectionForHazard(voxelAt(view, position), settings); - } - - private GrottoResult buildGrotto(HydrologyCaveSource source, HydrologyCavePlannerSettings settings) { - int horizontalRadius = settings.grottoHorizontalRadius(); - int verticalRadius = settings.grottoVerticalRadius(); - Set candidates = new LinkedHashSet<>(); - - for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) { - for (int dy = -verticalRadius; dy <= verticalRadius; dy++) { - for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) { - if (settings.grottoShape().contains(source, settings, dx, dy, dz)) { - candidates.add(source.target().offset(dx, dy, dz)); - if (candidates.size() > settings.maxFloodVolume()) { - return GrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); - } - } - } - } - } - candidates.add(source.target()); - for (int offset = 1; offset <= settings.dryHeadroom(); offset++) { - CavePosition headroom = new CavePosition( - source.target().x(), source.waterHeadY() + offset, source.target().z()); - if (Math.abs(headroom.y() - source.target().y()) > verticalRadius) { - return GrottoResult.rejected(HydrologyCaveRejection.DRY_HEADROOM_LIMIT); - } - candidates.add(headroom); - if (candidates.size() > settings.maxFloodVolume()) { - return GrottoResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT); - } - } - return GrottoResult.accepted(connectedGrotto(source.target(), candidates)); - } - - private Set connectedGrotto(CavePosition target, Set candidates) { - Queue queue = new ArrayDeque<>(); - Set connected = new LinkedHashSet<>(); - queue.add(target); - connected.add(target); - while (!queue.isEmpty()) { - CavePosition position = queue.remove(); - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (candidates.contains(neighbor) && connected.add(neighbor)) { - queue.add(neighbor); - } - } - } - return connected; - } - - private HydrologyCaveRejection validateGeneratedCarve( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - Set carve - ) { - for (CavePosition position : carve) { - if (!view.isInWorld(position)) { - return HydrologyCaveRejection.WORLD_BOUNDARY; - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return boundsRejection; - } - CaveVoxel voxel = voxelAt(view, position); - if (isGeneratedInletCarve(view, source, settings, position, voxel)) { - continue; - } - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return hazard; - } - if (voxel != CaveVoxel.SOLID) { - return HydrologyCaveRejection.GROTTO_INTERSECTION; - } - } - return HydrologyCaveRejection.NONE; - } - - private BoundaryResult validateGeneratedBoundary( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - Set carve - ) { - Set guards = new LinkedHashSet<>(); - for (CavePosition position : carve) { - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (carve.contains(neighbor)) { - continue; - } - if (isInletOpening(source, neighbor) - || isGeneratedInletOpening(view, source, settings, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return BoundaryResult.rejected(HydrologyCaveRejection.WORLD_BOUNDARY); - } - HydrologyCaveRejection boundsRejection = validateBounds(source, settings, neighbor); - if (boundsRejection != HydrologyCaveRejection.NONE) { - return BoundaryResult.rejected(boundsRejection); - } - CaveVoxel voxel = voxelAt(view, neighbor); - HydrologyCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != HydrologyCaveRejection.NONE) { - return BoundaryResult.rejected(hazard); - } - if (voxel != CaveVoxel.SOLID) { - return BoundaryResult.rejected(HydrologyCaveRejection.GROTTO_SHELL_OPEN); - } - guards.add(neighbor); - } - } - return BoundaryResult.accepted(guards); - } - - private void addChamberActions( - Map actions, - Collection positions, - int waterHeadY - ) { - for (CavePosition position : positions) { - HydrologyCaveAction action = position.y() <= waterHeadY - ? HydrologyCaveAction.WET_SOURCE - : HydrologyCaveAction.DRY_AIR; - actions.put(position, action); - } - } - - private void addThroatActions( - Map actions, - Collection throat, - HydrologyCaveSource source - ) { - for (CavePosition position : throat) { - HydrologyCaveAction action; - if (position.y() <= source.waterHeadY()) { - action = HydrologyCaveAction.WET_SOURCE; - } else if (source.mode() == HydrologyCaveMode.WATERFALL_POOL - || source.mode() == HydrologyCaveMode.GENERATED_GROTTO) { - action = HydrologyCaveAction.FALLING_FLUID; - } else { - action = HydrologyCaveAction.DRY_AIR; - } - actions.put(position, action); - } - } - - private void addSealGuards( - CaveVoxelView view, - HydrologyCaveSource source, - Map actions - ) { - Set guards = new LinkedHashSet<>(); - for (CavePosition position : List.copyOf(actions.keySet())) { - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (actions.containsKey(neighbor) - || isInletOpening(source, neighbor) - || !view.isInWorld(neighbor)) { - continue; - } - if (voxelAt(view, neighbor) == CaveVoxel.SOLID) { - guards.add(neighbor); - } - } - } - for (CavePosition guard : guards) { - actions.put(guard, HydrologyCaveAction.SEAL_GUARD); - } - } - - private boolean isInletOpening(HydrologyCaveSource source, CavePosition position) { - return position.equals(source.entry().offset(0, 1, 0)); - } - - private boolean isGeneratedInletCarve( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - CavePosition position, - CaveVoxel voxel - ) { - if (voxel != CaveVoxel.CAVE_AIR && voxel != CaveVoxel.COMPATIBLE_FLUID) { - return false; - } - int extent = Math.max(0, settings.throatRadius() - 1); - long deltaX = (long) position.x() - source.entry().x(); - long deltaZ = (long) position.z() - source.entry().z(); - return position.y() >= source.entry().y() - extent - && position.y() <= source.entry().y() - && deltaX * deltaX + deltaZ * deltaZ <= (long) extent * extent - && view.isOpenToSurface(position); - } - - private boolean isGeneratedInletOpening( - CaveVoxelView view, - HydrologyCaveSource source, - HydrologyCavePlannerSettings settings, - CavePosition position - ) { - int radius = Math.max(1, settings.throatRadius()); - long deltaX = (long) position.x() - source.entry().x(); - long deltaZ = (long) position.z() - source.entry().z(); - return position.y() >= source.entry().y() - && position.y() <= source.entry().y() + 1 - && deltaX * deltaX + deltaZ * deltaZ < (long) radius * radius - && view.isOpenToSurface(position); - } - - private HydrologyCaveRejection validateSource(HydrologyCaveSource source) { + HydrologyCaveRejection validateSource(HydrologyCaveSource source) { if (source.entry().y() < source.waterHeadY()) { return HydrologyCaveRejection.INVALID_SOURCE; } @@ -1634,7 +669,7 @@ public final class HydrologyCaveContainmentPlanner { return HydrologyCaveRejection.NONE; } - private HydrologyCaveRejection validateBounds( + HydrologyCaveRejection validateBounds( HydrologyCaveSource source, HydrologyCavePlannerSettings settings, CavePosition position @@ -1663,7 +698,7 @@ public final class HydrologyCaveContainmentPlanner { return HydrologyCaveRejection.NONE; } - private HydrologyCaveRejection rejectionForTarget( + HydrologyCaveRejection rejectionForTarget( CaveVoxel voxel, HydrologyCavePlannerSettings settings ) { @@ -1671,7 +706,7 @@ public final class HydrologyCaveContainmentPlanner { return hazard == HydrologyCaveRejection.NONE ? HydrologyCaveRejection.NO_CAVE_TARGET : hazard; } - private HydrologyCaveRejection rejectionForHazard( + HydrologyCaveRejection rejectionForHazard( CaveVoxel voxel, HydrologyCavePlannerSettings settings ) { @@ -1687,7 +722,7 @@ public final class HydrologyCaveContainmentPlanner { }; } - private boolean isFluidReachable(CaveVoxel voxel, HydrologyCavePlannerSettings settings) { + boolean isFluidReachable(CaveVoxel voxel, HydrologyCavePlannerSettings settings) { return voxel == CaveVoxel.CAVE_AIR || (voxel == CaveVoxel.COMPATIBLE_FLUID && settings.existingFluidPolicy() != HydrologyCaveFluidPolicy.REJECT_EXISTING) @@ -1695,11 +730,11 @@ public final class HydrologyCaveContainmentPlanner { && settings.existingFluidPolicy() == HydrologyCaveFluidPolicy.REPLACE_CONTAINED); } - private CaveVoxel voxelAt(CaveVoxelView view, CavePosition position) { + CaveVoxel voxelAt(CaveVoxelView view, CavePosition position) { return Objects.requireNonNull(view.voxelAt(position)); } - private OptionalLong findWinningSourceId( + OptionalLong findWinningSourceId( Set positions, Map claimedBy ) { @@ -1716,7 +751,7 @@ public final class HydrologyCaveContainmentPlanner { return winner == null ? OptionalLong.empty() : OptionalLong.of(winner.sourceId()); } - private HydrologyCavePlan accepted( + HydrologyCavePlan accepted( CaveVoxelView view, HydrologyCaveSource source, Map actions @@ -1731,7 +766,7 @@ public final class HydrologyCaveContainmentPlanner { return accepted(source, actions, preconditions); } - private HydrologyCavePlan accepted( + HydrologyCavePlan accepted( HydrologyCaveSource source, Map actions, Map preconditions @@ -1745,7 +780,7 @@ public final class HydrologyCaveContainmentPlanner { ); } - private HydrologyCavePlan acceptedDecision(HydrologyCaveSource source) { + HydrologyCavePlan acceptedDecision(HydrologyCaveSource source) { return new HydrologyCavePlan( source, HydrologyCaveRejection.NONE, @@ -1755,7 +790,7 @@ public final class HydrologyCaveContainmentPlanner { ); } - private HydrologyCavePlan rejected(HydrologyCaveSource source, HydrologyCaveRejection rejection) { + HydrologyCavePlan rejected(HydrologyCaveSource source, HydrologyCaveRejection rejection) { return new HydrologyCavePlan( source, rejection, @@ -1765,7 +800,7 @@ public final class HydrologyCaveContainmentPlanner { ); } - private HydrologyCavePlan rejectedOverlap(HydrologyCaveSource source, long winnerSourceId) { + HydrologyCavePlan rejectedOverlap(HydrologyCaveSource source, long winnerSourceId) { return new HydrologyCavePlan( source, HydrologyCaveRejection.OVERLAPPING_SOURCE, @@ -1774,72 +809,4 @@ public final class HydrologyCaveContainmentPlanner { OptionalLong.of(winnerSourceId) ); } - - private record PathResult(List positions, HydrologyCaveRejection rejection) { - private static PathResult accepted(List positions) { - return new PathResult(List.copyOf(positions), HydrologyCaveRejection.NONE); - } - - private static PathResult rejected(HydrologyCaveRejection rejection) { - return new PathResult(List.of(), rejection); - } - } - - private record IndependentDecision( - HydrologyCaveRejection rejection, - List guards - ) { - private static IndependentDecision accepted(List guards) { - return new IndependentDecision(HydrologyCaveRejection.NONE, List.copyOf(guards)); - } - - private static IndependentDecision rejected(HydrologyCaveRejection rejection) { - return new IndependentDecision(rejection, List.of()); - } - } - - private record ComponentResult(Set positions, HydrologyCaveRejection rejection) { - private static ComponentResult accepted(Set positions) { - return new ComponentResult( - Collections.unmodifiableSet(new LinkedHashSet<>(positions)), - HydrologyCaveRejection.NONE - ); - } - - private static ComponentResult rejected(HydrologyCaveRejection rejection) { - return new ComponentResult(Set.of(), rejection); - } - } - - private record BoundaryResult(Set sealGuards, HydrologyCaveRejection rejection) { - private static BoundaryResult accepted(Set sealGuards) { - return new BoundaryResult( - Collections.unmodifiableSet(new LinkedHashSet<>(sealGuards)), - HydrologyCaveRejection.NONE - ); - } - - private static BoundaryResult rejected(HydrologyCaveRejection rejection) { - return new BoundaryResult(Set.of(), rejection); - } - } - - private record GrottoResult(Set positions, HydrologyCaveRejection rejection) { - private static GrottoResult accepted(Set positions) { - return new GrottoResult( - Collections.unmodifiableSet(new LinkedHashSet<>(positions)), - HydrologyCaveRejection.NONE - ); - } - - private static GrottoResult rejected(HydrologyCaveRejection rejection) { - return new GrottoResult(Set.of(), rejection); - } - } - - private record CaveClaimGroup( - HydrologyCaveCandidate candidate, - Map actions - ) { - } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/cave/RecordingCaveVoxelView.java b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/RecordingCaveVoxelView.java new file mode 100644 index 000000000..2b7b5534b --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/cave/RecordingCaveVoxelView.java @@ -0,0 +1,96 @@ +package art.arcane.iris.engine.hydrology.cave; + +import java.util.Arrays; + +final class RecordingCaveVoxelView implements CaveVoxelView { + static final byte IN_WORLD = 0; + static final byte VOXEL = 1; + static final byte OPEN_TO_SURFACE = 2; + static final byte ABOVE_TERRAIN_SURFACE = 3; + private final CaveVoxelView delegate; + private final boolean observeColumnsOnly; + private CavePosition[] positions; + private byte[] operations; + private byte[] values; + private CavePositionIndex[] observed; + private int size; + + RecordingCaveVoxelView(CaveVoxelView delegate, boolean observeColumnsOnly) { + this.delegate = delegate; + this.observeColumnsOnly = observeColumnsOnly; + this.positions = new CavePosition[1024]; + this.operations = observeColumnsOnly ? new byte[0] : new byte[1024]; + this.values = observeColumnsOnly ? new byte[0] : new byte[1024]; + this.observed = observeColumnsOnly + ? new CavePositionIndex[]{new CavePositionIndex()} + : new CavePositionIndex[]{ + new CavePositionIndex(), + new CavePositionIndex(), + new CavePositionIndex(), + new CavePositionIndex() + }; + } + + @Override + public boolean isInWorld(CavePosition position) { + boolean result = delegate.isInWorld(position); + add(position, IN_WORLD, result ? 1 : 0); + return result; + } + + @Override + public CaveVoxel voxelAt(CavePosition position) { + CaveVoxel result = delegate.voxelAt(position); + add(position, VOXEL, result.ordinal()); + return result; + } + + @Override + public boolean isOpenToSurface(CavePosition position) { + boolean result = delegate.isOpenToSurface(position); + add(position, OPEN_TO_SURFACE, result ? 1 : 0); + return result; + } + + @Override + public boolean isAboveTerrainSurface(CavePosition position) { + boolean result = delegate.isAboveTerrainSurface(position); + add(position, ABOVE_TERRAIN_SURFACE, result ? 1 : 0); + return result; + } + + private void add(CavePosition position, byte operation, int value) { + int observedIndex = observeColumnsOnly ? 0 : operation; + int observedY = observeColumnsOnly ? 0 : position.y(); + if (!observed[observedIndex].add(position.x(), observedY, position.z())) { + return; + } + if (size == positions.length) { + int expandedSize = Math.multiplyExact(size, 2); + positions = Arrays.copyOf(positions, expandedSize); + if (!observeColumnsOnly) { + operations = Arrays.copyOf(operations, expandedSize); + values = Arrays.copyOf(values, expandedSize); + } + } + positions[size] = position; + if (!observeColumnsOnly) { + operations[size] = operation; + values[size] = (byte) value; + } + size++; + } + + CaveViewObservations snapshot() { + observed = null; + if (positions.length == 1024) { + return new CaveViewObservations( + Arrays.copyOf(positions, size), + observeColumnsOnly ? operations : Arrays.copyOf(operations, size), + observeColumnsOnly ? values : Arrays.copyOf(values, size), + size + ); + } + return new CaveViewObservations(positions, operations, values, size); + } +}