mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 07:24:16 +00:00
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
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<CavePosition, HydrologyCaveAction> actions;
|
||||
private final LinkedHashSet<CavePosition> openings;
|
||||
private final CavePositionIndex openingIndex;
|
||||
private final List<CaveSurfaceOpening> 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<CavePosition> 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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Long, CaveSpanSet> actions;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> openings;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> coastalGrottoActions;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> inlandGrottoActions;
|
||||
private final List<CaveSurfaceOpening> 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<Long, CaveSpanSet> 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<Long, CaveSpanSet> 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<CaveYSpan> actionSpans = excludedActions == null ? List.of() : excludedActions.spans();
|
||||
List<CaveYSpan> 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<Long, CaveSpanSet> 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<Long, CaveSpanSet> spans) {
|
||||
long count = 0L;
|
||||
for (CaveSpanSet spanSet : spans.values()) {
|
||||
count += spanSet.positionCount();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CaveCourseGraph(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets
|
||||
) {
|
||||
}
|
||||
@@ -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<CaveYSpan> 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<CaveYSpan> spans() {
|
||||
if (normalized != null) {
|
||||
return normalized;
|
||||
}
|
||||
sortPending();
|
||||
ArrayList<CaveYSpan> 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<CaveYSpan> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<HydrologyPoint> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
record CaveYSpan(int minimumY, int maximumY) {
|
||||
}
|
||||
@@ -8,10 +8,10 @@ import java.util.Set;
|
||||
|
||||
final class CrossTileResolutionContext {
|
||||
final Map<HydrologyTileKey, CrossTileResolvedOwner> resolved;
|
||||
final Set<HydrologyTileKey> resolving;
|
||||
private final Set<HydrologyTileKey> resolving;
|
||||
final HydrologyTileKey root;
|
||||
final long maximumTileOffset;
|
||||
final int maximumOwners;
|
||||
private final long maximumTileOffset;
|
||||
private final int maximumOwners;
|
||||
int iterations;
|
||||
|
||||
CrossTileResolutionContext(
|
||||
|
||||
@@ -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;
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
record FootprintCenterlineProjection(
|
||||
int pointIndex,
|
||||
double x,
|
||||
double z,
|
||||
double distance,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record FootprintCourseRasterKey(
|
||||
long courseId,
|
||||
RiverCourseType courseType,
|
||||
String profileKey,
|
||||
List<HydraulicSegment> segments
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
record FootprintFeatureKey(
|
||||
long courseId,
|
||||
long segmentId,
|
||||
HydrologyFeatureType type,
|
||||
int role,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Long, HydrologyColumnLayer> 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<HydrologyColumnLayer> 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<HydrologyColumnLayer> resolveSurfaceHeadConflicts(List<HydrologyColumnLayer> candidates) {
|
||||
HashMap<Long, HydrologyColumnLayer> 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<HydrologyColumnLayer> 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
record FootprintRasterStencil(
|
||||
int[] deltaXs,
|
||||
int[] deltaZs,
|
||||
double[] distances
|
||||
) {
|
||||
int size() {
|
||||
return deltaXs.length;
|
||||
}
|
||||
}
|
||||
+37
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
record FootprintRasterStencilKey(
|
||||
int channelRadius,
|
||||
double shoreWidth,
|
||||
double gradingWidth
|
||||
) {
|
||||
}
|
||||
+12
@@ -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<HydrologyColumnSample> columns,
|
||||
SurfaceFootprint surface
|
||||
) {
|
||||
}
|
||||
+88
-833
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
+116
-1279
File diff suppressed because it is too large
Load Diff
+565
@@ -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<FootprintMutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
boolean firstSegment,
|
||||
boolean clipStart,
|
||||
boolean clipEnd,
|
||||
boolean validationOnly,
|
||||
HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface
|
||||
) {
|
||||
List<HydrologyPoint> 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<HydrologyPoint> 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<FootprintMutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
List<HydrologyPoint> 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<HydrologyPoint> 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<HydrologyPoint> 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<HydrologyPoint> continuousCenterline(HydraulicSegment segment) {
|
||||
List<HydrologyPoint> configured = segment.centerline();
|
||||
if (configured.size() == 1) {
|
||||
return configured;
|
||||
}
|
||||
ArrayList<HydrologyPoint> 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<FootprintMutableColumn> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ final class SourceSelection {
|
||||
final boolean surface;
|
||||
final List<SourceCandidate> candidates;
|
||||
final SourceAdmissionSelection admission;
|
||||
final int guaranteedMinimum;
|
||||
private final int guaranteedMinimum;
|
||||
final int maximumOptionalRejections;
|
||||
final int targetCount;
|
||||
final ArrayList<Integer> 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<Long> acceptedSourceNodeIds(List<RiverCourse> acceptedCourses) {
|
||||
private Set<Long> acceptedSourceNodeIds(List<RiverCourse> acceptedCourses) {
|
||||
RiverCourseType expectedType = surface ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND;
|
||||
HashSet<Long> 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;
|
||||
}
|
||||
|
||||
@@ -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<CavePosition> sealGuards, HydrologyCaveRejection rejection) {
|
||||
static CaveBoundaryResult accepted(Set<CavePosition> sealGuards) {
|
||||
return new CaveBoundaryResult(
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(sealGuards)),
|
||||
HydrologyCaveRejection.NONE
|
||||
);
|
||||
}
|
||||
|
||||
static CaveBoundaryResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveBoundaryResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
record CaveClaimGroup(
|
||||
HydrologyCaveCandidate candidate,
|
||||
Map<CavePosition, HydrologyCaveAction> actions
|
||||
) {
|
||||
}
|
||||
@@ -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<CavePosition> positions, HydrologyCaveRejection rejection) {
|
||||
static CaveComponentResult accepted(Set<CavePosition> positions) {
|
||||
return new CaveComponentResult(
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(positions)),
|
||||
HydrologyCaveRejection.NONE
|
||||
);
|
||||
}
|
||||
|
||||
static CaveComponentResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveComponentResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -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<CavePosition> positions, HydrologyCaveRejection rejection) {
|
||||
static CaveGrottoResult accepted(Set<CavePosition> positions) {
|
||||
return new CaveGrottoResult(
|
||||
Collections.unmodifiableSet(new LinkedHashSet<>(positions)),
|
||||
HydrologyCaveRejection.NONE
|
||||
);
|
||||
}
|
||||
|
||||
static CaveGrottoResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveGrottoResult(Set.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CaveIndependentDecision(
|
||||
HydrologyCaveRejection rejection,
|
||||
List<CavePosition> guards
|
||||
) {
|
||||
private static CaveIndependentDecision accepted(List<CavePosition> guards) {
|
||||
return new CaveIndependentDecision(HydrologyCaveRejection.NONE, List.copyOf(guards));
|
||||
}
|
||||
|
||||
private static CaveIndependentDecision rejected(HydrologyCaveRejection rejection) {
|
||||
return new CaveIndependentDecision(rejection, List.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CavePathResult(List<CavePosition> positions, HydrologyCaveRejection rejection) {
|
||||
static CavePathResult accepted(List<CavePosition> positions) {
|
||||
return new CavePathResult(List.copyOf(positions), HydrologyCaveRejection.NONE);
|
||||
}
|
||||
|
||||
static CavePathResult rejected(HydrologyCaveRejection rejection) {
|
||||
return new CavePathResult(List.of(), rejection);
|
||||
}
|
||||
}
|
||||
@@ -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<HydrologyObservedPlannedSurface.Observation> observations
|
||||
) {
|
||||
static CaveSurfaceObservations empty() {
|
||||
return new CaveSurfaceObservations(List.of());
|
||||
}
|
||||
|
||||
static CaveSurfaceObservations capture(
|
||||
CaveViewObservations viewObservations,
|
||||
HydrologyObservedPlannedSurface plannedSurface
|
||||
) {
|
||||
ArrayList<HydrologyObservedPlannedSurface.Observation> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package art.arcane.iris.engine.hydrology.cave;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
record CaveValidationBatch(
|
||||
List<HydrologyCavePlan> plans,
|
||||
Map<CavePosition, HydrologyCaveAction> actions,
|
||||
Map<CavePosition, CaveVoxelPrecondition> baselinePreconditions
|
||||
) {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+809
@@ -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<CavePosition> 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<CavePosition> 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<CavePosition, HydrologyCaveAction> 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<CavePosition> 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<CavePosition, HydrologyCaveAction> 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<CavePosition> throat
|
||||
) {
|
||||
Set<CavePosition> 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<CavePosition> throat
|
||||
) {
|
||||
if (source.mode() != HydrologyCaveMode.WATERFALL_POOL) {
|
||||
return HydrologyCaveRejection.NONE;
|
||||
}
|
||||
|
||||
Set<CavePosition> 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<CavePosition> throat
|
||||
) {
|
||||
CaveGrottoResult grotto = buildGrotto(source, settings);
|
||||
if (grotto.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, grotto.rejection());
|
||||
}
|
||||
Set<CavePosition> chamber = grotto.positions();
|
||||
|
||||
Set<CavePosition> 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<CavePosition, HydrologyCaveAction> 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<CavePosition> throat
|
||||
) {
|
||||
CaveGrottoResult grotto = buildGrotto(source, settings);
|
||||
if (grotto.rejection() != HydrologyCaveRejection.NONE) {
|
||||
return planner.rejected(source, grotto.rejection());
|
||||
}
|
||||
Set<CavePosition> chamber = grotto.positions();
|
||||
Set<CavePosition> 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<CavePosition, HydrologyCaveAction> 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<CavePosition> 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<CavePosition> carve
|
||||
) {
|
||||
Set<CavePosition> 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<CavePosition> throat
|
||||
) {
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> 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<CavePosition> throat,
|
||||
Queue<CavePosition> queue,
|
||||
Set<CavePosition> 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<CavePosition> queue,
|
||||
Set<CavePosition> 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<CavePosition> 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<CavePosition> centerline
|
||||
) {
|
||||
int radius = settings.throatRadius();
|
||||
int extent = radius - 1;
|
||||
int radiusSquared = radius * radius;
|
||||
Set<CavePosition> 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<CavePosition> 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<CavePosition> connectedGrotto(CavePosition target, Set<CavePosition> candidates) {
|
||||
Queue<CavePosition> queue = new ArrayDeque<>();
|
||||
Set<CavePosition> 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<CavePosition> 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<CavePosition> carve
|
||||
) {
|
||||
Set<CavePosition> 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<CavePosition, HydrologyCaveAction> actions,
|
||||
Collection<CavePosition> 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<CavePosition, HydrologyCaveAction> actions,
|
||||
Collection<CavePosition> 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<CavePosition, HydrologyCaveAction> actions
|
||||
) {
|
||||
Set<CavePosition> 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);
|
||||
}
|
||||
}
|
||||
+52
-1085
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user