Tunabelly

This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-22 17:04:56 -04:00
1 parent 197d2c2461
commit 30c533feaf
114 files changed
+4071 -3994

No files matched your search

@@ -11,7 +11,8 @@ record FootprintLayerShape(
boolean archedChannel,
boolean roundedSurfaceBed,
boolean organicBoundary,
boolean fallingThroat
boolean fallingThroat,
boolean inlet
) {
double totalRadius() {
return channelRadius + shoreWidth + gradingWidth;
@@ -159,7 +159,7 @@ final class FootprintMutableColumn {
first.connectedFluid() || second.connectedFluid(),
first.fallingFluid() || second.fallingFluid(),
first.receivingPool() || second.receivingPool(),
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY < naturalHeight),
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY != naturalHeight),
first.fluidOwned() || second.fluidOwned(),
first.oceanApron() || second.oceanApron(),
first.profileKey(),
@@ -46,7 +46,7 @@ final class HydrologyCacheBudget {
}
long routingBytes() {
return regionalEnabled ? plannerBytes / 8L : plannerBytes * 3L / 16L;
return plannerBytes * 3L / 16L;
}
long ownerBytes() {
@@ -69,7 +69,4 @@ final class HydrologyCacheBudget {
return regionalEnabled ? plannerBytes / 4L : 0L;
}
long regionalReachBytes() {
return regionalEnabled ? plannerBytes / 16L : 0L;
}
}
@@ -69,7 +69,7 @@ final class HydrologyCacheWeights {
static int regionalCoarse(HydrologyRegionalCoarsePlan plan) {
HydrologyRegionalGraph.Tree tree = plan.tree();
long treeBytes = tree == null ? 0L : 96L + (long) tree.selected().length * 8L
+ (long) tree.accumulation().length * 4L + tree.retainedLabelCount() * 96L;
+ ((long) tree.accumulation().length + tree.heads().length) * 4L + tree.retainedLabelCount() * 96L;
return saturated(ENTRY_BYTES + grid(plan.grid()) + treeBytes + (long) plan.outlets().size() * 224L
+ (long) plan.sources().size() * 32L + (long) plan.envelopes().size() * 64L);
}
@@ -79,7 +79,7 @@ final class HydrologyCacheWeights {
}
private static long grid(HydrologySampledGrid grid) {
return grid == null ? 0L : 96L + (long) grid.nodes().size() * (64L + TERRAIN_BYTES);
return grid == null ? 0L : 96L + (long) grid.nodes().size() * (64L + TERRAIN_BYTES + 104L);
}
private static long routingPlan(HydrologyRoutingPlan plan) {
@@ -88,7 +88,8 @@ final class HydrologyCacheWeights {
}
return 128L + (long) plan.potential().length * 8L
+ ((long) plan.parent().length + plan.outletIndex().length + plan.routeLengths().length) * 4L
+ (long) plan.outlets().size() * 224L;
+ (long) plan.outlets().size() * 224L
+ (plan.surfaceDrainage() == null ? 0L : plan.surfaceDrainage().maximumRetainedBytes());
}
private static long network(List<DrainageNode> nodes, List<DrainageEdge> edges, List<RiverOutlet> outlets,
@@ -1194,8 +1194,12 @@ final class HydrologyCaveCourseFilter {
HydraulicSegment segment = segments.get(index);
if (segment.type().isSurface() && segment.fallingFluid()) {
int radius = Math.max(1, segment.width() / 2) + 1;
HydraulicSegment receiving = index + 1 < segments.size() ? segments.get(index + 1) : null;
boolean coastal = receiving != null && receiving.type() == HydrologyFeatureType.MOUTH
&& receiving.upstreamHeadY() == segment.downstreamHeadY()
&& receiving.start().equals(segment.end());
openings.add(CaveSurfaceOpening.create(segment, true, (long) radius * radius,
segment.downstreamHeadY() + 2, true));
segment.downstreamHeadY() + (coastal ? 0 : 2), true));
continue;
}
if (!segment.type().isUnderground()) {
@@ -1,9 +1,12 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.surface.SurfaceNoise;
import art.arcane.iris.generation.hydrology.surface.SurfaceInletRange;
final class HydrologyChannelGeometry {
private final HydrologyFootprintCompiler compiler;
private RiverCourse inletCourse;
private SurfaceInletRange inletRange;
HydrologyChannelGeometry(HydrologyFootprintCompiler compiler) {
this.compiler = compiler;
@@ -141,10 +144,24 @@ final class HydrologyChannelGeometry {
archedChannel,
roundedSurfaceBed,
organicBoundary,
falling
falling,
inletStation(course, segment, point)
);
}
private boolean inletStation(RiverCourse course, HydraulicSegment segment, HydrologyPoint point) {
if (course.type() != RiverCourseType.SURFACE || !segment.type().isSurface() || !segment.fallingFluid()) {
return false;
}
if (inletCourse != course) {
HydrologyTerrainSampler receiving = HydrologyOceanReceiver.forCourse(
compiler.settings, compiler::sampleTerrainBasis, course);
inletRange = SurfaceInletRange.forCourse(compiler.settings, course, receiving);
inletCourse = course;
}
return inletRange.allowsFallingStation(segment.id(), point.x(), point.z());
}
int organicVerticalVariation(
long seed,
int x,
@@ -0,0 +1,280 @@
package art.arcane.iris.generation.hydrology;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicReferenceArray;
final class HydrologyDrainageGeometry {
private final HydrologyPlanner planner;
private final Basin basin;
private final int[] heads;
private final AtomicReferenceArray<HydrologyPoint> anchors;
private final AtomicReferenceArray<CompletableFuture<List<HydrologyPoint>>> edges;
HydrologyDrainageGeometry(HydrologyPlanner planner, Basin basin) {
this.planner = planner;
this.basin = basin;
this.heads = new int[basin.grid().nodes().size()];
this.anchors = new AtomicReferenceArray<>(heads.length);
this.edges = new AtomicReferenceArray<>(heads.length);
Arrays.fill(heads, Integer.MIN_VALUE);
planHeads();
}
int head(int index) {
return heads[index];
}
HydrologyPoint anchor(int index) {
HydrologyPoint existing = anchors.get(index);
if (existing != null) {
return existing;
}
HydrologyGridNode node = basin.grid().node(index);
HydrologyPoint point;
if (basin.parent()[index] < 0 && basin.outletIndex()[index] >= 0) {
HydrologyPoint landward = basin.outlets().get(basin.outletIndex()[index]).outlet().landwardPoint();
point = new HydrologyPoint(landward.x(), heads[index], landward.z());
} else {
point = displacedAnchor(node);
}
anchors.compareAndSet(index, null, point);
return anchors.get(index);
}
List<HydrologyPoint> edge(int index) {
DraftProfile profile = planner.currentDraftProfile();
profile.routeCalls++;
CompletableFuture<List<HydrologyPoint>> future = edges.get(index);
if (future == null) {
CompletableFuture<List<HydrologyPoint>> owned = new CompletableFuture<>();
CompletableFuture<List<HydrologyPoint>> existing = edges.compareAndExchange(index, null, owned);
if (existing == null) {
long started = System.nanoTime();
profile.routeSolves++;
try {
List<HydrologyPoint> points = compile(index);
owned.complete(points);
return points;
} catch (Throwable failure) {
owned.completeExceptionally(failure);
edges.compareAndSet(index, owned, null);
throw HydrologyPlanner.propagateOwnerFailure(failure);
} finally {
profile.routeSolveNanos += System.nanoTime() - started;
}
}
future = existing;
}
try {
return future.join();
} catch (CompletionException failure) {
throw HydrologyPlanner.propagateOwnerFailure(failure.getCause());
}
}
long maximumRetainedBytes() {
int stations = Math.ceilDiv(basin.grid().spacing() * 3, planner.settings.routing().refinementSpacing()) + 2;
return (long) heads.length * (112L + stations * 40L);
}
private void planHeads() {
int[] path = new int[heads.length];
int[] nominal = new int[heads.length];
for (int index = 0; index < heads.length; index++) {
if (heads[index] != Integer.MIN_VALUE || basin.outletIndex()[index] < 0) {
continue;
}
int current = index;
int size = 0;
while (current >= 0 && heads[current] == Integer.MIN_VALUE) {
if (size == path.length) {
throw new IllegalStateException("Surface drainage must be acyclic.");
}
path[size++] = current;
current = basin.parent()[current];
}
int downstream = current < 0 ? Integer.MIN_VALUE : heads[current];
while (size > 0) {
int node = path[--size];
HydrologyTerrainSample terrain = basin.grid().node(node).terrain();
if (basin.parent()[node] < 0) {
HydrologyPoint landward = basin.outlets().get(basin.outletIndex()[node]).outlet().landwardPoint();
HydrologyTerrainSample sampled = planner.sampleLandBasisWithoutSlope(landward.x(), landward.z());
if (sampled != null) {
terrain = sampled;
}
}
int depth = (planner.settings.surface().minimumDepth() + planner.settings.surface().maximumDepth()) / 2;
nominal[node] = terrain.naturalHeight() - planner.settings.surface().banks().sink() - depth;
int required = terrain.naturalHeight() - planner.sourcePlanner.permittedSurfaceIncision(terrain) + depth;
heads[node] = Math.max(Math.max(planner.settings.seaLevel(), required), downstream);
downstream = heads[node];
}
}
int[] required = heads.clone();
int[] ceiling = new int[heads.length];
Arrays.fill(ceiling, Integer.MAX_VALUE);
int[] upstream = new int[heads.length];
for (int index = 0; index < heads.length; index++) {
if (heads[index] == Integer.MIN_VALUE) {
continue;
}
if (planner.sourcePlanner.rawSourceEligible(basin.grid().node(index).terrain(),
planner.settings.surface().sources(), true)) {
ceiling[index] = Math.max(required[index], nominal[index]);
}
int parent = basin.parent()[index];
if (parent >= 0) {
upstream[parent]++;
}
}
int end = 0;
for (int index = 0; index < heads.length; index++) {
if (heads[index] != Integer.MIN_VALUE && upstream[index] == 0) {
path[end++] = index;
}
}
for (int position = 0; position < end; position++) {
int node = path[position];
int parent = basin.parent()[node];
if (parent >= 0) {
ceiling[parent] = Math.min(ceiling[parent], ceiling[node]);
if (--upstream[parent] == 0) {
path[end++] = parent;
}
}
}
for (int position = end - 1; position >= 0; position--) {
int node = path[position];
int parent = basin.parent()[node];
int downstream = parent < 0 ? Integer.MIN_VALUE : heads[parent];
heads[node] = Math.max(Math.max(required[node], downstream), Math.min(nominal[node], ceiling[node]));
}
}
private HydrologyPoint displacedAnchor(HydrologyGridNode node) {
double ratio = Math.min(HydrologyRouteGeometry.ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO,
planner.settings.geometry().meanders().maximumOffsetRatio() * 0.625D);
double wavelength = planner.settings.geometry().meanders().primaryWavelength() * 5D;
int x = node.x() + (int) StrictMath.round(basin.grid().spacing() * ratio
* planner.routeAnchorX.noiseSigned(node.x() / wavelength, node.z() / wavelength));
int z = node.z() + (int) StrictMath.round(basin.grid().spacing() * ratio
* planner.routeAnchorZ.noiseSigned(node.x() / wavelength, node.z() / wavelength));
if (!permittedAnchor(node, x, z)) {
x = node.x();
z = node.z();
}
if (nearCoast(node)) {
int clearance = (int) StrictMath.ceil(planner.settings.surface().maximumWidth()
* planner.settings.surface().banks().springWidthRatio() / 2D) + 1;
if (!landClearance(x, z, clearance)) {
int radius = Math.max(1, (int) StrictMath.ceil(basin.grid().spacing()
* HydrologyRouteGeometry.ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO));
long nearest = Long.MAX_VALUE;
int selectedX = x;
int selectedZ = z;
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
int candidateX = node.x() + offset.x() * radius;
int candidateZ = node.z() + offset.z() * radius;
long dx = (long) candidateX - x;
long dz = (long) candidateZ - z;
long distance = dx * dx + dz * dz;
if (distance < nearest && permittedAnchor(node, candidateX, candidateZ)
&& landClearance(candidateX, candidateZ, clearance)) {
nearest = distance;
selectedX = candidateX;
selectedZ = candidateZ;
}
}
x = selectedX;
z = selectedZ;
}
}
return new HydrologyPoint(x, heads[node.index()], z);
}
private boolean permittedAnchor(HydrologyGridNode node, int x, int z) {
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x, z);
return terrain != null && !terrain.ocean() && terrain.transitAllowed()
&& node.terrain().drainsInto(terrain) && terrain.drainsInto(node.terrain())
&& HydrologySurfaceProfiles.sharesProfile(node.terrain(), terrain)
&& node.terrain().surfacePolicy().areaKey().equals(terrain.surfacePolicy().areaKey());
}
private boolean nearCoast(HydrologyGridNode node) {
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
HydrologyGridNode neighbor = basin.grid().nodeAt(node.gridX() + offset.x(), node.gridZ() + offset.z());
if (neighbor != null && neighbor.terrain().ocean()) {
return true;
}
}
return false;
}
private boolean landClearance(int x, int z, int radius) {
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x + offset.x() * radius,
z + offset.z() * radius);
if (terrain == null || terrain.ocean()) {
return false;
}
}
return true;
}
private List<HydrologyPoint> compile(int index) {
int downstream = basin.parent()[index];
if (downstream < 0) {
return List.of();
}
HydrologyPoint start = anchor(index);
HydrologyPoint end = anchor(downstream);
int continuation = basin.parent()[downstream];
HydrologyPoint next = continuation < 0 ? basin.outlets().get(basin.outletIndex()[index])
.outlet().connectionPoint() : anchor(continuation);
RouteDirection startTangent = planner.routePaths.direction(start.x(), start.z(), end.x(), end.z());
RouteDirection endTangent = planner.routePaths.direction(end.x(), end.z(), next.x(), next.z());
if (endTangent.x() == 0D && endTangent.z() == 0D) {
endTangent = startTangent;
}
double distance = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
int steps = Math.max(1, (int) StrictMath.ceil(distance / planner.settings.routing().refinementSpacing()));
ArrayList<HydrologyPoint> points = new ArrayList<>(steps + 1);
for (int step = 0; step <= steps; step++) {
double progress = step / (double) steps;
RoutePosition position = planner.routeGeometry.routePosition(start, end, startTangent, endTangent,
distance * HydrologyRouteGeometry.ROUTE_TANGENT_SCALE, distance, progress, 1D);
HydrologyPoint point = step == 0 ? start : step == steps ? end
: new HydrologyPoint((int) StrictMath.round(position.x()),
(int) StrictMath.round(start.y() + (end.y() - start.y()) * progress),
(int) StrictMath.round(position.z()));
if (points.isEmpty() || points.getLast().distanceSquared2D(point) > 0L) {
points.add(point);
}
}
return permitted(points) ? List.copyOf(points) : List.of();
}
private boolean permitted(List<HydrologyPoint> points) {
HydrologyTerrainSample previous = null;
for (int index = 1; index < points.size(); index++) {
for (HydrologyPoint point : planner.segments.rasterLine(points.get(index - 1), points.get(index))) {
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || terrain.ocean() || !terrain.transitAllowed()
|| previous != null && (!previous.drainsInto(terrain)
|| !HydrologySurfaceProfiles.sharesProfile(previous, terrain))) {
return false;
}
previous = terrain;
}
}
return points.size() >= 2;
}
record Basin(HydrologySampledGrid grid, int[] parent, int[] outletIndex, List<OutletCandidate> outlets) {
}
}
@@ -0,0 +1,50 @@
package art.arcane.iris.generation.hydrology;
final class HydrologyDrainageGraph {
private final HydrologyPlannerSettings settings;
private final int[] upstream;
private final double[] costs;
private final byte[] eligibility;
HydrologyDrainageGraph(HydrologySampledGrid grid, HydrologySourcePlanner planner) {
settings = planner.settings();
int directions = HydrologySourcePlanner.ROUTING_OFFSETS.size();
int size = Math.multiplyExact(grid.nodes().size(), directions);
upstream = new int[size];
costs = new double[size];
eligibility = new byte[size];
for (HydrologyGridNode downstream : grid.nodes()) {
int valleyFloor = planner.minimumNeighborHeight(grid, downstream);
for (int direction = 0; direction < directions; direction++) {
int edge = downstream.index() * directions + direction;
HydrologyGridOffset offset = HydrologySourcePlanner.ROUTING_OFFSETS.get(direction);
HydrologyGridNode source = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
upstream[edge] = source == null ? -1 : source.index();
if (source == null || source.terrain().ocean() || !source.terrain().transitAllowed()
|| !source.terrain().drainsInto(downstream.terrain())) {
continue;
}
eligibility[edge] = 1;
if (HydrologySurfaceProfiles.sharesProfile(source.terrain(), downstream.terrain())
&& downstream.terrain().naturalHeight() - source.terrain().naturalHeight()
<= planner.maximumSurfaceEdgeRise(downstream.terrain())) {
eligibility[edge] |= 2;
}
costs[edge] = planner.routeCost(source, downstream, offset, valleyFloor);
}
}
}
HydrologyPlannerSettings settings() {
return settings;
}
int upstream(int downstream, int direction, boolean surface) {
int edge = downstream * HydrologySourcePlanner.ROUTING_OFFSETS.size() + direction;
return (eligibility[edge] & (surface ? 2 : 1)) == 0 ? -1 : upstream[edge];
}
double cost(int downstream, int direction) {
return costs[downstream * HydrologySourcePlanner.ROUTING_OFFSETS.size() + direction];
}
}
@@ -565,6 +565,9 @@ final class HydrologyFootprintCompiler {
boolean surfaceDrop = segment.type().isSurface() && segment.fallingFluid();
if (surfaceDrop && channel && !falling) {
int maximumIncision = terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision());
if (shape.inlet()) {
maximumIncision = Math.max(maximumIncision, settings.surface().banks().inlet().maximumIncision());
}
int allowedIncision = Math.min(maximumIncision,
(int) StrictMath.floor(maximumIncision * terrain.incisionMultiplier()));
bed = Math.max(bed, Math.min(shape.fluidHead(), terrain.naturalHeight() - allowedIncision));
@@ -17,7 +17,6 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -701,7 +700,6 @@ public final class HydrologyPlanner {
final HashMap<UndergroundSegmentCapKey, Integer> undergroundSegmentCaps;
final HashMap<Long, HydrologyPoint> routeAnchors;
final HashMap<Long, HydrologyPoint> surfaceRouteAnchors;
final IdentityHashMap<HydrologySampledGrid, HashMap<SurfaceRouteKey, List<HydrologyPoint>>> surfaceRoutes;
final HydrologyCaveCourseFilter.CandidateCache caveCandidates;
final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
@@ -723,7 +721,6 @@ public final class HydrologyPlanner {
this.undergroundSegmentCaps = new HashMap<>();
this.routeAnchors = new HashMap<>();
this.surfaceRouteAnchors = new HashMap<>();
this.surfaceRoutes = new IdentityHashMap<>();
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
}
@@ -18,7 +18,7 @@ public record HydrologyPlannerSettings(
SeaCaves seaCaves,
SurfacePolicyBounds surfacePolicyBounds
) {
private static final long PLAN_FORMAT_REVISION = 8L;
private static final long PLAN_FORMAT_REVISION = 9L;
private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4;
public HydrologyPlannerSettings {
@@ -345,8 +345,8 @@ public record HydrologyPlannerSettings(
}
/**
* {@code sink} is how many blocks the water surface sits below the lowest natural ground beside
* the channel; zero keeps the water flush with the bank and the bank top always meets the water.
* {@code sink} is how many blocks the water surface sits below the shaped bank top;
* zero keeps the water flush with the bank.
* {@code channel} shapes the wet outline and {@code flow} the waterfalls and plunge basins.
*/
public record Banks(
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.hydrology;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Comparator;
import java.util.List;
@@ -94,7 +95,42 @@ final class HydrologyRegionalGraph {
accumulation[label.downstream.state.id()] += accumulation[label.state.id()];
}
}
return new Tree(selected, accumulation, List.copyOf(outlets));
int[] heads = new int[labels.size()];
int[] required = new int[labels.size()];
int[] nominal = new int[labels.size()];
int[] ceiling = new int[labels.size()];
Arrays.fill(ceiling, Integer.MAX_VALUE);
for (Label label : labels) {
HydrologyTerrainSample terrain = grid.node(label.state.index()).terrain();
if (label.downstream == null) {
HydrologyPoint landward = outlets.get(label.state.root()).outlet().landwardPoint();
terrain = planner.sampleLandBasisWithoutSlope(landward.x(), landward.z());
}
int index = label.state.id();
int depth = (planner.settings.surface().minimumDepth() + planner.settings.surface().maximumDepth()) / 2;
nominal[index] = terrain.naturalHeight() - planner.settings.surface().banks().sink() - depth;
int minimum = terrain.naturalHeight() - planner.sourcePlanner.permittedSurfaceIncision(terrain) + depth;
required[index] = coastal ? planner.settings.seaLevel() : Math.max(planner.settings.seaLevel(),
Math.max(minimum, label.downstream == null ? Integer.MIN_VALUE : required[label.downstream.state.id()]));
if (selected[label.state.index()] == label && planner.sourcePlanner.rawSourceEligible(
grid.node(label.state.index()).terrain(), planner.settings.surface().sources(), true)) {
ceiling[index] = Math.max(nominal[index], required[index]);
}
}
for (int index = labels.size() - 1; index >= 0; index--) {
Label label = labels.get(index);
if (label.downstream != null) {
int parent = label.downstream.state.id();
ceiling[parent] = Math.min(ceiling[parent], ceiling[index]);
}
}
for (Label label : labels) {
int index = label.state.id();
int downstream = label.downstream == null ? Integer.MIN_VALUE : heads[label.downstream.state.id()];
heads[index] = coastal ? planner.settings.seaLevel()
: Math.max(Math.max(required[index], downstream), Math.min(nominal[index], ceiling[index]));
}
return new Tree(selected, accumulation, List.copyOf(outlets), heads);
}
List<Integer> path(Tree tree, int source) {
@@ -160,7 +196,7 @@ final class HydrologyRegionalGraph {
+ first.routingCost() + second.routingCost()) * policy);
}
record Tree(Label[] selected, int[] accumulation, List<OutletCandidate> outlets) {
record Tree(Label[] selected, int[] accumulation, List<OutletCandidate> outlets, int[] heads) {
int retainedLabelCount() {
BitSet retained = new BitSet(accumulation.length);
for (Label label : selected) {
@@ -187,6 +223,15 @@ final class HydrologyRegionalGraph {
return selected[source] == null ? 0D : selected[source].state.length();
}
List<HydrologyPoint> centerline(HydrologySampledGrid grid, int source) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
for (Label label = selected[source]; label != null; label = label.downstream) {
HydrologyGridNode node = grid.node(label.state.index());
points.add(new HydrologyPoint(node.x(), heads[label.state.id()], node.z()));
}
return List.copyOf(points);
}
int[] contributions(int source) {
int count = 0;
for (Label label = selected[source]; label != null; label = label.downstream) {
@@ -1,31 +1,10 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.surface.SurfaceBankSupport;
import java.util.Arrays;
final class HydrologyRegionalHydraulics {
private static final int MAXIMUM_WIDTH_CANDIDATES = 8;
private static final int MAXIMUM_ADDITIONAL_SAMPLES = 2048;
private static final int MAXIMUM_BANK_RADIUS = 96;
private final HydrologyPlannerSettings settings;
private final SurfaceBankSupport bankSupport;
private final double minimumHalfWidth;
private final double maximumProbedHalfWidth;
private final double[] candidateHalfWidths;
HydrologyRegionalHydraulics(HydrologyPlannerSettings settings) {
this.settings = settings;
this.bankSupport = new SurfaceBankSupport(settings.surface(), settings.seaLevel());
HydrologyPlannerSettings.Surface surface = settings.surface();
this.minimumHalfWidth = surface.minimumWidth() / 2D;
double authored = surface.maximumWidth() / 2D;
double expanded = surface.maximumWidth();
this.maximumProbedHalfWidth = Math.min(expanded, Math.max(minimumHalfWidth,
(MAXIMUM_BANK_RADIUS - 2D) / (1D + surface.banks().roughness())));
this.candidateHalfWidths = candidates(authored, midpoint(minimumHalfWidth, authored),
maximumProbedHalfWidth, midpoint(minimumHalfWidth, maximumProbedHalfWidth));
}
int minimumHead(HydrologyTerrainSample terrain) {
@@ -41,90 +20,6 @@ final class HydrologyRegionalHydraulics {
return Math.max(settings.seaLevel(), terrain.naturalHeight() - settings.surface().banks().sink());
}
int supportedHead(HeadStation station, HydrologyTerrainSampler sampler) {
int centerHead = maximumHead(station.terrain());
if (centerHead >= station.incomingHead()) {
return station.incomingHead();
}
int supported = support(station, sampler, minimumHalfWidth, centerHead);
if (supported >= station.incomingHead()) {
return station.incomingHead();
}
SampleBudget budget = new SampleBudget(sampler);
double localMinimum = effectiveHalfWidth(settings.surface().minimumWidth(), station.terrain().widthMultiplier());
double localMaximum = effectiveHalfWidth(settings.surface().maximumWidth(), station.terrain().widthMultiplier());
if (additional(localMaximum)) {
supported = support(station, budget, localMaximum, supported);
}
if (supported < station.incomingHead() && localMinimum != localMaximum && additional(localMinimum)) {
supported = support(station, budget, localMinimum, supported);
}
double localMiddle = midpoint(localMinimum, localMaximum);
if (supported < station.incomingHead() && localMiddle != localMinimum && localMiddle != localMaximum && additional(localMiddle)) {
supported = support(station, budget, localMiddle, supported);
}
for (double halfWidth : candidateHalfWidths) {
if (supported >= station.incomingHead() || budget.remaining == 0) {
break;
}
supported = support(station, budget, halfWidth, supported);
}
return Math.min(station.incomingHead(), supported);
}
private int support(HeadStation station, HydrologyTerrainSampler sampler, double halfWidth, int supportedHead) {
SurfaceBankSupport.Station crossSection = new SurfaceBankSupport.Station(station.x(), station.z(),
station.tangentX(), station.tangentZ(), halfWidth);
SurfaceBankSupport.CrossSection support = bankSupport.crossSection(sampler, crossSection, true);
if (support.blocked() || support.minimum() - settings.surface().banks().sink() <= supportedHead) {
return supportedHead;
}
SurfaceBankSupport.Perimeter perimeter = bankSupport.perimeter(sampler, crossSection, support.minimum());
if (!perimeter.complete()) {
return supportedHead;
}
int supported = perimeter.minimum() - settings.surface().banks().sink();
return Math.max(supportedHead, supported);
}
private double effectiveHalfWidth(int width, double multiplier) {
return Math.max(settings.surface().minimumWidth(), Math.min(settings.surface().maximumWidth() * 2D, width * multiplier)) / 2D;
}
private boolean additional(double halfWidth) {
if (halfWidth <= minimumHalfWidth || halfWidth > maximumProbedHalfWidth) {
return false;
}
for (double candidate : candidateHalfWidths) {
if (candidate == halfWidth) {
return false;
}
}
return true;
}
private double[] candidates(double... widths) {
double[] distinct = new double[MAXIMUM_WIDTH_CANDIDATES - 4];
int size = 0;
for (double width : widths) {
if (!Double.isFinite(width) || width <= minimumHalfWidth || width > maximumProbedHalfWidth) {
continue;
}
boolean duplicate = false;
for (int index = 0; index < size; index++) {
duplicate |= distinct[index] == width;
}
if (!duplicate && size < distinct.length) {
distinct[size++] = width;
}
}
return Arrays.copyOf(distinct, size);
}
private static double midpoint(double first, double second) {
return first + (second - first) / 2D;
}
private int minimumHead(HydrologyTerrainSample terrain, int incision) {
int permitted = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
int depth = (int) StrictMath.round(Math.max(1D, Math.min(settings.surface().maximumDepth() * 2D,
@@ -132,24 +27,4 @@ final class HydrologyRegionalHydraulics {
return Math.max(settings.seaLevel(), terrain.naturalHeight() - permitted + depth);
}
record HeadStation(int x, int z, double tangentX, double tangentZ, HydrologyTerrainSample terrain, int incomingHead) {
}
private static final class SampleBudget implements HydrologyTerrainSampler {
private final HydrologyTerrainSampler sampler;
private int remaining = MAXIMUM_ADDITIONAL_SAMPLES;
private SampleBudget(HydrologyTerrainSampler sampler) {
this.sampler = sampler;
}
@Override
public HydrologyTerrainSample sample(int blockX, int blockZ) {
if (remaining == 0) {
return null;
}
remaining--;
return sampler.sample(blockX, blockZ);
}
}
}
@@ -475,24 +475,25 @@ final class HydrologyRegionalPlanner {
List<Integer> indices = graph.path(tree, source);
OutletCandidate destination = tree.outlets().get(tree.root(source));
RiverOutlet outlet = destination.outlet();
ArrayList<HydrologyPoint> guide = new ArrayList<>(indices.size() + 1);
ArrayList<HydrologyPoint> guide = new ArrayList<>(tree.centerline(grid, source));
ArrayList<String> profiles = new ArrayList<>(grid.node(source).terrain().preferredProfileKeys());
for (int index : indices) {
HydrologyGridNode node = grid.node(index);
guide.add(node.naturalPoint());
profiles.retainAll(node.terrain().preferredProfileKeys());
}
profiles.retainAll(sample(outlet.connectionPoint().x(), outlet.connectionPoint().z()).preferredProfileKeys());
if (origin != null) {
profiles.retainAll(sample(origin.outlet().connectionPoint().x(), origin.outlet().connectionPoint().z()).preferredProfileKeys());
guide.set(0, origin.outlet().landwardPoint());
HydrologyPoint start = origin.outlet().landwardPoint();
guide.set(0, new HydrologyPoint(start.x(), planner.settings.seaLevel(), start.z()));
}
if (profiles.isEmpty() || guide.size() < 2) {
diagnostic(diagnostics, grid.node(source).id(), grid.node(source).naturalPoint(), origin != null,
HydrologyCandidateRejection.POLICY_EXCLUDED, guide.size());
return HydrologyRegionalNetwork.EMPTY;
}
guide.set(guide.size() - 1, outlet.landwardPoint());
HydrologyPoint end = outlet.landwardPoint();
guide.set(guide.size() - 1, new HydrologyPoint(end.x(), guide.getLast().y(), end.z()));
String profile = profiles.get(HydrologyHash.between(HydrologyHash.mix(basinSeed, source), 0, profiles.size() - 1));
BuildContext context = new BuildContext(grid.node(source), origin, basinSeed, outlet, profile,
new HydrologyRegionalFlow(guide, tree.contributions(source)),
@@ -18,41 +18,32 @@ final class HydrologyRegionalRoute {
private static final int MAXIMUM_CORRIDOR_WORKERS = 8;
private final HydrologyPlanner planner;
private final HydrologyRegionalTerrainRefiner terrainRefiner;
private final HydrologyRegionalHydraulics hydraulics;
private final HydrologyRegionalTerrain terrain;
private final SimplexNoise bends;
private final SimplexNoise details;
HydrologyRegionalRoute(HydrologyPlanner planner) {
this.planner = planner;
this.terrainRefiner = new HydrologyRegionalTerrainRefiner(planner);
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
this.terrain = new HydrologyRegionalTerrain(planner);
this.bends = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x52454742454e4453L));
this.details = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x5245474445544149L));
}
void clear() {
terrainRefiner.clear();
terrain.clear();
}
Attempt select(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver,
CandidateAdmission admission) {
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide, terrainRefiner::sample, planner.settings);
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide, this.terrain::sample, planner.settings);
Refinement topology = validateCorridor(guide, profile, coastal);
OceanEntry resolved = topology.rejection() == null ? topology.oceanEntry() : null;
if (resolved != null) {
guide = topology.points();
receiver = HydrologyOceanReceiver.forConnection(planner.settings, terrainRefiner::sample,
receiver = HydrologyOceanReceiver.forConnection(planner.settings, this.terrain::sample,
resolved.landward(), resolved.receiving());
}
CandidateSelection selection = new CandidateSelection(admission, resolved);
List<HydrologyPoint> repaired = terrainRefiner.refine(guide, profile, coastal, receiver);
if (!repaired.isEmpty() && !repaired.equals(guide)) {
Attempt accepted = selectCurve(repaired, profile, coastal, receiver, selection, morphology);
if (accepted != null) {
return accepted;
}
}
Attempt accepted = selectCurve(guide, profile, coastal, receiver, selection, morphology);
return accepted == null ? selection.failure : accepted;
}
@@ -396,7 +387,7 @@ final class HydrologyRegionalRoute {
terrain.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth(), null);
}
previousTerrain = terrain;
previousPoint = failure;
previousPoint = new HydrologyPoint(x, (int) StrictMath.round(first.y() + (second.y() - first.y()) * progress), z);
}
length += StrictMath.sqrt(first.distanceSquared2D(second));
}
@@ -419,7 +410,7 @@ final class HydrologyRegionalRoute {
if (++samples > MAXIMUM_RECEIVER_SAMPLES) {
return new Refinement(List.of(), previousPoint, HydrologyCandidateRejection.ROUTE_LIMIT, samples, null);
}
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
HydrologyTerrainSample terrain = this.terrain.sample(x, z);
HydrologyPoint point = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()) {
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, 1, null);
@@ -453,7 +444,7 @@ final class HydrologyRegionalRoute {
private boolean wetBridge(int x, int z, HydrologyTerrainSample previous, HydrologyTerrainSample next,
String profile, boolean coastal) {
HydrologyTerrainSample bridge = terrainRefiner.sample(x, z);
HydrologyTerrainSample bridge = this.terrain.sample(x, z);
return bridge != null && bridge.naturalHeight() < planner.settings.seaLevel()
&& bridge.preferredProfileKeys().contains(profile) && previous.drainsInto(bridge) && bridge.drainsInto(next)
&& (!coastal || next.drainsInto(bridge) && bridge.drainsInto(previous));
@@ -465,7 +456,7 @@ final class HydrologyRegionalRoute {
HydrologyPoint landward = new HydrologyPoint(shore.point().x(), seaLevel, shore.point().z());
receiving = new HydrologyPoint(receiving.x(), seaLevel, receiving.z());
HydrologyTerrainSampler proof = HydrologyOceanReceiver.forConnection(planner.settings,
terrainRefiner::sample, landward, receiving);
this.terrain::sample, landward, receiving);
if (!shore.terrain().outletAllowed() || !proof.receivingWater(firstWet.x(), firstWet.z(), seaLevel)
|| !receivingChordAllowed(shore, receiving, profile, coastal)) {
return new Refinement(List.of(), firstWet, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
@@ -489,7 +480,7 @@ final class HydrologyRegionalRoute {
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);
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
HydrologyTerrainSample terrain = this.terrain.sample(x, z);
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()
|| !terrain.preferredProfileKeys().contains(profile) || !previous.drainsInto(terrain)
|| coastal && !terrain.drainsInto(previous)) {
@@ -517,30 +508,32 @@ final class HydrologyRegionalRoute {
Math.abs(points.get(index).z() - points.get(index - 1).z()));
}
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
boolean oceanMouth = corridor.oceanEntry() != null || terrainRefiner.receivingTerminal(points.getLast(), profile, false, receiver);
int reach = oceanMouth ? Math.min(inlet.length(), (int) StrictMath.floor(count * inlet.courseFraction())) : 0;
boolean ocean = corridor.oceanEntry() != null || terrain.receivingTerminal(points.getLast(), profile, false, receiver);
int reach = ocean ? Math.min(inlet.length(), (int) StrictMath.floor(count * inlet.courseFraction())) : 0;
int rampStart = count - reach - reach / 2;
int availableHead = Integer.MAX_VALUE;
int station = 0;
HydrologyTerrainSampler bankSampler = terrainRefiner::sample;
for (int index = 0; index + 1 < points.size(); index++) {
HydrologyPoint first = points.get(index);
HydrologyPoint second = points.get(index + 1);
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
double distance = StrictMath.sqrt(first.distanceSquared2D(second));
double tangentX = distance == 0D ? 1D : (second.x() - first.x()) / distance;
double tangentZ = distance == 0D ? 0D : (second.z() - first.z()) / distance;
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
double progress = steps == 0 ? 0D : step / (double) steps;
int x = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
int z = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
availableHead = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
x, z, tangentX, tangentZ, terrain, availableHead), bankSampler);
int requiredHead = station >= rampStart ? hydraulics.inletMinimumHead(terrain) : hydraulics.minimumHead(terrain);
if (requiredHead > availableHead) {
return new Refinement(List.of(), new HydrologyPoint(x, terrain.naturalHeight(), z),
HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, requiredHead - availableHead, null);
if (first.y() < second.y()) {
return new Refinement(List.of(), second, HydrologyCandidateRejection.SURFACE_HEAD_RANGE,
second.y() - first.y(), null);
}
List<HydrologyPoint> raster = planner.segments.rasterLine(first, second);
for (int step = index == 0 ? 0 : 1; step < raster.size(); step++) {
HydrologyPoint sampled = raster.get(step);
double progress = raster.size() < 2 ? 0D : step / (double) (raster.size() - 1);
HydrologyPoint point = new HydrologyPoint(sampled.x(),
(int) StrictMath.round(first.y() + (second.y() - first.y()) * progress), sampled.z());
HydrologyTerrainSample terrain = this.terrain.sample(point.x(), point.z());
int incision = terrain.surfacePolicy().maximumIncision(planner.settings.surface().maximumIncision());
if (station >= rampStart) {
incision = Math.max(incision, inlet.maximumIncision());
}
int permitted = HydrologySourcePlanner.permittedSurfaceIncision(incision, terrain.incisionMultiplier());
int cut = terrain.naturalHeight() - point.y() + planner.settings.surface().minimumDepth();
if (cut > permitted) {
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, cut, null);
}
station++;
}
@@ -596,7 +589,7 @@ final class HydrologyRegionalRoute {
private HydrologyTerrainSample next(int segment, int step, int x, int z) {
if (workers == 1) {
return terrainRefiner.sample(x, z);
return HydrologyRegionalRoute.this.terrain.sample(x, z);
}
if (cursor == count) {
fill(segment, step);
@@ -643,7 +636,7 @@ final class HydrologyRegionalRoute {
private void sampleRange(int start, int end) {
for (int index = start; index < end; index++) {
try {
terrain[index] = terrainRefiner.sample(xs[index], zs[index]);
terrain[index] = HydrologyRegionalRoute.this.terrain.sample(xs[index], zs[index]);
} catch (RuntimeException failure) {
failures[index] = failure;
}
@@ -0,0 +1,66 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
final class HydrologyRegionalTerrain implements HydrologyTerrainSampler {
private static final int MAXIMUM_CACHED_SAMPLES = 4194304;
private static final int[][] CARDINAL = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
private final HydrologyPlanner planner;
private final HydrologyCacheBudget budget;
private volatile Cache<Long, Terrain> terrain;
HydrologyRegionalTerrain(HydrologyPlanner planner) {
this.planner = planner;
this.budget = HydrologyCacheBudget.runtime(planner.settings.routing().regional().enabled());
this.terrain = cache();
}
void clear() {
terrain = cache();
}
@Override
public HydrologyTerrainSample sample(int x, int z) {
Cache<Long, Terrain> active = terrain;
long key = CacheKey.mix(RiverFootprint.pack(x, z));
Terrain cached = active.getIfPresent(key);
if (cached != null) {
return cached.sample();
}
Terrain sampled = new Terrain(planner.naturalSampler == null
? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z));
Terrain previous = active.asMap().putIfAbsent(key, sampled);
return (previous == null ? sampled : previous).sample();
}
boolean receivingTerminal(HydrologyPoint point, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
HydrologyTerrainSample land = sample(point.x(), point.z());
if (land == null || land.ocean() || land.naturalHeight() < planner.settings.seaLevel() || !land.outletAllowed()) {
return false;
}
for (int index = 0; index < 4; index++) {
int[] offset = CARDINAL[index];
HydrologyTerrainSample ocean = sample(point.x() + offset[0], point.z() + offset[1]);
if (ocean != null && receiver.receivingWater(point.x() + offset[0], point.z() + offset[1], planner.settings.seaLevel())
&& ocean.preferredProfileKeys().contains(profile) && land.drainsInto(ocean)
&& (!coastal || ocean.drainsInto(land))) {
return true;
}
}
return false;
}
private Cache<Long, Terrain> cache() {
return Caffeine.newBuilder()
.maximumWeight(budget.regionalTerrainBytes())
.weigher((Long key, Terrain value) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.terrain(value.sample()), budget.regionalTerrainBytes(),
(int) Math.max(65536L, Math.min(MAXIMUM_CACHED_SAMPLES, Runtime.getRuntime().maxMemory() / 8192L))))
.build();
}
private record Terrain(HydrologyTerrainSample sample) {
}
}
@@ -1,514 +0,0 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;
final class HydrologyRegionalTerrainRefiner {
private static final int GRID_SPACING = 16;
private static final int MAXIMUM_EXPANSIONS = 4096;
private static final int MAXIMUM_SAMPLES = 65536;
private static final int MAXIMUM_CACHED_SAMPLES = 4194304;
private static final int MAXIMUM_SKIPPED_WAYPOINTS = 2;
private static final int[][] NEIGHBOURS = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {-1, 1}, {-1, -1}, {1, -1}};
private final HydrologyPlanner planner;
private final HydrologyRegionalHydraulics hydraulics;
private final HydrologyCacheBudget cacheBudget;
private volatile CacheState caches;
HydrologyRegionalTerrainRefiner(HydrologyPlanner planner) {
this.planner = planner;
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
this.cacheBudget = HydrologyCacheBudget.runtime(planner.settings.routing().regional().enabled());
this.caches = new CacheState(cacheBudget);
}
void clear() {
caches = new CacheState(cacheBudget);
}
HydrologyTerrainSample sample(int x, int z) {
return sample(caches.terrain, x, z);
}
private HydrologyTerrainSample sample(Cache<Long, Terrain> cache, int x, int z) {
long key = CacheKey.mix(RiverFootprint.pack(x, z));
Terrain cached = cache.getIfPresent(key);
if (cached != null) {
return cached.sample();
}
Terrain sampled = new Terrain(planner.naturalSampler == null
? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z));
Terrain existing = cache.asMap().putIfAbsent(key, sampled);
return (existing == null ? sampled : existing).sample();
}
List<HydrologyPoint> refine(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
if (guide.size() < 2) {
return List.of();
}
double maximumLength = planner.settings.routing().maximumRouteLength();
double remaining = 0D;
for (int index = 1; index < guide.size(); index++) {
remaining += distance(guide.get(index - 1), guide.get(index));
if (remaining > maximumLength) {
return List.of();
}
}
Terminal terminal = terminal(guide.getLast(), profile, coastal, receiver);
int[] requiredHead = requiredHeads(guide, terminal, MAXIMUM_SKIPPED_WAYPOINTS + 1);
if (requiredHead.length == 0) {
return List.of();
}
int availableHead = Integer.MAX_VALUE;
double length = 0D;
ArrayList<HydrologyPoint> refined = new ArrayList<>();
refined.add(guide.getFirst());
Long2IntOpenHashMap stations = new Long2IntOpenHashMap();
stations.defaultReturnValue(-1);
stations.put(RiverFootprint.pack(guide.getFirst().x(), guide.getFirst().z()), 0);
for (int index = 1; index < guide.size(); index++) {
int start = index - 1;
Reach reach = null;
int selected = index;
for (int candidate = index; candidate < guide.size() && candidate <= index + MAXIMUM_SKIPPED_WAYPOINTS; candidate++) {
Edge edge = new Edge(guide.get(start), guide.get(candidate), profile, coastal,
requiredHead[candidate], availableHead, terminal);
reach = edge(edge);
if (!reach.points().isEmpty()) {
selected = candidate;
break;
}
}
if (reach.points().isEmpty()) {
return List.of();
}
List<HydrologyPoint> points = reach.points();
availableHead = reach.availableHead();
for (int skipped = start; skipped < selected; skipped++) {
remaining -= distance(guide.get(skipped), guide.get(skipped + 1));
}
index = selected;
for (int point = 1; point < points.size(); point++) {
HydrologyPoint next = points.get(point);
long key = RiverFootprint.pack(next.x(), next.z());
int previous = stations.get(key);
if (previous >= 0) {
while (refined.size() > previous + 1) {
HydrologyPoint removed = refined.removeLast();
stations.remove(RiverFootprint.pack(removed.x(), removed.z()));
length -= distance(refined.getLast(), removed);
}
} else {
length += distance(refined.getLast(), next);
stations.put(key, refined.size());
refined.add(next);
}
if (length + Math.max(0D, remaining) > maximumLength) {
return List.of();
}
}
}
return simplify(refined, profile, coastal, terminal);
}
boolean receivingTerminal(HydrologyPoint point, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
HydrologyTerrainSample land = sample(point.x(), point.z());
if (land == null || land.ocean() || land.naturalHeight() < planner.settings.seaLevel() || !land.outletAllowed()) {
return false;
}
for (int index = 0; index < 4; index++) {
int[] offset = NEIGHBOURS[index];
HydrologyTerrainSample ocean = sample(point.x() + offset[0], point.z() + offset[1]);
if (ocean != null && receiver.receivingWater(point.x() + offset[0], point.z() + offset[1], planner.settings.seaLevel())
&& ocean.preferredProfileKeys().contains(profile) && land.drainsInto(ocean)
&& (!coastal || ocean.drainsInto(land))) {
return true;
}
}
return false;
}
private Terminal terminal(HydrologyPoint point, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
int inlet = receivingTerminal(point, profile, coastal, receiver) ? planner.settings.surface().banks().inlet().length() : 0;
return new Terminal(point, inlet + inlet / 2);
}
private int minimumHead(HydrologyTerrainSample terrain, int x, int z, Terminal terminal) {
boolean inlet = terminal.possibleStations() > 0
&& Math.max(Math.abs(x - terminal.landward().x()), Math.abs(z - terminal.landward().z())) < terminal.possibleStations();
return inlet ? hydraulics.inletMinimumHead(terrain) : hydraulics.minimumHead(terrain);
}
private int[] requiredHeads(List<HydrologyPoint> points, Terminal terminal, int maximumAdvance) {
int[] heads = new int[points.size()];
for (int index = points.size() - 1; index >= 0; index--) {
HydrologyPoint point = points.get(index);
HydrologyTerrainSample terrain = sample(point.x(), point.z());
if (terrain == null) {
return new int[0];
}
int required = planner.settings.seaLevel();
if (index + 1 < points.size()) {
required = Integer.MAX_VALUE;
for (int downstream = index + 1; downstream < points.size() && downstream <= index + maximumAdvance; downstream++) {
required = Math.min(required, heads[downstream]);
}
}
heads[index] = Math.max(required, minimumHead(terrain, point.x(), point.z(), terminal));
}
return heads;
}
private List<HydrologyPoint> simplify(List<HydrologyPoint> points, String profile, boolean coastal, Terminal terminal) {
double maximumReach = Math.min(512D, planner.settings.routing().regional().sampleSpacing() * 2D);
Samples samples = new Samples();
ArrayList<HydrologyPoint> simplified = new ArrayList<>();
int current = 0;
int[] requiredHead = requiredHeads(points, terminal, 1);
if (requiredHead.length == 0) {
return List.of();
}
int availableHead = Integer.MAX_VALUE;
simplified.add(points.getFirst());
while (current + 1 < points.size()) {
int farthest = current + 1;
double reach = distance(points.get(current), points.get(farthest));
while (farthest + 1 < points.size()) {
double next = distance(points.get(farthest), points.get(farthest + 1));
if (reach + next > maximumReach) {
break;
}
reach += next;
farthest++;
}
int selected = current + 1;
Traversal selectedTraversal = traverse(points.get(current), points.get(selected),
new Edge(points.get(current), points.get(selected), profile, coastal, requiredHead[selected], availableHead, terminal),
samples, availableHead);
for (int candidate = farthest; candidate > current + 1; candidate--) {
Edge shortcut = new Edge(points.get(current), points.get(candidate), profile, coastal,
requiredHead[candidate], availableHead, terminal);
Traversal traversal = traverse(shortcut.start(), shortcut.end(), shortcut, samples, availableHead);
if (traversal.cost() == 0D) {
selected = candidate;
selectedTraversal = traversal;
break;
}
}
if (!Double.isFinite(selectedTraversal.cost())) {
return List.copyOf(points);
}
availableHead = selectedTraversal.availableHead();
current = selected;
simplified.add(points.get(current));
}
return List.copyOf(simplified);
}
private Reach route(Edge edge) {
Samples samples = new Samples();
Traversal direct = traverse(edge.start(), edge.end(), edge, samples, edge.maximumHead());
if (direct.cost() == 0D) {
return new Reach(List.of(edge.start(), edge.end()), direct.availableHead());
}
int margin = planner.settings.routing().regional().sampleSpacing();
int minimumX = Math.min(edge.start().x(), edge.end().x()) - margin;
int maximumX = Math.max(edge.start().x(), edge.end().x()) + margin;
int minimumZ = Math.min(edge.start().z(), edge.end().z()) - margin;
int maximumZ = Math.max(edge.start().z(), edge.end().z()) + margin;
Long2ObjectOpenHashMap<List<Node>> visited = new Long2ObjectOpenHashMap<>();
PriorityQueue<Pending> pending = new PriorityQueue<>(Comparator.comparingDouble(Pending::estimate)
.thenComparingLong(Pending::key)
.thenComparing(Comparator.comparingInt(Pending::availableHead).reversed()));
long firstKey = RiverFootprint.pack(edge.start().x(), edge.start().z());
long lastKey = RiverFootprint.pack(edge.end().x(), edge.end().z());
Search search = new Search(edge, samples, visited, pending);
Node first = new Node(edge.start(), 0D, edge.maximumHead(), null);
ArrayList<Node> firstLabels = new ArrayList<>(1);
firstLabels.add(first);
visited.put(firstKey, firstLabels);
pending.add(new Pending(firstKey, first, distance(edge.start(), edge.end())));
for (int expanded = 0; !pending.isEmpty() && expanded < MAXIMUM_EXPANSIONS; expanded++) {
Pending next = pending.remove();
Node current = next.node();
if (!retained(visited.get(next.key()), current)) {
continue;
}
if (next.key() == lastKey) {
return new Reach(path(current), current.availableHead());
}
if (distance(current.point(), edge.end()) <= GRID_SPACING * StrictMath.sqrt(2D)) {
offer(search, current, edge.end());
}
for (int[] offset : NEIGHBOURS) {
int x = current.point().x() + offset[0] * GRID_SPACING;
int z = current.point().z() + offset[1] * GRID_SPACING;
if (x < minimumX || x > maximumX || z < minimumZ || z > maximumZ) {
continue;
}
offer(search, current, new HydrologyPoint(x, current.point().y(), z));
}
}
return new Reach(List.of(), edge.maximumHead());
}
private Reach edge(Edge edge) {
CacheState state = caches;
Reach cached = state.edges.getIfPresent(edge);
if (cached != null) {
return cached;
}
HydrologyForkJoin.Task<Reach> task = new HydrologyForkJoin.Task<>(() -> {
try {
Reach present = state.edges.getIfPresent(edge);
Reach result = present == null ? route(edge) : present;
state.edges.put(edge, result);
return result;
} finally {
state.loading.remove(edge);
}
});
HydrologyForkJoin.Task<Reach> existing = state.loading.putIfAbsent(edge, task);
return (existing == null ? task : existing).await();
}
private void offer(Search search, Node current, HydrologyPoint target) {
applyOffer(search, current, evaluateOffer(search, current, target));
}
private Offer evaluateOffer(Search search, Node current, HydrologyPoint target) {
Samples samples = search.samples();
HydrologyTerrainSample terrain = samples.sample(target.x(), target.z());
if (!allowed(terrain, search.edge())) {
return null;
}
HydrologyPoint point = new HydrologyPoint(target.x(), terrain.naturalHeight(), target.z());
double cost = current.cost() + distance(current.point(), point)
+ Math.max(0D, point.y() - current.point().y()) * planner.settings.routing().uphillPenalty()
+ Math.max(0D, terrain.routingCost() * terrain.routingMultiplier());
long key = RiverFootprint.pack(point.x(), point.z());
List<Node> labels = search.visited().get(key);
if (dominated(labels, cost, current.availableHead())) {
return null;
}
Traversal traversal = traverse(current.point(), point, search.edge(), samples, current.availableHead());
cost += traversal.cost();
if (!Double.isFinite(cost) || dominated(labels, cost, traversal.availableHead())) {
return null;
}
cost += clearanceCost(point, search.edge(), samples);
if (dominated(labels, cost, traversal.availableHead())) {
return null;
}
return new Offer(point, cost, traversal.availableHead());
}
private void applyOffer(Search search, Node current, Offer offer) {
if (offer == null) {
return;
}
long key = RiverFootprint.pack(offer.point().x(), offer.point().z());
List<Node> labels = search.visited().get(key);
if (labels == null) {
labels = new ArrayList<>(2);
search.visited().put(key, labels);
}
for (int index = labels.size() - 1; index >= 0; index--) {
Node previous = labels.get(index);
if (offer.cost() <= previous.cost() && offer.availableHead() >= previous.availableHead()) {
labels.remove(index);
}
}
Node retained = new Node(offer.point(), offer.cost(), offer.availableHead(), current);
labels.add(retained);
search.pending().add(new Pending(key, retained, offer.cost() + distance(offer.point(), search.edge().end())));
}
private static boolean dominated(List<Node> labels, double cost, int availableHead) {
if (labels == null) {
return false;
}
for (Node previous : labels) {
if (previous.cost() <= cost && previous.availableHead() >= availableHead) {
return true;
}
}
return false;
}
private static boolean retained(List<Node> labels, Node current) {
for (Node retained : labels) {
if (retained == current) {
return true;
}
}
return false;
}
private Traversal traverse(HydrologyPoint start, HydrologyPoint end, Edge edge,
Samples samples, int availableHead) {
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
HydrologyTerrainSample previous = null;
double distance = distance(start, end);
double tangentX = distance == 0D ? 1D : (end.x() - start.x()) / distance;
double tangentZ = distance == 0D ? 0D : (end.z() - start.z()) / distance;
double cost = 0D;
for (int step = 0; step <= steps; step++) {
double progress = steps == 0 ? 0D : 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);
HydrologyTerrainSample terrain = samples.sample(x, z);
if (!allowed(terrain, edge) || previous != null && (!previous.drainsInto(terrain)
|| edge.coastal() && !terrain.drainsInto(previous))) {
return new Traversal(Double.POSITIVE_INFINITY, availableHead);
}
if (!edge.coastal()) {
availableHead = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
x, z, tangentX, tangentZ, terrain, availableHead), samples);
if (availableHead < Math.max(edge.minimumHead(), minimumHead(terrain, x, z, edge.terminal()))) {
return new Traversal(Double.POSITIVE_INFINITY, availableHead);
}
}
cost += ridgeCost(x, z, terrain, edge);
previous = terrain;
}
return new Traversal(cost, availableHead);
}
private double ridgeCost(int x, int z, HydrologyTerrainSample terrain, Edge edge) {
if (edge.coastal()) {
return 0D;
}
double deltaX = edge.end().x() - edge.start().x();
double deltaZ = edge.end().z() - edge.start().z();
double squared = deltaX * deltaX + deltaZ * deltaZ;
double progress = squared == 0D ? 0D : Math.max(0D, Math.min(1D,
((x - edge.start().x()) * deltaX + (z - edge.start().z()) * deltaZ) / squared));
double envelope = edge.start().y() + (edge.end().y() - edge.start().y()) * progress;
int incision = terrain.surfacePolicy().maximumIncision(planner.settings.surface().maximumIncision());
double permitted = Math.min(incision, StrictMath.floor(incision * terrain.incisionMultiplier()));
double excess = terrain.naturalHeight() - envelope + planner.settings.surface().minimumDepth()
+ planner.settings.surface().banks().sink() - permitted;
return Math.max(0D, excess) * Math.max(1D, planner.settings.routing().uphillPenalty());
}
private double clearanceCost(HydrologyPoint point, Edge edge, Samples samples) {
double radius = planner.settings.surface().maximumWidth() / 2D
* (1D + planner.settings.surface().banks().roughness()) + 1D;
int blocked = 0;
for (int[] offset : NEIGHBOURS) {
double scale = offset[0] != 0 && offset[1] != 0 ? radius / StrictMath.sqrt(2D) : radius;
int x = point.x() + (int) StrictMath.round(offset[0] * scale);
int z = point.z() + (int) StrictMath.round(offset[1] * scale);
if (!allowed(samples.sample(x, z), edge)) {
blocked++;
}
}
return blocked * GRID_SPACING / 2D;
}
private boolean allowed(HydrologyTerrainSample terrain, Edge edge) {
return terrain != null && !terrain.ocean() && terrain.naturalHeight() >= planner.settings.seaLevel() && terrain.transitAllowed()
&& terrain.preferredProfileKeys().contains(edge.profile())
&& (!edge.coastal() || terrain.naturalHeight() - planner.settings.seaLevel()
+ planner.settings.surface().minimumDepth()
<= planner.settings.routing().regional().maximumCoastalIncision());
}
private static List<HydrologyPoint> path(Node current) {
ArrayList<HydrologyPoint> points = new ArrayList<>();
while (current != null) {
points.add(current.point());
current = current.parent();
}
Collections.reverse(points);
return List.copyOf(points);
}
private static double distance(HydrologyPoint first, HydrologyPoint second) {
return StrictMath.sqrt(first.distanceSquared2D(second));
}
final class Samples implements HydrologyTerrainSampler {
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> values = new Long2ObjectOpenHashMap<>();
private final Cache<Long, Terrain> terrainCache = caches.terrain;
@Override
public HydrologyTerrainSample sample(int x, int z) {
long key = RiverFootprint.pack(x, z);
HydrologyTerrainSample sampled = values.get(key);
if (sampled != null || values.containsKey(key)) {
return sampled;
}
if (values.size() >= MAXIMUM_SAMPLES) {
return null;
}
sampled = HydrologyRegionalTerrainRefiner.this.sample(terrainCache, x, z);
values.put(key, sampled);
return sampled;
}
}
private static final class CacheState {
private final Cache<Edge, Reach> edges;
private final ConcurrentHashMap<Edge, HydrologyForkJoin.Task<Reach>> loading = new ConcurrentHashMap<>();
private final Cache<Long, Terrain> terrain;
private CacheState(HydrologyCacheBudget budget) {
edges = Caffeine.newBuilder()
.maximumWeight(budget.regionalReachBytes())
.weigher((Edge edge, Reach reach) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.points(reach.points()), budget.regionalReachBytes(), 128))
.build();
terrain = Caffeine.newBuilder()
.maximumWeight(budget.regionalTerrainBytes())
.weigher((Long key, Terrain terrain) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.terrain(terrain.sample()), budget.regionalTerrainBytes(),
(int) Math.max(MAXIMUM_SAMPLES, Math.min(MAXIMUM_CACHED_SAMPLES, Runtime.getRuntime().maxMemory() / 8192L))))
.build();
}
}
private record Offer(HydrologyPoint point, double cost, int availableHead) {
}
private record Edge(HydrologyPoint start, HydrologyPoint end, String profile, boolean coastal,
int minimumHead, int maximumHead, Terminal terminal) {
}
private record Terminal(HydrologyPoint landward, int possibleStations) {
}
private record Reach(List<HydrologyPoint> points, int availableHead) {
}
private record Traversal(double cost, int availableHead) {
}
private record Terrain(HydrologyTerrainSample sample) {
}
private record Search(Edge edge, Samples samples, Long2ObjectOpenHashMap<List<Node>> visited,
PriorityQueue<Pending> pending) {
}
private record Node(HydrologyPoint point, double cost, int availableHead, Node parent) {
}
private record Pending(long key, Node node, double estimate) {
private int availableHead() {
return node.availableHead();
}
}
}
@@ -15,7 +15,6 @@ final class HydrologyRouteGeometry {
this.planner = planner;
}
static final double CROSS_DROP_WEIGHT = 3D;
static final long ROUTE_ANCHOR_X_SALT = 0x52414e434858L;
static final long ROUTE_ANCHOR_Z_SALT = 0x52414e43485aL;
static final long ROUTE_WORM_PRIMARY_SALT = 0x52575052494dL;
@@ -24,22 +23,17 @@ final class HydrologyRouteGeometry {
static final double ROUTE_TANGENT_SCALE = 0.68D;
static final int ROUTE_TRANSVERSE_CANDIDATES = 2;
static final int ROUTE_FALLBACK_TRANSVERSE_CANDIDATES = 4;
static final int SURFACE_COURSE_TRANSVERSE_CANDIDATES = 2;
static final double SURFACE_COURSE_MAXIMUM_TURN_DEGREES = 50D;
static final double SURFACE_COURSE_TURN_COST = 2.5D;
static final int SURFACE_MINIMUM_NATURAL_BANK_RISE = 1;
static final int MAXIMUM_SURFACE_BORE_STEP = 4;
HydrologyPoint edgeContinuation(
HydrologySampledGrid grid,
HydrologyRoutingPlan routing,
HydrologyGridNode downstream,
RiverOutlet outlet,
boolean surface
RiverOutlet outlet
) {
int continuationIndex = routing.parent()[downstream.index()];
if (continuationIndex >= 0) {
return routeAnchor(grid.node(continuationIndex), surface);
return routeAnchor(grid.node(continuationIndex), false);
}
HydrologyPoint landward = outlet.landwardPoint();
if (landward.x() != downstream.x() || landward.z() != downstream.z()) {
@@ -177,17 +171,15 @@ final class HydrologyRouteGeometry {
return score + HydrologyHash.unit(tie) * 1.0E-6D;
}
List<HydrologyPoint> refineEdge(
List<HydrologyPoint> refineUndergroundEdge(
long upstreamId,
long downstreamId,
HydrologyPoint upstream,
HydrologyPoint downstream,
HydrologyPoint continuation,
int transverseCandidates,
boolean surface
int transverseCandidates
) {
int refinement = planner.settings.routing().refinementSpacing();
int effectiveTransverseCandidates = transverseCandidates;
double deltaX = downstream.x() - upstream.x();
double deltaZ = downstream.z() - upstream.z();
double distance = StrictMath.hypot(deltaX, deltaZ);
@@ -240,7 +232,7 @@ final class HydrologyRouteGeometry {
nominal,
progress,
refinement,
elevationTransition && !surface ? 0 : effectiveTransverseCandidates,
elevationTransition ? 0 : transverseCandidates,
upstreamTerrain == null ? null : upstreamTerrain.confinesKey()
);
if (candidates.isEmpty()) {
@@ -531,144 +523,6 @@ final class HydrologyRouteGeometry {
return List.copyOf(candidates.values());
}
List<RouteCandidate> unresolvedSurfaceRouteCandidates(
HydrologySampledGrid grid,
long upstreamId,
long downstreamId,
RoutePosition nominal,
double progress,
int refinement,
int transverseCandidates
) {
double searchEnvelope = StrictMath.sin(StrictMath.PI * progress);
LinkedHashMap<Long, RouteCandidate> candidates = new LinkedHashMap<>();
for (int offsetIndex = -transverseCandidates;
offsetIndex <= transverseCandidates;
offsetIndex++) {
double offset = offsetIndex * refinement * searchEnvelope;
int x = (int) StrictMath.round(nominal.x() - nominal.tangent().z() * offset);
int z = (int) StrictMath.round(nominal.z() + nominal.tangent().x() * offset);
long packed = RiverFootprint.pack(x, z);
RouteTerrainEstimate estimate = routeTerrainEstimate(grid, x, z);
double localScore = estimate.terrainScore()
+ StrictMath.abs(offset) * 0.18D
+ HydrologyHash.unit(HydrologyHash.mix(
planner.worldSeed,
upstreamId,
downstreamId,
x,
z
)) * 1.0E-6D;
RouteCandidate candidate = new RouteCandidate(
new HydrologyPoint(x, estimate.height(), z),
nominal.x() - nominal.tangent().z() * offset,
nominal.z() + nominal.tangent().x() * offset,
offset,
localScore,
estimate.terrainScore(),
nominal.tangent(),
false
);
RouteCandidate current = candidates.get(packed);
if (current == null || candidate.localScore() < current.localScore()) {
candidates.put(packed, candidate);
}
}
return List.copyOf(candidates.values());
}
RouteTerrainEstimate routeTerrainEstimate(HydrologySampledGrid grid, int x, int z) {
double gridX = (x - grid.minimumX()) / (double) grid.spacing();
double gridZ = (z - grid.minimumZ()) / (double) grid.spacing();
int minimumGridX = Math.max(0, Math.min(grid.width() - 1, (int) StrictMath.floor(gridX)));
int minimumGridZ = Math.max(0, Math.min(grid.width() - 1, (int) StrictMath.floor(gridZ)));
int maximumGridX = Math.min(grid.width() - 1, minimumGridX + 1);
int maximumGridZ = Math.min(grid.width() - 1, minimumGridZ + 1);
double progressX = Math.max(0D, Math.min(1D, gridX - minimumGridX));
double progressZ = Math.max(0D, Math.min(1D, gridZ - minimumGridZ));
HydrologyTerrainSample northwest = grid.nodeAt(minimumGridX, minimumGridZ).terrain();
HydrologyTerrainSample northeast = grid.nodeAt(maximumGridX, minimumGridZ).terrain();
HydrologyTerrainSample southwest = grid.nodeAt(minimumGridX, maximumGridZ).terrain();
HydrologyTerrainSample southeast = grid.nodeAt(maximumGridX, maximumGridZ).terrain();
double height = planner.routePaths.bilinear(
northwest.naturalHeight(),
northeast.naturalHeight(),
southwest.naturalHeight(),
southeast.naturalHeight(),
progressX,
progressZ
);
double terrainScore = planner.routePaths.bilinear(
routeTerrainScore(northwest),
routeTerrainScore(northeast),
routeTerrainScore(southwest),
routeTerrainScore(southeast),
progressX,
progressZ
);
return new RouteTerrainEstimate((int) StrictMath.round(height), terrainScore);
}
RouteCandidate resolveRouteCandidate(RouteCandidate candidate) {
if (candidate.terrainResolved()) {
return candidate;
}
HydrologyPoint point = candidate.point();
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
if (terrain == null || !terrain.transitAllowed()) {
return new RouteCandidate(
point,
candidate.continuousX(),
candidate.continuousZ(),
candidate.offset(),
Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY,
candidate.tangent(),
true
);
}
double terrainScore = routeTerrainScore(terrain) + crossDropPenalty(point, candidate.tangent(), terrain);
double exactLocalScore = candidate.localScore() - candidate.terrainScore() + terrainScore;
return new RouteCandidate(
new HydrologyPoint(point.x(), terrain.naturalHeight(), point.z()),
candidate.continuousX(),
candidate.continuousZ(),
candidate.offset(),
exactLocalScore,
terrainScore,
candidate.tangent(),
true
);
}
/**
* Ground that falls away beside a candidate forces the valley solver to cut the channel down to the
* lower bank; costing that drop keeps refined routes on valley floors and contour lines instead of
* traversing hillsides that the incision cap would later reject.
*/
double crossDropPenalty(HydrologyPoint point, RouteDirection tangent, HydrologyTerrainSample center) {
double length = StrictMath.hypot(tangent.x(), tangent.z());
if (length <= 0D) {
return 0D;
}
double normalX = -tangent.z() / length;
double normalZ = tangent.x() / length;
double reach = planner.settings.surface().maximumWidth() / 2D + planner.settings.surface().shoreWidth() + 2D;
int lowest = center.naturalHeight();
for (double distance = reach / 2D; distance <= reach; distance += reach / 2D) {
for (double direction = -1D; direction <= 1D; direction += 2D) {
double side = planner.sampleLandHeight(
(int) StrictMath.round(point.x() + normalX * distance * direction),
(int) StrictMath.round(point.z() + normalZ * distance * direction));
if (!Double.isNaN(side)) {
lowest = Math.min(lowest, (int) side);
}
}
}
double drop = center.naturalHeight() - lowest;
return drop * planner.settings.routing().valleyPreference() * CROSS_DROP_WEIGHT;
}
double routeTerrainScore(HydrologyTerrainSample terrain) {
return terrain.naturalHeight() * planner.settings.routing().valleyPreference()
+ terrain.slope() * planner.settings.routing().slopePenalty()
@@ -791,414 +645,6 @@ final class HydrologyRouteGeometry {
return null;
}
RouteCandidate[] selectCurvatureAwareTerrainRoute(
List<List<RouteCandidate>> layers,
double targetStepLength,
double maximumTurn,
double turnCost
) {
int layerCount = layers.size();
if (layerCount < 2) {
return new RouteCandidate[0];
}
double[][] localPenalties = new double[layerCount][];
double[][][] transitionCosts = new double[layerCount][][];
double[][][][] turnCosts = new double[layerCount][][][];
for (int layerIndex = 0; layerIndex < layerCount; layerIndex++) {
int layerSize = layers.get(layerIndex).size();
localPenalties[layerIndex] = new double[layerSize];
if (layerIndex > 0 && layerIndex < layerCount - 1) {
Arrays.fill(localPenalties[layerIndex], Double.NaN);
}
if (layerIndex == 0) {
continue;
}
int previousSize = layers.get(layerIndex - 1).size();
transitionCosts[layerIndex] = new double[previousSize][layerSize];
for (double[] row : transitionCosts[layerIndex]) {
Arrays.fill(row, Double.NaN);
}
}
CurvatureWorkspace workspace = new CurvatureWorkspace(layers);
while (true) {
CurvatureRouteSelection selection = selectCurvatureAwareTerrainRoute(
layers,
targetStepLength,
maximumTurn,
turnCost,
localPenalties,
transitionCosts,
turnCosts,
workspace
);
if (selection.route().length == 0) {
return selection.route();
}
boolean evaluated = evaluateSelectedSurfaceCosts(
selection,
layers,
localPenalties,
transitionCosts,
targetStepLength
);
if (!evaluated) {
return selection.route();
}
}
}
CurvatureRouteSelection selectCurvatureAwareTerrainRoute(
List<List<RouteCandidate>> layers,
double targetStepLength,
double maximumTurn,
double turnCost,
double[][] localPenalties,
double[][][] transitionCosts,
double[][][][] turnCosts,
CurvatureWorkspace workspace
) {
int layerCount = layers.size();
double[][] previousCosts = workspace.firstCosts;
double[][] currentCosts = workspace.secondCosts;
int[][][] predecessors = workspace.predecessors;
List<RouteCandidate> firstLayer = layers.getFirst();
List<RouteCandidate> secondLayer = layers.get(1);
for (int firstIndex = 0; firstIndex < firstLayer.size(); firstIndex++) {
Arrays.fill(previousCosts[firstIndex], Double.POSITIVE_INFINITY);
Arrays.fill(predecessors[1][firstIndex], -1);
for (int secondIndex = 0; secondIndex < secondLayer.size(); secondIndex++) {
RouteCandidate first = firstLayer.get(firstIndex);
RouteCandidate second = secondLayer.get(secondIndex);
double transition = resolvedTransitionCost(
first,
second,
targetStepLength,
transitionCosts[1][firstIndex][secondIndex],
workspace,
1,
firstIndex,
secondIndex
);
if (Double.isFinite(transition)) {
previousCosts[firstIndex][secondIndex] = first.localScore()
+ second.localScore()
+ resolvedLocalPenalty(localPenalties[1][secondIndex])
+ transition;
}
}
}
for (int layerIndex = 2; layerIndex < layerCount; layerIndex++) {
List<RouteCandidate> beforeLayer = layers.get(layerIndex - 2);
List<RouteCandidate> previousLayer = layers.get(layerIndex - 1);
List<RouteCandidate> currentLayer = layers.get(layerIndex);
if (turnCosts[layerIndex] == null) {
turnCosts[layerIndex] = new double[previousLayer.size()][currentLayer.size()][];
}
for (int previousIndex = 0; previousIndex < previousLayer.size(); previousIndex++) {
Arrays.fill(currentCosts[previousIndex], Double.POSITIVE_INFINITY);
Arrays.fill(predecessors[layerIndex][previousIndex], -1);
RouteCandidate previous = previousLayer.get(previousIndex);
for (int currentIndex = 0; currentIndex < currentLayer.size(); currentIndex++) {
RouteCandidate current = currentLayer.get(currentIndex);
double transition = resolvedTransitionCost(
previous,
current,
targetStepLength,
transitionCosts[layerIndex][previousIndex][currentIndex],
workspace,
layerIndex,
previousIndex,
currentIndex
);
if (!Double.isFinite(transition)) {
continue;
}
double localPenalty = resolvedLocalPenalty(localPenalties[layerIndex][currentIndex]);
for (int beforeIndex = 0; beforeIndex < beforeLayer.size(); beforeIndex++) {
double previousCost = previousCosts[beforeIndex][previousIndex];
if (!Double.isFinite(previousCost)) {
continue;
}
double[] candidateTurns = turnCosts[layerIndex][previousIndex][currentIndex];
if (candidateTurns == null) {
candidateTurns = new double[beforeLayer.size()];
Arrays.fill(candidateTurns, Double.NaN);
turnCosts[layerIndex][previousIndex][currentIndex] = candidateTurns;
}
double turnPenalty = candidateTurns[beforeIndex];
if (Double.isNaN(turnPenalty)) {
double turn = continuousTurnDegrees(
beforeLayer.get(beforeIndex),
previous,
current
);
turnPenalty = turn > maximumTurn
? Double.POSITIVE_INFINITY
: curvatureCost(turn, maximumTurn, turnCost);
candidateTurns[beforeIndex] = turnPenalty;
}
if (turnPenalty == Double.POSITIVE_INFINITY) {
continue;
}
double cost = previousCost
+ current.localScore()
+ localPenalty
+ transition
+ turnPenalty;
if (cost < currentCosts[previousIndex][currentIndex]) {
currentCosts[previousIndex][currentIndex] = cost;
predecessors[layerIndex][previousIndex][currentIndex] = beforeIndex;
}
}
}
}
double[][] completedCosts = currentCosts;
currentCosts = previousCosts;
previousCosts = completedCosts;
}
int lastLayerIndex = layerCount - 1;
int selectedPreviousIndex = -1;
int selectedCurrentIndex = -1;
double selectedCost = Double.POSITIVE_INFINITY;
for (int previousIndex = 0; previousIndex < layers.get(lastLayerIndex - 1).size(); previousIndex++) {
for (int currentIndex = 0;
currentIndex < layers.get(lastLayerIndex).size();
currentIndex++) {
if (previousCosts[previousIndex][currentIndex] < selectedCost) {
selectedCost = previousCosts[previousIndex][currentIndex];
selectedPreviousIndex = previousIndex;
selectedCurrentIndex = currentIndex;
}
}
}
if (!Double.isFinite(selectedCost)) {
return CurvatureRouteSelection.empty();
}
RouteCandidate[] route = new RouteCandidate[layerCount];
int[] indices = new int[layerCount];
route[lastLayerIndex] = layers.get(lastLayerIndex).get(selectedCurrentIndex);
route[lastLayerIndex - 1] = layers.get(lastLayerIndex - 1).get(selectedPreviousIndex);
indices[lastLayerIndex] = selectedCurrentIndex;
indices[lastLayerIndex - 1] = selectedPreviousIndex;
for (int layerIndex = lastLayerIndex; layerIndex >= 2; layerIndex--) {
int beforeIndex = predecessors[layerIndex][selectedPreviousIndex][selectedCurrentIndex];
if (beforeIndex < 0) {
return CurvatureRouteSelection.empty();
}
route[layerIndex - 2] = layers.get(layerIndex - 2).get(beforeIndex);
indices[layerIndex - 2] = beforeIndex;
selectedCurrentIndex = selectedPreviousIndex;
selectedPreviousIndex = beforeIndex;
}
return new CurvatureRouteSelection(route, indices);
}
boolean evaluateSelectedSurfaceCosts(
CurvatureRouteSelection selection,
List<List<RouteCandidate>> layers,
double[][] localPenalties,
double[][][] transitionCosts,
double targetStepLength
) {
boolean evaluated = false;
RouteCandidate[] route = selection.route();
int[] indices = selection.indices();
for (int layerIndex = 1; layerIndex < route.length - 1; layerIndex++) {
RouteCandidate candidate = route[layerIndex];
if (candidate.terrainResolved()) {
continue;
}
RouteCandidate resolved = resolveRouteCandidate(candidate);
ArrayList<RouteCandidate> updatedLayer = new ArrayList<>(layers.get(layerIndex));
updatedLayer.set(indices[layerIndex], resolved);
layers.set(layerIndex, List.copyOf(updatedLayer));
route[layerIndex] = resolved;
evaluated = true;
}
for (int layerIndex = 1; layerIndex < route.length - 1; layerIndex++) {
int candidateIndex = indices[layerIndex];
if (!Double.isNaN(localPenalties[layerIndex][candidateIndex])) {
continue;
}
if (!Double.isFinite(route[layerIndex].localScore())) {
localPenalties[layerIndex][candidateIndex] = 0D;
continue;
}
localPenalties[layerIndex][candidateIndex] = planner.surfaceCourses.surfaceRouteCandidateBankPenalty(
route[layerIndex]
);
evaluated = true;
}
for (int layerIndex = 1; layerIndex < route.length; layerIndex++) {
int previousIndex = indices[layerIndex - 1];
int currentIndex = indices[layerIndex];
if (!Double.isNaN(transitionCosts[layerIndex][previousIndex][currentIndex])) {
continue;
}
RouteCandidate previous = route[layerIndex - 1];
RouteCandidate current = route[layerIndex];
if (!Double.isFinite(previous.localScore()) || !Double.isFinite(current.localScore())) {
transitionCosts[layerIndex][previousIndex][currentIndex] = Double.POSITIVE_INFINITY;
continue;
}
double transition = planner.routePaths.routeTransitionCost(previous, current, targetStepLength);
if (Double.isFinite(transition)) {
transition += surfaceRouteTransitionPenalty(previous, current);
}
transitionCosts[layerIndex][previousIndex][currentIndex] = transition;
evaluated = true;
}
return evaluated;
}
double resolvedLocalPenalty(double penalty) {
return Double.isNaN(penalty) ? 0D : penalty;
}
double resolvedTransitionCost(
RouteCandidate previous,
RouteCandidate current,
double targetStepLength,
double exactCost,
CurvatureWorkspace workspace,
int layerIndex,
int previousIndex,
int currentIndex
) {
if (!Double.isNaN(exactCost)) {
return exactCost;
}
long heights = ((long) previous.point().y() << 32) | (current.point().y() & 0xffffffffL);
if (!workspace.geometryKnown[layerIndex][previousIndex][currentIndex]
|| workspace.geometryHeights[layerIndex][previousIndex][currentIndex] != heights) {
workspace.geometryCosts[layerIndex][previousIndex][currentIndex] =
planner.routePaths.routeTransitionGeometryCost(previous, current, targetStepLength);
workspace.geometryHeights[layerIndex][previousIndex][currentIndex] = heights;
workspace.geometryKnown[layerIndex][previousIndex][currentIndex] = true;
}
return workspace.geometryCosts[layerIndex][previousIndex][currentIndex];
}
double curvatureCost(double turn, double maximumTurn, double turnCost) {
double excess = Math.max(0D, turn - 8D);
return excess * excess * turnCost / maximumTurn;
}
double continuousTurnDegrees(RouteCandidate before, RouteCandidate center, RouteCandidate after) {
double incomingX = center.continuousX() - before.continuousX();
double incomingZ = center.continuousZ() - before.continuousZ();
double outgoingX = after.continuousX() - center.continuousX();
double outgoingZ = after.continuousZ() - center.continuousZ();
double length = StrictMath.hypot(incomingX, incomingZ) * StrictMath.hypot(outgoingX, outgoingZ);
if (length <= 0D) {
return 0D;
}
double cosine = (incomingX * outgoingX + incomingZ * outgoingZ) / length;
return StrictMath.toDegrees(StrictMath.acos(Math.max(-1D, Math.min(1D, cosine))));
}
double surfaceRouteTransitionPenalty(
RouteCandidate start,
RouteCandidate end
) {
int maximumBankDistance = planner.surfaceCourses.surfaceBankDistance(planner.settings.surface().maximumWidth());
RouteDirection tangent = planner.routePaths.direction(start.point().x(), start.point().z(), end.point().x(), end.point().z());
int endpointCeiling = Math.max(start.point().y(), end.point().y());
double penalty = 0D;
List<HydrologyPoint> raster = planner.segments.rasterLine(start.point(), end.point());
int sampleStride = Math.max(1, planner.settings.routing().refinementSpacing() / 4);
for (int pointIndex = 0; pointIndex < raster.size(); pointIndex += sampleStride) {
HydrologyPoint point = raster.get(pointIndex);
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
if (terrain == null) {
return 1.0E12D;
}
int permittedIncision = planner.sourcePlanner.permittedSurfaceIncision(terrain);
int ridgeExcess = Math.max(
0,
terrain.naturalHeight() - endpointCeiling - permittedIncision
);
penalty += (double) ridgeExcess * ridgeExcess * 65536D;
if (terrain.slope() >= planner.settings.hydraulics().waterfallMinimumDrop()) {
continue;
}
int head = Math.subtractExact(
terrain.naturalHeight(),
planner.settings.surface().banks().sink()
);
penalty += planner.surfaceCourses.surfaceRouteBankBandPenalty(
point,
tangent,
maximumBankDistance,
head,
planner.surfaceCourses.maximumSurfaceBankRise(terrain)
);
}
if ((raster.size() - 1) % sampleStride != 0) {
HydrologyPoint point = raster.getLast();
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
if (terrain == null) {
return 1.0E12D;
}
int permittedIncision = planner.sourcePlanner.permittedSurfaceIncision(terrain);
int ridgeExcess = Math.max(
0,
terrain.naturalHeight() - endpointCeiling - permittedIncision
);
penalty += (double) ridgeExcess * ridgeExcess * 65536D;
if (terrain.slope() < planner.settings.hydraulics().waterfallMinimumDrop()) {
int head = Math.subtractExact(
terrain.naturalHeight(),
planner.settings.surface().banks().sink()
);
penalty += planner.surfaceCourses.surfaceRouteBankBandPenalty(
point,
tangent,
maximumBankDistance,
head,
planner.surfaceCourses.maximumSurfaceBankRise(terrain)
);
}
}
return penalty;
}
static final class CurvatureWorkspace {
private final double[][] firstCosts;
private final double[][] secondCosts;
private final int[][][] predecessors;
private final double[][][] geometryCosts;
private final long[][][] geometryHeights;
private final boolean[][][] geometryKnown;
CurvatureWorkspace(List<? extends List<?>> layers) {
int maximumCandidates = 0;
for (List<?> layer : layers) {
maximumCandidates = Math.max(maximumCandidates, layer.size());
}
firstCosts = new double[maximumCandidates][maximumCandidates];
secondCosts = new double[maximumCandidates][maximumCandidates];
predecessors = new int[layers.size()][][];
geometryCosts = new double[layers.size()][][];
geometryHeights = new long[layers.size()][][];
geometryKnown = new boolean[layers.size()][][];
for (int layer = 1; layer < layers.size(); layer++) {
int previousSize = layers.get(layer - 1).size();
int currentSize = layers.get(layer).size();
predecessors[layer] = new int[previousSize][currentSize];
geometryCosts[layer] = new double[previousSize][currentSize];
geometryHeights[layer] = new long[previousSize][currentSize];
geometryKnown[layer] = new boolean[previousSize][currentSize];
}
}
}
record CurvatureRouteSelection(RouteCandidate[] route, int[] indices) {
private static CurvatureRouteSelection empty() {
return new CurvatureRouteSelection(new RouteCandidate[0], new int[0]);
}
}
record TerrainTransitionKey(HydrologyPoint start, HydrologyPoint end) {
}
}
@@ -23,19 +23,6 @@ final class HydrologyRoutePath {
return false;
}
double bilinear(
double northwest,
double northeast,
double southwest,
double southeast,
double progressX,
double progressZ
) {
double north = northwest + (northeast - northwest) * progressX;
double south = southwest + (southeast - southwest) * progressX;
return north + (south - north) * progressZ;
}
int removeTerrainPits(List<List<RouteCandidate>> layers, RouteCandidate[] route) {
int removed = 0;
for (int routeIndex = 1; routeIndex < route.length - 1; routeIndex++) {
@@ -8,6 +8,7 @@ record HydrologyRoutingPlan(
int[] outletIndex,
int[] routeLengths,
List<OutletCandidate> outlets,
boolean organicSurfaceRequired
boolean organicSurfaceRequired,
HydrologyDrainageGeometry surfaceDrainage
) {
}
@@ -2,16 +2,77 @@ package art.arcane.iris.generation.hydrology;
import java.util.List;
record HydrologySampledGrid(
int minimumX,
int minimumZ,
int ownerMinimumX,
int ownerMinimumZ,
int ownerSize,
int width,
int spacing,
List<HydrologyGridNode> nodes
) {
final class HydrologySampledGrid {
private final int minimumX;
private final int minimumZ;
private final int ownerMinimumX;
private final int ownerMinimumZ;
private final int ownerSize;
private final int width;
private final int spacing;
private final List<HydrologyGridNode> nodes;
private volatile HydrologyDrainageGraph drainage;
HydrologySampledGrid(int minimumX, int minimumZ, int ownerMinimumX, int ownerMinimumZ,
int ownerSize, int width, int spacing, List<HydrologyGridNode> nodes) {
this.minimumX = minimumX;
this.minimumZ = minimumZ;
this.ownerMinimumX = ownerMinimumX;
this.ownerMinimumZ = ownerMinimumZ;
this.ownerSize = ownerSize;
this.width = width;
this.spacing = spacing;
this.nodes = List.copyOf(nodes);
}
int minimumX() {
return minimumX;
}
int minimumZ() {
return minimumZ;
}
int ownerMinimumX() {
return ownerMinimumX;
}
int ownerMinimumZ() {
return ownerMinimumZ;
}
int ownerSize() {
return ownerSize;
}
int width() {
return width;
}
int spacing() {
return spacing;
}
List<HydrologyGridNode> nodes() {
return nodes;
}
HydrologyDrainageGraph drainage(HydrologySourcePlanner sourcePlanner) {
HydrologyDrainageGraph current = drainage;
if (current != null && current.settings().equals(sourcePlanner.settings())) {
return current;
}
synchronized (this) {
current = drainage;
if (current == null || !current.settings().equals(sourcePlanner.settings())) {
current = new HydrologyDrainageGraph(this, sourcePlanner);
drainage = current;
}
return current;
}
}
HydrologyGridNode node(int index) {
return nodes.get(index);
}
@@ -21,6 +21,10 @@ final class HydrologySourcePlanner {
this.planner = planner;
}
HydrologyPlannerSettings settings() {
return planner.settings;
}
static final long EDGE_SALT = 0x45444745L;
static final long SURFACE_SOURCE_SALT = 0x53555246414345L;
static final long UNDERGROUND_SOURCE_SALT = 0x554e444552L;
@@ -70,10 +74,7 @@ final class HydrologySourcePlanner {
double[] potential = new double[nodeCount];
int[] parent = new int[nodeCount];
int[] outletIndex = new int[nodeCount];
int[] minimumNeighborHeights = new int[nodeCount];
for (HydrologyGridNode node : grid.nodes()) {
minimumNeighborHeights[node.index()] = minimumNeighborHeight(grid, node);
}
HydrologyDrainageGraph drainage = grid.drainage(this);
Arrays.fill(potential, Double.POSITIVE_INFINITY);
Arrays.fill(parent, -1);
Arrays.fill(outletIndex, -1);
@@ -91,25 +92,13 @@ final class HydrologySourcePlanner {
continue;
}
HydrologyGridNode downstream = grid.node(currentEntry.nodeIndex());
for (HydrologyGridOffset offset : ROUTING_OFFSETS) {
HydrologyGridNode upstream = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
if (upstream == null || upstream.terrain().ocean() || !upstream.terrain().transitAllowed()
|| !upstream.terrain().drainsInto(downstream.terrain())
|| surface && !HydrologySurfaceProfiles.sharesProfile(upstream.terrain(), downstream.terrain())) {
// Confined ground only drains into its own area, so every course that starts or
// arrives there keeps to that area up to and including its outlet.
for (int direction = 0; direction < ROUTING_OFFSETS.size(); direction++) {
int upstreamIndex = drainage.upstream(downstream.index(), direction, surface);
if (upstreamIndex < 0) {
continue;
}
if (surface && downstream.terrain().naturalHeight() - upstream.terrain().naturalHeight()
> maximumSurfaceEdgeRise(downstream.terrain())) {
continue;
}
double edgeCost = routeCost(
upstream,
downstream,
offset,
minimumNeighborHeights[downstream.index()]
);
HydrologyGridNode upstream = grid.node(upstreamIndex);
double edgeCost = drainage.cost(downstream.index(), direction);
double candidatePotential = currentEntry.potential() + edgeCost;
int existingParent = parent[upstream.index()];
boolean replace = candidatePotential < potential[upstream.index()] - 1.0E-9D;
@@ -135,7 +124,9 @@ final class HydrologySourcePlanner {
outletIndex,
routeLengths(parent, grid.width(), planner.settings.routing()),
List.copyOf(outlets),
false
false,
surface ? new HydrologyDrainageGeometry(planner,
new HydrologyDrainageGeometry.Basin(grid, parent, outletIndex, List.copyOf(outlets))) : null
);
}
@@ -160,7 +151,8 @@ final class HydrologySourcePlanner {
routing.outletIndex(),
routing.routeLengths(),
routing.outlets(),
true
true,
routing.surfaceDrainage()
);
}
@@ -985,75 +977,75 @@ final class HydrologySourcePlanner {
HydrologyGridNode downstream = grid.node(downstreamIndex);
RiverOutlet outlet = routing.outlets().get(routing.outletIndex()[upstream.index()]).outlet();
long edgeId = HydrologyHash.mix(planner.worldSeed, EDGE_SALT, upstream.id(), downstream.id(), outlet.id());
long refinementId = surface ? HydrologyHash.mix(edgeId, SURFACE_SOURCE_SALT) : edgeId;
List<HydrologyPoint> centerline = refinedEdges.get(refinementId);
int transverseCandidates = HydrologyRouteGeometry.ROUTE_TRANSVERSE_CANDIDATES;
if (centerline == null) {
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, surface);
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, surface);
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet, surface);
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
upstream.id(),
downstream.id(),
upstreamAnchor.x(),
upstreamAnchor.z(),
downstreamAnchor.x(),
downstreamAnchor.z(),
continuation.x(),
continuation.z(),
transverseCandidates,
surface
);
centerline = planner.refinedEdgeCache.get(
refinedEdgeKey,
ignored -> planner.routeGeometry.refineEdge(
upstream.id(),
downstream.id(),
upstreamAnchor,
downstreamAnchor,
continuation,
transverseCandidates,
surface
)
);
refinedEdges.put(refinementId, centerline);
}
if (centerline.isEmpty()) {
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, surface);
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, surface);
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet, surface);
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
upstream.id(),
downstream.id(),
upstreamAnchor.x(),
upstreamAnchor.z(),
downstreamAnchor.x(),
downstreamAnchor.z(),
continuation.x(),
continuation.z(),
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES,
surface
);
centerline = planner.refinedEdgeCache.get(
refinedEdgeKey,
ignored -> planner.routeGeometry.refineEdge(
upstream.id(),
downstream.id(),
upstreamAnchor,
downstreamAnchor,
continuation,
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES,
surface
)
);
refinedEdges.put(refinementId, centerline);
}
if (centerline.isEmpty()) {
centerline = planner.routeGeometry.constrainedTerrainFallbackEdge(
planner.routeGeometry.routeAnchor(upstream, surface),
planner.routeGeometry.routeAnchor(downstream, surface)
);
refinedEdges.put(refinementId, centerline);
List<HydrologyPoint> centerline;
if (surface) {
centerline = routing.surfaceDrainage().edge(upstream.index());
} else {
centerline = refinedEdges.get(edgeId);
int transverseCandidates = HydrologyRouteGeometry.ROUTE_TRANSVERSE_CANDIDATES;
if (centerline == null) {
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, false);
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, false);
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet);
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
upstream.id(),
downstream.id(),
upstreamAnchor.x(),
upstreamAnchor.z(),
downstreamAnchor.x(),
downstreamAnchor.z(),
continuation.x(),
continuation.z(),
transverseCandidates
);
centerline = planner.refinedEdgeCache.get(
refinedEdgeKey,
ignored -> planner.routeGeometry.refineUndergroundEdge(
upstream.id(),
downstream.id(),
upstreamAnchor,
downstreamAnchor,
continuation,
transverseCandidates
)
);
refinedEdges.put(edgeId, centerline);
}
if (centerline.isEmpty()) {
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, false);
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, false);
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet);
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
upstream.id(),
downstream.id(),
upstreamAnchor.x(),
upstreamAnchor.z(),
downstreamAnchor.x(),
downstreamAnchor.z(),
continuation.x(),
continuation.z(),
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES
);
centerline = planner.refinedEdgeCache.get(
refinedEdgeKey,
ignored -> planner.routeGeometry.refineUndergroundEdge(
upstream.id(),
downstream.id(),
upstreamAnchor,
downstreamAnchor,
continuation,
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES
)
);
refinedEdges.put(edgeId, centerline);
}
if (centerline.isEmpty()) {
centerline = planner.routeGeometry.constrainedTerrainFallbackEdge(
planner.routeGeometry.routeAnchor(upstream, false),
planner.routeGeometry.routeAnchor(downstream, false)
);
refinedEdges.put(edgeId, centerline);
}
}
if (centerline.isEmpty()) {
continue;
@@ -1081,7 +1073,8 @@ final class HydrologySourcePlanner {
}
int outletIndex = routing.outletIndex()[node.index()];
RiverOutlet outlet = routing.outlets().get(outletIndex).outlet();
HydrologyPoint anchor = planner.routeGeometry.routeAnchor(node, surface);
HydrologyPoint anchor = surface ? routing.surfaceDrainage().anchor(node.index())
: planner.routeGeometry.routeAnchor(node, false);
HydrologyTerrainSample terrain = Objects.requireNonNull(
planner.sampleLandBasis(anchor.x(), anchor.z()),
"Hydrology route anchor left natural land"
@@ -9,8 +9,8 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.OptionalLong;
import java.util.Objects;
final class HydrologySurfaceCoursePlanner {
private final HydrologyPlanner planner;
@@ -28,7 +28,6 @@ final class HydrologySurfaceCoursePlanner {
static final double SURFACE_GRID_LOCK_TOLERANCE_DEGREES = 5D;
static final double SURFACE_MAXIMUM_GRID_LOCKED_FRACTION = 0.5D;
static final double SURFACE_MAXIMUM_GRID_LOCKED_RUN = 64D;
static final double SURFACE_MAXIMUM_WIDTH_SCALE = 1.18D;
void compileSurfaceCourses(
HydrologyTileKey key,
@@ -407,224 +406,8 @@ final class HydrologySurfaceCoursePlanner {
if (guide.size() < 2 || guideEdges.size() != guide.size() - 1) {
return null;
}
ArrayList<HydrologyPoint> routedGuide = new ArrayList<>(guide);
ArrayList<DrainageEdge> routedGuideEdges = new ArrayList<>(guideEdges);
HydrologyPoint landward = outlet.landwardPoint();
HydrologyPoint guideEnd = routedGuide.getLast();
boolean terminateAtSinkhole = planner.segments.surfaceSinkhole(outlet)
&& (guideEnd.x() != landward.x() || guideEnd.z() != landward.z());
if (terminateAtSinkhole) {
routedGuide.set(routedGuide.size() - 1, landward);
}
List<HydrologyPoint> surfaceGuide = planner.routePaths.withoutDuplicateRoutePoints(routedGuide);
long geometryId = HydrologyHash.mix(
planner.worldSeed,
COURSE_SALT,
HydrologySourcePlanner.SURFACE_SOURCE_SALT,
edges.getFirst().upstreamNodeId(),
outlet.id()
);
Objects.requireNonNull(grid, "Surface course routing grid is required");
HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get();
DraftProfile profile = planner.currentDraftProfile();
profile.routeCalls++;
List<HydrologyPoint> points;
if (samples == null) {
profile.routeSolves++;
long solveStarted = System.nanoTime();
points = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
profile.routeSolveNanos += System.nanoTime() - solveStarted;
} else {
HashMap<SurfaceRouteKey, List<HydrologyPoint>> routes = samples.surfaceRoutes.computeIfAbsent(
grid, ignored -> new HashMap<>()
);
SurfaceRouteKey routeKey = new SurfaceRouteKey(surfaceGuide, geometryId);
points = routes.computeIfAbsent(routeKey, ignored -> {
profile.routeSolves++;
long solveStarted = System.nanoTime();
List<HydrologyPoint> solved = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
profile.routeSolveNanos += System.nanoTime() - solveStarted;
return solved;
});
}
if (points.size() < 2) {
return null;
}
List<DrainageEdge> pairEdges = mapCoursePairs(points, routedGuide, routedGuideEdges);
if (pairEdges.size() != points.size() - 1) {
return null;
}
return new HydrologyCoursePath(
points,
pairEdges,
List.copyOf(edges),
outlet,
true,
routing.organicSurfaceRequired(),
null
);
}
List<HydrologyPoint> surfaceCourseCenterline(
HydrologySampledGrid grid,
List<HydrologyPoint> guide,
long geometryId
) {
if (guide.size() < 2) {
return List.of();
}
int refinement = planner.settings.routing().refinementSpacing();
List<SurfaceCurvePoint> base = smoothSurfaceGuide(resampleSurfaceGuide(guide, refinement));
List<SurfaceCurvePoint> nominal = meanderSurfaceGuide(base, geometryId, refinement);
if (nominal.size() < 2) {
return List.of();
}
ArrayList<List<RouteCandidate>> layers = new ArrayList<>(nominal.size());
int transverseCandidates = HydrologyRouteGeometry.SURFACE_COURSE_TRANSVERSE_CANDIDATES;
for (int pointIndex = 0; pointIndex < nominal.size(); pointIndex++) {
SurfaceCurvePoint point = nominal.get(pointIndex);
RouteDirection tangent = surfaceCurveTangent(nominal, pointIndex);
if (pointIndex == 0 || pointIndex == nominal.size() - 1) {
HydrologyPoint endpoint = pointIndex == 0 ? guide.getFirst() : guide.getLast();
layers.add(List.of(planner.routeGeometry.routeEndpoint(endpoint, tangent)));
continue;
}
double progress = pointIndex / (double) (nominal.size() - 1);
SurfaceCurvePoint basePoint = base.get(pointIndex);
RoutePosition position = new RoutePosition(
point.x(),
point.z(),
basePoint.x(),
basePoint.z(),
tangent
);
long candidateId = HydrologyHash.mix(geometryId, pointIndex);
List<RouteCandidate> candidates = planner.routeGeometry.unresolvedSurfaceRouteCandidates(
grid,
geometryId,
candidateId,
position,
progress,
refinement,
transverseCandidates
);
if (candidates.isEmpty()) {
return List.of();
}
layers.add(candidates);
}
ArrayList<List<RouteCandidate>> availableLayers = new ArrayList<>(layers);
double maximumTurn = Math.min(
HydrologyRouteGeometry.SURFACE_COURSE_MAXIMUM_TURN_DEGREES,
planner.settings.geometry().meanders().maximumTurnDegrees()
);
while (true) {
RouteCandidate[] route = planner.routeGeometry.selectCurvatureAwareTerrainRoute(
availableLayers,
refinement,
maximumTurn,
HydrologyRouteGeometry.SURFACE_COURSE_TURN_COST
);
if (route.length == 0) {
if (addExhaustedSurfaceRouteFallbacks(
grid,
availableLayers,
nominal,
base,
geometryId
)) {
continue;
}
return List.of();
}
int pitRemoval = planner.routePaths.removeTerrainPits(availableLayers, route);
if (pitRemoval < 0) {
return List.of();
}
if (pitRemoval > 0) {
continue;
}
ArrayList<HydrologyPoint> points = new ArrayList<>(route.length);
for (RouteCandidate candidate : route) {
HydrologyPoint point = candidate.point();
if (points.isEmpty()
|| point.x() != points.getLast().x()
|| point.z() != points.getLast().z()) {
points.add(point);
}
}
if (points.size() < 2 || !planner.routePaths.traversableRoute(points)) {
return List.of();
}
return planner.routePaths.smoothTerrainRoute(points);
}
}
boolean addExhaustedSurfaceRouteFallbacks(
HydrologySampledGrid grid,
List<List<RouteCandidate>> layers,
List<SurfaceCurvePoint> nominal,
List<SurfaceCurvePoint> base,
long geometryId
) {
boolean added = false;
for (int layerIndex = 1; layerIndex < layers.size() - 1; layerIndex++) {
List<RouteCandidate> layer = layers.get(layerIndex);
if (!exhaustedSurfaceRouteLayer(layer)) {
continue;
}
SurfaceCurvePoint nominalPoint = nominal.get(layerIndex);
SurfaceCurvePoint basePoint = base.get(layerIndex);
RouteDirection tangent = surfaceCurveTangent(nominal, layerIndex);
int fallbackX = (int) StrictMath.round(basePoint.x());
int fallbackZ = (int) StrictMath.round(basePoint.z());
long packed = RiverFootprint.pack(fallbackX, fallbackZ);
boolean present = false;
for (RouteCandidate candidate : layer) {
if (RiverFootprint.pack(candidate.point().x(), candidate.point().z()) == packed) {
present = true;
break;
}
}
if (present) {
continue;
}
RouteTerrainEstimate estimate = planner.routeGeometry.routeTerrainEstimate(grid, fallbackX, fallbackZ);
double offset = (basePoint.x() - nominalPoint.x()) * -tangent.z()
+ (basePoint.z() - nominalPoint.z()) * tangent.x();
double localScore = estimate.terrainScore()
+ StrictMath.abs(offset) * 0.18D
+ HydrologyHash.unit(HydrologyHash.mix(
planner.worldSeed,
geometryId,
HydrologyHash.mix(geometryId, layerIndex),
fallbackX,
fallbackZ
)) * 1.0E-6D;
ArrayList<RouteCandidate> expanded = new ArrayList<>(layer);
expanded.add(new RouteCandidate(
new HydrologyPoint(fallbackX, estimate.height(), fallbackZ),
basePoint.x(),
basePoint.z(),
offset,
localScore,
estimate.terrainScore(),
tangent,
false
));
layers.set(layerIndex, List.copyOf(expanded));
added = true;
}
return added;
}
boolean exhaustedSurfaceRouteLayer(List<RouteCandidate> layer) {
for (RouteCandidate candidate : layer) {
if (!candidate.terrainResolved() || Double.isFinite(candidate.localScore())) {
return false;
}
}
return true;
return new HydrologyCoursePath(List.copyOf(guide), List.copyOf(guideEdges), List.copyOf(edges),
outlet, true, routing.organicSurfaceRequired(), null);
}
double routeTurnScore(List<HydrologyPoint> route) {
@@ -652,289 +435,6 @@ final class HydrologySurfaceCoursePlanner {
return percentile + maximum * 0.05D;
}
double surfaceRouteCandidateBankPenalty(RouteCandidate candidate) {
int maximumBankDistance = surfaceBankDistance(planner.settings.surface().maximumWidth());
HydrologyPoint point = candidate.point();
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
if (terrain == null) {
return Double.POSITIVE_INFINITY;
}
if (terrain.slope() >= planner.settings.hydraulics().waterfallMinimumDrop()) {
return 0D;
}
int head = Math.subtractExact(
terrain.naturalHeight(),
planner.settings.surface().banks().sink()
);
return surfaceRouteBankBandPenalty(
point,
candidate.tangent(),
maximumBankDistance,
head,
maximumSurfaceBankRise(terrain)
);
}
int maximumSurfaceBankRise(HydrologyTerrainSample terrain) {
return Math.addExact(
terrain.surfacePolicy().maximumIncision(planner.settings.surface().maximumIncision()),
(int) StrictMath.ceil(planner.settings.surface().banks().minimumBlendWidth() * 0.5D)
);
}
double surfaceRouteBankBandPenalty(
HydrologyPoint point,
RouteDirection tangent,
int maximumDistance,
int head,
int maximumBankRise
) {
int innerDistance = surfaceBankInnerDistance();
double penalty = 0D;
for (int distance = innerDistance; distance <= maximumDistance; distance++) {
penalty += surfaceRouteBankPenalty(point, tangent, distance, head, maximumBankRise);
penalty += surfaceRouteBankPenalty(point, tangent, -distance, head, maximumBankRise);
}
return penalty;
}
double surfaceRouteBankPenalty(
HydrologyPoint point,
RouteDirection tangent,
int signedDistance,
int head,
int maximumBankRise
) {
int bankX = (int) StrictMath.round(point.x() - tangent.z() * signedDistance);
int bankZ = (int) StrictMath.round(point.z() + tangent.x() * signedDistance);
double bankHeight = planner.sampleLandHeight(bankX, bankZ);
if (Double.isNaN(bankHeight)) {
return 1.0E12D;
}
int rise = (int) bankHeight - head;
int shortfall = HydrologyRouteGeometry.SURFACE_MINIMUM_NATURAL_BANK_RISE - rise;
if (shortfall > 0) {
return (double) shortfall * shortfall * 4096D;
}
int excess = Math.max(0, rise - maximumBankRise);
return (double) excess * excess * 128D;
}
/**
* Innermost probed ring, in blocks out from the centerline. It reads {@code surface.maximumWidth()} and
* nothing else - no {@code shoreWidth}, no candidate, no terrain - so raising the shore knob widens the
* probe band instead of sliding it. That is the whole guarantee.
*
* <p>The ring sits one block outside half the nominal maximum width, which is the widest channel the
* width sampler will draw, not the widest channel that gets cut: {@code ChannelProfileBuilder} clamps a
* station to {@code maximumWidth * 2} after the terrain width multiplier, then scales it again by
* {@code banks().springWidthRatio()} over the spring reach and {@code banks().mouthFlareRatio()} through
* the inlet flare. Routing runs before any profile exists, so no ring can promise to clear the channel.
*/
int surfaceBankInnerDistance() {
return (int) StrictMath.ceil(planner.settings.surface().maximumWidth() / 2D) + 1;
}
/**
* Outermost probed ring: where the bank of the widest channel is expected once the shore is cut. It
* gains a ring per block of {@code shoreWidth}, which is what widens the band walked by the penalty.
*/
int surfaceBankDistance(int width) {
int channelRadius = (int) StrictMath.ceil(width / 2D);
return Math.max(
channelRadius + 1,
(int) StrictMath.ceil(
channelRadius * SURFACE_MAXIMUM_WIDTH_SCALE + planner.settings.surface().shoreWidth()
) + 1
);
}
List<SurfaceCurvePoint> resampleSurfaceGuide(List<HydrologyPoint> guide, int spacing) {
double[] cumulative = new double[guide.size()];
for (int pointIndex = 1; pointIndex < guide.size(); pointIndex++) {
HydrologyPoint previous = guide.get(pointIndex - 1);
HydrologyPoint point = guide.get(pointIndex);
cumulative[pointIndex] = cumulative[pointIndex - 1]
+ StrictMath.hypot(point.x() - previous.x(), point.z() - previous.z());
}
double totalLength = cumulative[cumulative.length - 1];
if (totalLength <= 0D) {
return List.of();
}
int capacity = Math.addExact(2, (int) StrictMath.ceil(totalLength / spacing));
ArrayList<SurfaceCurvePoint> sampled = new ArrayList<>(capacity);
int segmentIndex = 0;
for (double distance = 0D; distance < totalLength; distance += spacing) {
while (segmentIndex < guide.size() - 2
&& cumulative[segmentIndex + 1] < distance) {
segmentIndex++;
}
HydrologyPoint start = guide.get(segmentIndex);
HydrologyPoint end = guide.get(segmentIndex + 1);
double segmentLength = cumulative[segmentIndex + 1] - cumulative[segmentIndex];
double progress = segmentLength <= 0D
? 0D
: (distance - cumulative[segmentIndex]) / segmentLength;
sampled.add(new SurfaceCurvePoint(
start.x() + (end.x() - start.x()) * progress,
start.z() + (end.z() - start.z()) * progress,
distance
));
}
HydrologyPoint end = guide.getLast();
sampled.add(new SurfaceCurvePoint(end.x(), end.z(), totalLength));
return List.copyOf(sampled);
}
List<SurfaceCurvePoint> smoothSurfaceGuide(List<SurfaceCurvePoint> guide) {
if (guide.size() < 3) {
return guide;
}
List<SurfaceCurvePoint> current = guide;
int passes = Math.max(2, planner.settings.geometry().meanders().smoothingPasses() + 1);
for (int pass = 0; pass < passes; pass++) {
ArrayList<SurfaceCurvePoint> smoothed = new ArrayList<>(current.size());
smoothed.add(current.getFirst());
for (int pointIndex = 1; pointIndex < current.size() - 1; pointIndex++) {
SurfaceCurvePoint previous = current.get(pointIndex - 1);
SurfaceCurvePoint point = current.get(pointIndex);
SurfaceCurvePoint next = current.get(pointIndex + 1);
smoothed.add(new SurfaceCurvePoint(
previous.x() * 0.25D + point.x() * 0.5D + next.x() * 0.25D,
previous.z() * 0.25D + point.z() * 0.5D + next.z() * 0.25D,
point.distance()
));
}
smoothed.add(current.getLast());
current = List.copyOf(smoothed);
}
return current;
}
List<SurfaceCurvePoint> meanderSurfaceGuide(
List<SurfaceCurvePoint> guide,
long geometryId,
int refinement
) {
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
double detailWeight = meanders.detailStrength() * 0.08D;
double totalWeight = meanders.primaryStrength() + detailWeight;
if (guide.size() < 3 || totalWeight <= 0D || meanders.maximumOffsetRatio() <= 0D) {
return guide;
}
double maximumOffset = planner.settings.routing().sampleSpacing() * Math.min(
0.16D,
meanders.maximumOffsetRatio() * 0.32D
);
double primaryPhase = HydrologyHash.unit(HydrologyHash.mix(geometryId, HydrologyRouteGeometry.ROUTE_WORM_PRIMARY_SALT))
* StrictMath.PI * 2D;
double detailPhase = HydrologyHash.unit(HydrologyHash.mix(geometryId, HydrologyRouteGeometry.ROUTE_WORM_DETAIL_SALT))
* StrictMath.PI * 2D;
double totalLength = guide.getLast().distance();
double taperLength = Math.max(
refinement * 8D,
Math.min(planner.settings.routing().sampleSpacing() * 0.75D, meanders.primaryWavelength() * 0.35D)
);
ArrayList<SurfaceCurvePoint> meandered = new ArrayList<>(guide.size());
for (int pointIndex = 0; pointIndex < guide.size(); pointIndex++) {
SurfaceCurvePoint point = guide.get(pointIndex);
RouteDirection tangent = surfaceCurveTangent(guide, pointIndex);
double endpointDistance = Math.min(point.distance(), totalLength - point.distance());
double taperProgress = Math.min(1D, endpointDistance / taperLength);
double taper = StrictMath.pow(StrictMath.sin(taperProgress * StrictMath.PI * 0.5D), 2D);
double primary = StrictMath.sin(
point.distance() * StrictMath.PI * 2D / meanders.primaryWavelength() + primaryPhase
) * meanders.primaryStrength();
double detail = StrictMath.sin(
point.distance() * StrictMath.PI * 2D / meanders.detailWavelength() + detailPhase
) * detailWeight;
double offset = maximumOffset * taper * (primary + detail) / totalWeight;
meandered.add(new SurfaceCurvePoint(
point.x() - tangent.z() * offset,
point.z() + tangent.x() * offset,
point.distance()
));
}
return List.copyOf(meandered);
}
RouteDirection surfaceCurveTangent(List<SurfaceCurvePoint> points, int pointIndex) {
SurfaceCurvePoint start = points.get(Math.max(0, pointIndex - 2));
SurfaceCurvePoint end = points.get(Math.min(points.size() - 1, pointIndex + 2));
double deltaX = end.x() - start.x();
double deltaZ = end.z() - start.z();
double length = StrictMath.hypot(deltaX, deltaZ);
return length <= 0D
? new RouteDirection(1D, 0D)
: new RouteDirection(deltaX / length, deltaZ / length);
}
List<DrainageEdge> mapCoursePairs(
List<HydrologyPoint> points,
List<HydrologyPoint> guide,
List<DrainageEdge> guideEdges
) {
ArrayList<DrainageEdge> mapped = new ArrayList<>(points.size() - 1);
int minimumGuideIndex = 0;
for (int pointIndex = 0; pointIndex < points.size() - 1; pointIndex++) {
HydrologyPoint first = points.get(pointIndex);
HydrologyPoint second = points.get(pointIndex + 1);
double x = (first.x() + second.x()) * 0.5D;
double z = (first.z() + second.z()) * 0.5D;
int selectedIndex = minimumGuideIndex;
double selectedDistance = Double.POSITIVE_INFINITY;
for (int guideIndex = minimumGuideIndex; guideIndex < guideEdges.size(); guideIndex++) {
double distance = pointToSegmentDistanceSquared(
x,
z,
guide.get(guideIndex),
guide.get(guideIndex + 1)
);
if (distance < selectedDistance) {
selectedIndex = guideIndex;
selectedDistance = distance;
}
}
minimumGuideIndex = selectedIndex;
mapped.add(guideEdges.get(selectedIndex));
}
return List.copyOf(mapped);
}
double pointToSegmentDistanceSquared(
double x,
double z,
HydrologyPoint start,
HydrologyPoint end
) {
double deltaX = end.x() - start.x();
double deltaZ = end.z() - start.z();
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
if (lengthSquared == 0D) {
double pointX = x - start.x();
double pointZ = z - start.z();
return pointX * pointX + pointZ * pointZ;
}
double progress = ((x - start.x()) * deltaX + (z - start.z()) * deltaZ) / lengthSquared;
progress = Math.max(0D, Math.min(1D, progress));
double nearestX = start.x() + deltaX * progress;
double nearestZ = start.z() + deltaZ * progress;
double pointX = x - nearestX;
double pointZ = z - nearestZ;
return pointX * pointX + pointZ * pointZ;
}
HydrologyPoint surfaceBankPoint(HydrologyPoint point, RouteDirection tangent, int signedDistance) {
return new HydrologyPoint(
(int) StrictMath.round(point.x() - tangent.z() * signedDistance),
point.y(),
(int) StrictMath.round(point.z() + tangent.x() * signedDistance)
);
}
record SurfaceCurvePoint(double x, double z, double distance) {
}
record SurfaceCourseBuild(
RiverCourse course,
HydrologyCandidateRejection rejection,
@@ -3,8 +3,8 @@ package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -14,16 +14,15 @@ public final class HydrologySurfaceDropRaster {
private static final HydrologySurfaceDropRaster EMPTY = new HydrologySurfaceDropRaster(List.of(), Map.of());
private final List<HydrologyColumnSample> columns;
private final Map<Long, HydrologyColumnSample> indexed;
private final Long2ObjectOpenHashMap<HydrologyColumnSample> indexed;
private final Map<Long, OceanReceiver> oceanReceivers;
private HydrologySurfaceDropRaster(List<HydrologyColumnSample> columns, Map<Long, OceanReceiver> oceanReceivers) {
this.columns = List.copyOf(columns);
Map<Long, HydrologyColumnSample> indexed = new HashMap<>(columns.size());
this.indexed = new Long2ObjectOpenHashMap<>(columns.size());
for (HydrologyColumnSample sample : columns) {
indexed.put(RiverFootprint.pack(sample.x(), sample.z()), sample);
}
this.indexed = Map.copyOf(indexed);
this.oceanReceivers = Map.copyOf(oceanReceivers);
}
@@ -65,9 +65,10 @@ public final class HydrologyTileCache implements AutoCloseable {
private final Object publicationLock = new Object();
private final BooleanSupplier waitingForbidden;
private volatile SharedCacheScope sharedCacheScope;
private volatile StudioHydrologyTileStore persistentStore;
private volatile PreparedHydrologyTileStore persistentStore;
private volatile boolean neighbourPrefetchEnabled;
private volatile Runnable terrainPreparation;
private PregenerationScope pregenerationScope;
private int demandBatches;
public HydrologyTileCache(HydrologyPlanner planner) {
@@ -338,27 +339,31 @@ public final class HydrologyTileCache implements AutoCloseable {
SharedTileKey sharedKey = sharedCacheScope == null ? null : new SharedTileKey(sharedCacheScope, key);
HydrologyTile shared = sharedKey == null ? null : SHARED_TILES.getIfPresent(sharedKey);
if (shared != null && validSharedTile(shared, sharedKey)) {
PreparedHydrologyTileStore store = persistentStore;
if (store != null && store.load(key).isEmpty()) {
persistTile(store, shared);
}
planner.reuseResolvedTile(shared);
IrisLogging.debug("Reused shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
IrisLogging.debug("Reused shared prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
return shared;
}
if (shared != null) {
SHARED_TILES.invalidate(sharedKey);
}
StudioHydrologyTileStore store = persistentStore;
PreparedHydrologyTileStore store = persistentStore;
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
return planAdmitted(key, planningEpoch, sharedKey, store);
}
}
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
SharedTileKey sharedKey, StudioHydrologyTileStore store) {
if (store != null && persistentStudioKey(key)) {
SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
if (store != null) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
SHARED_TILES.put(sharedKey, persisted);
planner.reuseResolvedTile(persisted);
IrisLogging.debug("Loaded persisted Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
IrisLogging.debug("Loaded persisted prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
return persisted;
}
}
@@ -369,18 +374,12 @@ public final class HydrologyTileCache implements AutoCloseable {
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
if (existing == null) {
IrisLogging.debug("Published shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
IrisLogging.debug("Published shared prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
}
}
}
if (store != null && persistentStudioKey(key) && !closed.get() && planningEpoch == cacheEpoch.get()) {
try {
store.save(planned);
IrisLogging.debug("Persisted Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
} catch (IOException failure) {
IrisLogging.reportError("Failed to persist Studio hydrology tile "
+ key.tileX() + "," + key.tileZ() + ".", failure);
}
if (store != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
persistTile(store, planned);
}
return planned;
} catch (RuntimeException failure) {
@@ -394,6 +393,15 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private void persistTile(PreparedHydrologyTileStore store, HydrologyTile tile) {
try {
store.save(tile);
} catch (IOException failure) {
IrisLogging.reportError("Failed to persist prepared hydrology tile "
+ tile.key().tileX() + "," + tile.key().tileZ() + ".", failure);
}
}
private static boolean interrupted(Throwable failure) {
if (Thread.currentThread().isInterrupted()) {
return true;
@@ -510,6 +518,9 @@ public final class HydrologyTileCache implements AutoCloseable {
public void clear() {
ArrayList<CompletableFuture<ChunkColumns>> cancelledQueries = new ArrayList<>();
synchronized (prefetchQueue) {
if (pregenerationScope != null) {
pregenerationScope.close();
}
synchronized (publicationLock) {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
@@ -547,20 +558,29 @@ public final class HydrologyTileCache implements AutoCloseable {
neighbourPrefetchEnabled = enabled;
}
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
public PregenerationScope preparePregeneration(PregenerationArea area) {
Objects.requireNonNull(area, "area");
int tileSize = planner.settings().routing().tileSize();
int minimumBlockX = Math.subtractExact(centerBlockX, tileSize);
int minimumBlockZ = Math.subtractExact(centerBlockZ, tileSize);
int maximumBlockX = Math.addExact(centerBlockX, tileSize);
int maximumBlockZ = Math.addExact(centerBlockZ, tileSize);
enqueuePrefetchArea(prefetchAreaKeys(
minimumBlockX,
minimumBlockZ,
maximumBlockX,
maximumBlockZ,
centerBlockX,
centerBlockZ
), true);
int publicationRadius = planner.settings().publicationRadius();
TileBounds bounds = new TileBounds(
tileCoordinate(Math.subtractExact(area.minimumBlockX(), publicationRadius), tileSize),
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), publicationRadius), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockX(), publicationRadius), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockZ(), publicationRadius), tileSize));
List<HydrologyTileKey> keys = prefetchAreaKeys(
Math.max(area.minimumBlockX(), (long) area.centerBlockX() - tileSize),
Math.max(area.minimumBlockZ(), (long) area.centerBlockZ() - tileSize),
Math.min(area.maximumBlockX(), (long) area.centerBlockX() + tileSize),
Math.min(area.maximumBlockZ(), (long) area.centerBlockZ() + tileSize),
area.centerBlockX(), area.centerBlockZ());
PregenerationScope scope = new PregenerationScope(bounds);
try {
enqueuePrefetchArea(keys, scope);
return scope;
} catch (RuntimeException | Error failure) {
scope.close();
throw failure;
}
}
public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) {
@@ -570,16 +590,7 @@ public final class HydrologyTileCache implements AutoCloseable {
sharedCacheScope = Objects.requireNonNull(scope, "scope");
persistentStore = persistentRoot == null
? null
: new StudioHydrologyTileStore(persistentRoot, scope, planner.settings().routing().tileSize());
}
private boolean persistentStudioKey(HydrologyTileKey key) {
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTile = Math.subtractExact(tileCoordinate(-(long) publicationRadius, tileSize), 1);
int maximumTile = Math.addExact(tileCoordinate(CHUNK_SIZE - 1L + publicationRadius, tileSize), 1);
return key.tileX() >= minimumTile && key.tileX() <= maximumTile
&& key.tileZ() >= minimumTile && key.tileZ() <= maximumTile;
: new PreparedHydrologyTileStore(persistentRoot, scope, planner.settings().routing().tileSize());
}
@Override
@@ -622,6 +633,11 @@ public final class HydrologyTileCache implements AutoCloseable {
.join();
planning.clear();
diagnosing.clear();
synchronized (prefetchQueue) {
if (pregenerationScope != null) {
pregenerationScope.close();
}
}
clear();
}
@@ -833,11 +849,11 @@ public final class HydrologyTileCache implements AutoCloseable {
return;
}
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
centreBlockX, centreBlockZ), false);
centreBlockX, centreBlockZ), null);
}
private List<HydrologyTileKey> prefetchAreaKeys(int minimumBlockX, int minimumBlockZ, int maximumBlockX,
int maximumBlockZ, int centreBlockX, int centreBlockZ) {
private List<HydrologyTileKey> prefetchAreaKeys(long minimumBlockX, long minimumBlockZ, long maximumBlockX,
long maximumBlockZ, int centreBlockX, int centreBlockZ) {
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
@@ -858,11 +874,20 @@ public final class HydrologyTileCache implements AutoCloseable {
return keys;
}
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, boolean discardOutsideArea) {
Set<HydrologyTileKey> area = discardOutsideArea ? new HashSet<>(keys) : Set.of();
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, PregenerationScope scope) {
Set<HydrologyTileKey> area = scope != null ? new HashSet<>(keys) : Set.of();
tiles.cleanUp();
synchronized (prefetchQueue) {
if (discardOutsideArea) {
if (closed.get()) {
if (scope != null) {
requireOpen();
}
return;
}
if (scope != null) {
scope.previousNeighbourPrefetch = pregenerationScope == null
? neighbourPrefetchEnabled : pregenerationScope.previousNeighbourPrefetch;
pregenerationScope = scope;
prefetchQueue.removeIf(key -> {
if (!area.contains(key)) {
queuedPrefetches.remove(key);
@@ -903,6 +928,7 @@ public final class HydrologyTileCache implements AutoCloseable {
private boolean enqueuePrefetch(HydrologyTileKey key) {
if (closed.get()
|| pregenerationScope != null && !pregenerationScope.bounds.contains(key)
|| tiles.getIfPresent(key) != null
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)
@@ -1056,7 +1082,8 @@ public final class HydrologyTileCache implements AutoCloseable {
if (!queuedPrefetches.remove(key)) {
continue;
}
if (tiles.getIfPresent(key) == null) {
if ((pregenerationScope == null || pregenerationScope.bounds.contains(key))
&& tiles.getIfPresent(key) == null) {
return key;
}
}
@@ -1322,6 +1349,48 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
public record PregenerationArea(
int centerBlockX, int centerBlockZ,
long minimumBlockX, long minimumBlockZ,
long maximumBlockX, long maximumBlockZ
) {
public PregenerationArea {
if (minimumBlockX > centerBlockX || maximumBlockX < centerBlockX
|| minimumBlockZ > centerBlockZ || maximumBlockZ < centerBlockZ) {
throw new IllegalArgumentException("Pregeneration bounds must contain their center.");
}
}
}
public final class PregenerationScope implements AutoCloseable {
private final TileBounds bounds;
private boolean previousNeighbourPrefetch;
private PregenerationScope(TileBounds bounds) {
this.bounds = bounds;
}
@Override
public void close() {
synchronized (prefetchQueue) {
if (pregenerationScope != this) {
return;
}
pregenerationScope = null;
neighbourPrefetchEnabled = previousNeighbourPrefetch;
prefetchQueue.clear();
queuedPrefetches.clear();
}
}
}
private record TileBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
private boolean contains(HydrologyTileKey key) {
return key.tileX() >= minimumX && key.tileX() <= maximumX
&& key.tileZ() >= minimumZ && key.tileZ() <= maximumZ;
}
}
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
private long estimatedBytes() {
long bytes = 64L + 8L * columns.length;
@@ -492,14 +492,6 @@ final class HydrologyTributaryPlanner {
}
}
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
// Where the two channels touch, the tributary must sit at the lowest stem water it reaches.
double touch = stemWidths.get(stemIndex) + lastWidth(result) + 1D;
for (int index = 0; index < stemStations.size(); index++) {
HydrologyPoint station = stemStations.get(index);
if (station.distanceSquared2D(joinStation) <= touch * touch) {
stemHead = Math.min(stemHead, station.y());
}
}
if (segments.getLast().downstreamHeadY() < stemHead) {
int shortfall = stemHead - segments.getLast().downstreamHeadY();
if (shortfall > TRIBUTARY_BACKWATER_LIMIT) {
@@ -10,16 +10,16 @@ import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
@Description("Bounds terrain removal outside the wet river channel.")
@Description("Bounds terrain shaping outside the wet river channel.")
public class IrisRiverExcavationConfig {
@MinNumber(0)
@MaxNumber(64)
@Description("Maximum blocks removed vertically from a dry bank column. Zero preserves dry bank heights.")
@Description("Maximum blocks cut or filled vertically in a dry bank column. Fill is also limited by the local channel incision policy. Zero preserves dry bank heights.")
private int maximumDepth = 8;
@MinNumber(1)
@MaxNumber(64)
@Description("Maximum horizontal distance from the channel outline affected by shore and bank excavation.")
@Description("Maximum horizontal distance from the channel outline affected by shore and bank shaping.")
private int maximumWidth = 16;
@MinNumber(0)
@@ -20,7 +20,7 @@ public class IrisSurfaceRiverBankConfig {
@MinNumber(0.5)
@MaxNumber(12)
@Description("Horizontal run in blocks of eroded bank per block of cut depth.")
@Description("Horizontal bank blend in blocks per block of terrain cut or fill.")
private double blendSlope = 3D;
@MinNumber(1)
@@ -33,7 +33,7 @@ public class IrisSurfaceRiverBankConfig {
@Description("Widest eroded band outside the shore, in blocks.")
private int maximumBlendWidth = 32;
@Description("Limits dry bank cut depth, width, and excavated volume independently of the wet channel.")
@Description("Limits dry bank cut depth, fill height, width, and excavated volume independently of the wet channel. Fill also respects channel.maximumIncision and local river policy.")
private IrisRiverExcavationConfig excavation = new IrisRiverExcavationConfig();
@Description("Show the biome's deeper layers on eroded banks instead of the surface layer.")
@@ -24,12 +24,12 @@ public class IrisSurfaceRiverChannelConfig {
@MinNumber(0)
@MaxNumber(3)
@Description("Blocks the water surface sinks below the lowest natural ground beside the channel; 0 keeps the water flush with the bank, and the bank always meets the water at its own height.")
@Description("Blocks the water surface sinks below the shaped bank top; 0 keeps the water flush with its banks.")
private int sink = 0;
@MinNumber(1)
@MaxNumber(32)
@Description("Maximum cut below natural terrain at the channel center before a course is rejected.")
@Description("Maximum channel cut below natural terrain. Also caps bank fill, together with banks.excavation.maximumDepth; courses exceeding containment or terrain limits are rejected.")
private int maximumIncision = 10;
@MinNumber(0)
@@ -41,8 +41,8 @@ import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
final class StudioHydrologyTileStore {
private static final int SCHEMA_VERSION = 5;
final class PreparedHydrologyTileStore {
private static final int SCHEMA_VERSION = 6;
private static final long MAXIMUM_COMPRESSED_BYTES = 128L * 1024L * 1024L;
private static final long MAXIMUM_DECOMPRESSED_BYTES = 512L * 1024L * 1024L;
private static final Gson GSON = new GsonBuilder()
@@ -51,19 +51,21 @@ final class StudioHydrologyTileStore {
.create();
private final Path directory;
private final String scopeIdentity;
private final HydrologyTileCache.SharedCacheScope scope;
private final int expectedTileSize;
StudioHydrologyTileStore(Path root, HydrologyTileCache.SharedCacheScope scope, int expectedTileSize) {
PreparedHydrologyTileStore(Path root, HydrologyTileCache.SharedCacheScope scope, int expectedTileSize) {
this.scope = Objects.requireNonNull(scope, "scope");
if (expectedTileSize < 1) {
throw new IllegalArgumentException("Hydrology tile size must be positive.");
}
this.expectedTileSize = expectedTileSize;
this.scopeIdentity = scopeFingerprint(scope);
this.directory = Objects.requireNonNull(root, "root")
.toAbsolutePath()
.normalize()
.resolve(scopeFingerprint(scope));
.resolve(scopeIdentity);
}
Optional<HydrologyTile> load(HydrologyTileKey key) {
@@ -85,6 +87,7 @@ final class StudioHydrologyTileStore {
PersistedTile persisted = GSON.fromJson(reader, PersistedTile.class);
if (persisted == null
|| persisted.schemaVersion() != SCHEMA_VERSION
|| !scopeIdentity.equals(persisted.scopeIdentity())
|| !key.equals(persisted.key())
|| persisted.worldSeed() != scope.worldSeed()
|| persisted.settingsFingerprint() != scope.settingsFingerprint()) {
@@ -99,7 +102,7 @@ final class StudioHydrologyTileStore {
void save(HydrologyTile tile) throws IOException {
if (!valid(tile, tile.key())) {
throw new IOException("Refused to persist a hydrology tile outside the active Studio cache scope.");
throw new IOException("Refused to persist a hydrology tile outside the active prepared-plan scope.");
}
Files.createDirectories(directory);
Path target = file(tile.key());
@@ -109,10 +112,10 @@ final class StudioHydrologyTileStore {
OutputStream buffered = new BufferedOutputStream(raw);
OutputStream compressed = new GZIPOutputStream(buffered);
Writer writer = new OutputStreamWriter(compressed, StandardCharsets.UTF_8)) {
GSON.toJson(PersistedTile.from(tile), writer);
GSON.toJson(PersistedTile.from(tile, scopeIdentity), writer);
}
if (Files.size(staged) > MAXIMUM_COMPRESSED_BYTES) {
throw new IOException("Studio hydrology tile cache entry exceeds the size limit.");
throw new IOException("Prepared hydrology tile cache entry exceeds the size limit.");
}
try {
Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
@@ -161,6 +164,7 @@ final class StudioHydrologyTileStore {
private record PersistedTile(
int schemaVersion,
String scopeIdentity,
HydrologyTileKey key,
long worldSeed,
long settingsFingerprint,
@@ -174,7 +178,7 @@ final class StudioHydrologyTileStore {
List<HydrologyDiagnosticCandidate> localDiagnosticCandidates,
List<HydrologyColumnSample> columns
) {
private static PersistedTile from(HydrologyTile tile) {
private static PersistedTile from(HydrologyTile tile, String scopeIdentity) {
ArrayList<PersistedCourse> courses = new ArrayList<>(tile.courses().size());
for (RiverCourse course : tile.courses()) {
courses.add(PersistedCourse.from(course));
@@ -187,6 +191,7 @@ final class StudioHydrologyTileStore {
}
return new PersistedTile(
SCHEMA_VERSION,
scopeIdentity,
tile.key(),
tile.worldSeed(),
tile.settingsFingerprint(),
@@ -375,7 +380,7 @@ final class StudioHydrologyTileStore {
private void advance(long amount) throws IOException {
count += amount;
if (count > limit) {
throw new IOException("Studio hydrology tile cache entry exceeds the decompressed size limit.");
throw new IOException("Prepared hydrology tile cache entry exceeds the decompressed size limit.");
}
}
}
@@ -9,7 +9,6 @@ record RefinedEdgeKey(
int downstreamZ,
int continuationX,
int continuationZ,
int transverseCandidates,
boolean surface
int transverseCandidates
) {
}
@@ -1,4 +0,0 @@
package art.arcane.iris.generation.hydrology;
record RouteTerrainEstimate(int height, double terrainScore) {
}
@@ -1,6 +0,0 @@
package art.arcane.iris.generation.hydrology;
import java.util.List;
record SurfaceRouteKey(List<HydrologyPoint> guide, long geometryId) {
}
@@ -170,8 +170,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
cache.prefetchArea(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ, centreBlockX, centreBlockZ);
}
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
cache.preparePregeneration(centerBlockX, centerBlockZ);
public HydrologyTileCache.PregenerationScope preparePregeneration(HydrologyTileCache.PregenerationArea area) {
return cache.preparePregeneration(area);
}
public void setNeighbourPrefetchEnabled(boolean enabled) {
@@ -31,15 +31,17 @@ public final class ErosionFieldCompiler {
private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler;
private final int seaLevel;
private final double maximumShoreWidth;
public ErosionFieldCompiler(
HydrologyPlannerSettings.Surface surface,
HydrologyTerrainSampler sampler,
int seaLevel
HydrologyPlannerSettings settings,
HydrologyTerrainSampler sampler
) {
this.surface = Objects.requireNonNull(surface, "surface");
Objects.requireNonNull(settings, "settings");
this.surface = settings.surface();
this.sampler = Objects.requireNonNull(sampler, "sampler");
this.seaLevel = seaLevel;
this.seaLevel = settings.seaLevel();
this.maximumShoreWidth = Math.max(surface.shoreWidth(), settings.widestShoreBiomeWidth());
}
public ErosionField compile(
@@ -80,11 +82,12 @@ public final class ErosionFieldCompiler {
SurfaceRasterContext context
) {
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit, ponds, context,
prepare(centerline, channel, valley, terminal));
prepare(centerline, channel, valley, terminal, terminal == SurfaceTerminal.OCEAN_MOUTH
? inletStart(valley.exposedStations()) : centerline.size()));
}
PreparedField prepare(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley,
SurfaceTerminal terminal) {
SurfaceTerminal terminal, int inletStart) {
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
boolean[] basin = basins(valley, channel, count);
double[] bendOffsets = SurfaceBendProfile.offsets(centerline, channel);
@@ -99,7 +102,7 @@ public final class ErosionFieldCompiler {
radii[station] = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + surface.banks().roughness())
+ Math.max(stationShore + widest, stationBand)) + 1;
}
return new PreparedField(count, basin, bendOffsets, blendWidth, radii);
return new PreparedField(count, basin, bendOffsets, blendWidth, radii, inletStart);
}
ErosionField compile(long courseSeed, SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley,
@@ -157,6 +160,7 @@ public final class ErosionFieldCompiler {
}
}
int oceanStart = oceanStart(centerline, valley, terminal);
int inletStart = prepared.inletStart();
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(nearest.size());
LongArrayList wetKeys = new LongArrayList();
for (long key : nearest.keySet()) {
@@ -168,7 +172,6 @@ public final class ErosionFieldCompiler {
if (!context.boundary().allows(cellX, cellZ) || context.drops().connects(cellX, cellZ, head)) {
continue;
}
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
double halfWidth = channel.halfWidth()[station];
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
@@ -176,6 +179,13 @@ public final class ErosionFieldCompiler {
Math.max(0.5D, channelSettings.outlineMinimumRatio() * halfWidth),
Math.min(channelSettings.outlineMaximumRatio() * halfWidth, outline));
boolean wet = cellDistance <= outline + 0.25D;
double bankDistance = cellDistance - outline;
if (!wet && cellDistance > outline + maximumShoreWidth + 0.25D
&& (bankDistance > erosion.excavation().maximumWidth() || !erosion.enabled()
|| bankDistance - maximumShoreWidth >= blendWidth[station][side(centerline, station, cellX, cellZ) < 0D ? 0 : 1])) {
continue;
}
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
boolean mouthLand = terminal == SurfaceTerminal.OCEAN_MOUTH && head == seaLevel
&& SurfaceCellAdmission.mouthLand(terrain, seaLevel);
if (!SurfaceCellAdmission.writable(terrain, seaLevel) && !mouthLand) {
@@ -198,6 +208,10 @@ public final class ErosionFieldCompiler {
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
+ (basin[station] ? flow.plungeBasinDepth() : 0D);
int bed = Math.min(natural, head - Math.max(1, (int) StrictMath.round(local)));
int minimumBed = natural - maximumIncision(terrain, station >= inletStart);
if (minimumBed < head) {
bed = Math.max(bed, minimumBed);
}
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
wetKeys.add(key);
continue;
@@ -212,7 +226,6 @@ public final class ErosionFieldCompiler {
// geometric shore with the bank biome.
boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D;
SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK;
double bankDistance = cellDistance - outline;
if (bankDistance > erosion.excavation().maximumWidth()) {
if (shoreBiome) {
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
@@ -220,14 +233,14 @@ public final class ErosionFieldCompiler {
continue;
}
if (cellDistance <= outline + shore + 0.25D) {
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(cellDistance, outline, shore)));
height = boundedBankHeight(natural, height, erosion.excavation());
int height = bankTop + (int) StrictMath.round(shoreRise * benchProgress(cellDistance, outline, shore));
height = boundedBankHeight(terrain, height, erosion.excavation());
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
continue;
}
int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
int cut = natural - shoreTop;
if (cut <= 0 || !erosion.enabled() || !terrain.erosion()) {
if (cut == 0 || !erosion.enabled() || !terrain.erosion()) {
if (shoreBiome) {
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
}
@@ -243,8 +256,8 @@ public final class ErosionFieldCompiler {
}
continue;
}
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
height = boundedBankHeight(natural, height, erosion.excavation());
int height = (int) StrictMath.round(shoreTop + cut * blend(progress, erosion));
height = boundedBankHeight(terrain, height, erosion.excavation());
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
}
if (count > 0) {
@@ -255,7 +268,7 @@ public final class ErosionFieldCompiler {
}
int uncontained = contain(columns, wetKeys, sink, bounds, context.drops());
connectSteps(columns, wetKeys);
return validate(columns, wetKeys, centerline, terminal, bounds, uncontained);
return validate(columns, wetKeys, centerline, terminal, bounds, uncontained, inletStart);
}
/**
@@ -340,6 +353,10 @@ public final class ErosionFieldCompiler {
if (wasWet) {
bed = Math.min(bed, existing.height());
}
int minimumBed = natural - maximumIncision(terrain, false);
if (minimumBed < head) {
bed = Math.max(bed, minimumBed);
}
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
if (!wasWet) {
wetKeys.add(key);
@@ -353,24 +370,24 @@ public final class ErosionFieldCompiler {
continue;
}
if (distance <= outline + shore + 0.25D) {
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore)));
height = boundedBankHeight(natural, height, erosion.excavation());
int height = bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore));
height = boundedBankHeight(terrain, height, erosion.excavation());
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
continue;
}
int cut = natural - shoreTop;
if (cut <= 0 || !erosion.enabled() || !erode) {
if (cut == 0 || !erosion.enabled() || !erode) {
continue;
}
double width = Math.max(banks.minimumBlendWidth(),
Math.min(banks.maximumBlendWidth(), cut * banks.blendSlope() + erosion.blendBaseWidth()));
Math.min(banks.maximumBlendWidth(), Math.abs(cut) * banks.blendSlope() + erosion.blendBaseWidth()));
width = Math.min(width, Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
double progress = (distance - outline - shore) / width;
if (progress >= 1D) {
continue;
}
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
height = boundedBankHeight(natural, height, erosion.excavation());
int height = (int) StrictMath.round(shoreTop + cut * blend(progress, erosion));
height = boundedBankHeight(terrain, height, erosion.excavation());
if (existing != null && existing.height() <= height) {
continue;
}
@@ -401,7 +418,8 @@ public final class ErosionFieldCompiler {
int cellX = (int) StrictMath.round(centreX + Math.cos(angle) * ring);
int cellZ = (int) StrictMath.round(centreZ + Math.sin(angle) * ring);
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
if (!SurfaceCellAdmission.writable(terrain, seaLevel) || terrain.naturalHeight() < bankTop) {
if (!SurfaceCellAdmission.writable(terrain, seaLevel)
|| (long) terrain.naturalHeight() + maximumFill(terrain) < bankTop) {
return false;
}
}
@@ -512,20 +530,17 @@ public final class ErosionFieldCompiler {
int uncontained = 0;
for (int index = 0; index < wetKeys.size(); index++) {
SurfaceColumn wet = columns.get(wetKeys.getLong(index));
if (bounds != null && !bounds.contains(wet.x(), wet.z())) {
continue;
}
uncontained += lip(columns, wet, CARDINALS, sink, true, drops);
boolean owned = bounds == null || bounds.contains(wet.x(), wet.z());
uncontained += lip(columns, wet, CARDINALS, sink, owned, drops);
lip(columns, wet, DIAGONALS, sink, false, drops);
}
return uncontained;
}
/**
* The bank beside water must reach the natural ground the head was taken from: the water's own
* block level plus the sink. A neighbour lower than that is raised to it. The solver takes the head
* from the banks it samples at each station. A missed low cell can retain its natural ground,
* but cannot be raised above it. A shortfall below the water head stays uncontained and is counted.
* The bank beside water reaches the planned water level plus the sink. Low neighbours may be
* filled within the local channel and bank terrain limits. Any remaining shortfall below the
* water head stays uncontained and is counted.
*/
private int lip(
Long2ObjectOpenHashMap<SurfaceColumn> columns,
@@ -558,8 +573,7 @@ public final class ErosionFieldCompiler {
}
int required = wet.headY() + sink;
if (neighbour.height() < required) {
int ceiling = neighbour.terrain().naturalHeight();
int raised = Math.min(ceiling, required);
int raised = boundedBankHeight(neighbour.terrain(), required, surface.banks().erosion().excavation());
neighbour = neighbour.withHeight(raised);
columns.put(key, neighbour);
}
@@ -584,18 +598,51 @@ public final class ErosionFieldCompiler {
return count;
}
private static int boundedBankHeight(int natural, int target, HydrologyPlannerSettings.Excavation limits) {
return Math.max(natural - limits.maximumDepth(), Math.min(natural, target));
private int boundedBankHeight(HydrologyTerrainSample terrain, int target, HydrologyPlannerSettings.Excavation limits) {
int natural = terrain.naturalHeight();
return (int) Math.clamp((long) target, (long) natural - limits.maximumDepth(),
(long) natural + maximumFill(terrain));
}
private int maximumFill(HydrologyTerrainSample terrain) {
int incision = terrain.surfacePolicy().maximumIncision(surface.maximumIncision());
return Math.min(surface.banks().erosion().excavation().maximumDepth(),
Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier())));
}
int inletStart(int exposedStations) {
HydrologyPlannerSettings.Inlet inlet = surface.banks().inlet();
int reach = Math.min(inlet.length(), (int) StrictMath.floor(exposedStations * inlet.courseFraction()));
return Math.max(0, exposedStations - reach - reach / 2);
}
private int maximumIncision(HydrologyTerrainSample terrain, boolean inlet) {
int incision = terrain.surfacePolicy().maximumIncision(surface.maximumIncision());
if (inlet) {
incision = Math.max(incision, surface.banks().inlet().maximumIncision());
}
return Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
}
private ErosionField validate(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys,
SurfaceCenterline centerline, SurfaceTerminal terminal, SurfaceBounds bounds, int uncontained) {
SurfaceCenterline centerline, SurfaceTerminal terminal,
SurfaceBounds bounds, int uncontained, int inletStart) {
long excavation = 0L;
int[] stationVolumes = new int[centerline.size()];
int deepest = 0;
for (SurfaceColumn column : columns.values()) {
if (column.apron() || column.role() == SurfaceRole.CHANNEL
|| bounds != null && !bounds.contains(column.x(), column.z())) {
if (column.apron() || bounds != null && !bounds.contains(column.x(), column.z())) {
continue;
}
if (column.role() == SurfaceRole.CHANNEL) {
HydrologyTerrainSample terrain = column.terrain();
int incision = maximumIncision(terrain, column.station() >= inletStart);
int cut = terrain.naturalHeight() - column.height();
if (cut > incision) {
return new ErosionField(columns, uncontained,
HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
cut, excavation);
}
continue;
}
int cut = Math.max(0, column.terrain().naturalHeight() - column.height());
@@ -613,14 +660,20 @@ public final class ErosionFieldCompiler {
return new ErosionField(columns, uncontained, HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT,
uncontained, excavation);
}
if (bounds == null && terminal == SurfaceTerminal.OCEAN_MOUTH && !connectedToOcean(columns, wetKeys)) {
if (bounds == null && terminal == SurfaceTerminal.OCEAN_MOUTH && !connectedToOcean(columns, wetKeys, centerline)) {
return new ErosionField(columns, 0, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, excavation);
}
return new ErosionField(columns, uncontained, null, 0, excavation);
}
private boolean connectedToOcean(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys) {
private boolean connectedToOcean(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys,
SurfaceCenterline centerline) {
if (wetKeys.isEmpty()) {
for (int station = 0; station < centerline.size(); station++) {
if (sampler.receivingWater(centerline.x()[station], centerline.z()[station], seaLevel)) {
return true;
}
}
return false;
}
long firstKey = wetKeys.getLong(0);
@@ -700,7 +753,7 @@ public final class ErosionFieldCompiler {
int probeX = (int) StrictMath.round(centerline.x()[station] + centerline.normalX(station) * probe * direction);
int probeZ = (int) StrictMath.round(centerline.z()[station] + centerline.normalZ(station) * probe * direction);
HydrologyTerrainSample terrain = sampler.sample(probeX, probeZ);
double cut = terrain == null ? 0D : Math.max(0D, terrain.naturalHeight() - bankTop);
double cut = terrain == null ? 0D : Math.abs(terrain.naturalHeight() - (double) bankTop);
double width = cut * banks.blendSlope() * channel.bankMultiplier()[station] + erosion.blendBaseWidth();
widths[station][side] = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), width));
}
@@ -765,6 +818,7 @@ public final class ErosionFieldCompiler {
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
}
record PreparedField(int count, boolean[] basin, double[] bendOffsets, double[][] blendWidth, int[] radii) {
record PreparedField(int count, boolean[] basin, double[] bendOffsets, double[][] blendWidth, int[] radii,
int inletStart) {
}
}
@@ -0,0 +1,169 @@
package art.arcane.iris.generation.hydrology.surface;
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyPoint;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
import java.util.List;
import java.util.Objects;
final class RiverProfileSolver {
private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler;
private final int seaLevel;
RiverProfileSolver(Options options, HydrologyTerrainSampler sampler) {
this.surface = Objects.requireNonNull(options.surface());
this.sampler = Objects.requireNonNull(sampler);
this.seaLevel = options.seaLevel();
}
ValleyProfile solve(List<HydrologyPoint> path, SurfaceCenterline centerline, ChannelProfile channel,
SurfaceTerminal terminal, int terminalHead, int minimumLength) {
int count = centerline.size();
int[] heads = interpolate(path, centerline);
int[] natural = new int[count];
int[] incision = new int[count];
int[] inletIncision = new int[count];
int exposed = count;
double length = 0D;
for (int station = 0; station < count; station++) {
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]);
boolean writable = terminal == SurfaceTerminal.OCEAN_MOUTH
? SurfaceCellAdmission.mouthLand(terrain, seaLevel)
: SurfaceCellAdmission.writable(terrain, seaLevel);
if (!writable) {
exposed = station;
break;
}
if (!terrain.transitAllowed()) {
return ValleyProfile.rejected(HydrologyCandidateRejection.POLICY_EXCLUDED, station);
}
natural[station] = terrain.naturalHeight();
int limit = terrain.surfacePolicy().maximumIncision(surface.maximumIncision());
incision[station] = Math.min(limit, (int) StrictMath.floor(limit * terrain.incisionMultiplier()));
int inletLimit = Math.max(limit, surface.banks().inlet().maximumIncision());
inletIncision[station] = Math.min(inletLimit,
(int) StrictMath.floor(inletLimit * terrain.incisionMultiplier()));
if (station > 0) {
length += StrictMath.hypot((double) centerline.x()[station] - centerline.x()[station - 1],
(double) centerline.z()[station] - centerline.z()[station - 1]);
}
}
if (exposed < 2 || length < minimumLength) {
return ValleyProfile.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, (int) StrictMath.floor(length));
}
if (exposed < count && terminal != SurfaceTerminal.OCEAN_MOUTH
&& terminal != SurfaceTerminal.COASTAL_GROTTO) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_EXPOSURE, exposed);
}
int floor = terminal == SurfaceTerminal.OCEAN_MOUTH || terminal == SurfaceTerminal.COASTAL_GROTTO
? Math.max(seaLevel, terminalHead) : terminalHead;
for (int station = 0; station < exposed; station++) {
heads[station] = Math.max(heads[station], floor);
}
if (terminal == SurfaceTerminal.TRIBUTARY) {
gradeTerminal(heads, exposed, terminalHead, surface.banks().cascadeRun());
}
int inletStart = exposed;
if (terminal == SurfaceTerminal.OCEAN_MOUTH) {
inletStart = gradeInlet(heads, natural, inletIncision, channel, exposed);
for (int station = exposed; station < count; station++) {
heads[station] = seaLevel;
}
}
int sourceHead = heads[0];
int downstream = floor;
for (int station = exposed - 1; station >= 0; station--) {
int depth = (int) StrictMath.round(channel.depth()[station]);
int limit = incision[station];
if (station >= inletStart) {
limit = inletIncision[station];
}
int required = Math.max(Math.max(heads[station], downstream), natural[station] - limit + depth);
if (required > sourceHead || terminal == SurfaceTerminal.TRIBUTARY
&& station == exposed - 1 && required > terminalHead) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
natural[station] - heads[station] + depth);
}
heads[station] = required;
downstream = required;
}
for (int station = 1; station < exposed; station++) {
int drop = heads[station - 1] - heads[station];
if (drop >= surface.banks().waterfallMinimumDrop()) {
continue;
}
int limit = station - 1 >= inletStart ? inletIncision[station - 1] : incision[station - 1];
int connectedHead = Math.max(heads[station - 1] - 1, natural[station - 1] - limit + 1);
if (terminal == SurfaceTerminal.TRIBUTARY && station == exposed - 1 && connectedHead > terminalHead) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
natural[station - 1] - terminalHead + 1);
}
heads[station] = Math.max(heads[station], Math.min(heads[station - 1], connectedHead));
}
return new ValleyProfile(heads, natural, natural, natural, exposed, null, 0);
}
private static int[] interpolate(List<HydrologyPoint> path, SurfaceCenterline centerline) {
int[] heads = new int[centerline.size()];
for (int station = 0; station < heads.length; station++) {
int index = Math.min(centerline.pathIndex()[station], path.size() - 1);
HydrologyPoint start = path.get(index);
HydrologyPoint end = path.get(Math.min(index + 1, path.size() - 1));
double dx = (double) end.x() - start.x();
double dz = (double) end.z() - start.z();
double squared = dx * dx + dz * dz;
double progress = squared == 0D ? 0D : Math.clamp(
((centerline.x()[station] - (double) start.x()) * dx
+ (centerline.z()[station] - (double) start.z()) * dz) / squared, 0D, 1D);
int head = (int) StrictMath.round(start.y() + ((double) end.y() - start.y()) * progress);
heads[station] = station == 0 ? head : Math.min(heads[station - 1], head);
}
return heads;
}
private static void gradeTerminal(int[] heads, int exposed, int terminalHead, int run) {
for (int station = exposed - 1; station >= 0; station--) {
long target = (long) terminalHead + Math.floorDiv(exposed - 1L - station, Math.max(1, run));
if (heads[station] <= target) {
break;
}
heads[station] = (int) Math.min(Integer.MAX_VALUE, target);
}
}
private int gradeInlet(int[] heads, int[] natural, int[] incision, ChannelProfile channel, int exposed) {
HydrologyPlannerSettings.Inlet inlet = surface.banks().inlet();
int reach = Math.min(inlet.length(), (int) StrictMath.floor(exposed * inlet.courseFraction()));
int start = exposed;
while (start > 0 && exposed - start < reach) {
int index = start - 1;
int cut = natural[index] - seaLevel + (int) StrictMath.round(channel.depth()[index]);
if (cut > incision[index]) {
break;
}
heads[index] = seaLevel;
start--;
}
int rampStart = start;
for (int station = start - 1; station >= Math.max(0, start - reach / 2); station--) {
int target = seaLevel + (int) StrictMath.round((start - station) * inlet.rampSlope());
if (heads[station] <= target) {
break;
}
int cut = natural[station] - target + (int) StrictMath.round(channel.depth()[station]);
if (cut > incision[station]) {
break;
}
heads[station] = target;
rampStart = station;
}
return rampStart;
}
record Options(HydrologyPlannerSettings.Surface surface, int seaLevel) {
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.hydrology.HydraulicSegment;
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.generation.hydrology.HydrologyHash;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyPoint;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
@@ -15,6 +16,8 @@ import java.util.List;
import java.util.Objects;
public final class SurfaceCourseBuilder {
private static final long COASTAL_DROP_SALT = 0x434f41535446414cL;
private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler;
private final HydrologyGeometrySampler geometry;
@@ -45,14 +48,14 @@ public final class SurfaceCourseBuilder {
boolean directOcean = terminal == SurfaceTerminal.OCEAN_MOUTH;
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, geometry)
.build(centerline, profileKey, directOcean);
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, seaLevel, minimumCourseLength)
.solve(centerline, channel, terminal, terminalHead);
ValleyProfile valley = new RiverProfileSolver(new RiverProfileSolver.Options(surface, seaLevel), sampler)
.solve(path, centerline, channel, terminal, terminalHead, minimumCourseLength);
if (!valley.accepted()) {
return SurfaceCourseResult.rejected(valley.rejection(), valley.rejectionDetail());
}
int exposed = valley.exposedStations();
boolean coastalDrop = directOcean && valley.head()[exposed - 1] > seaLevel;
int stations = coastalDrop ? exposed + 1 : exposed;
int stations = exposed;
int[] x = new int[stations];
int[] z = new int[stations];
int[] head = new int[stations];
@@ -65,40 +68,52 @@ public final class SurfaceCourseBuilder {
width[station] = channel.halfWidth()[station] * 2D;
depth[station] = channel.depth()[station];
}
if (coastalDrop) {
int last = exposed - 1;
if (exposed < centerline.size()) {
x[exposed] = centerline.x()[exposed];
z[exposed] = centerline.z()[exposed];
} else {
x[exposed] = x[last] + (int) StrictMath.round(centerline.tangentX()[last]);
z[exposed] = z[last] + (int) StrictMath.round(centerline.tangentZ()[last]);
if (x[exposed] == x[last] && z[exposed] == z[last]) {
x[exposed] = x[last] + 1;
}
}
head[exposed] = seaLevel;
width[exposed] = width[last];
depth[exposed] = depth[last];
}
List<HydraulicSegment> segments = fallingTransitions(SurfaceSegmentLabeler.label(
worldSeed, courseId, x, z, head, width, depth, surface.banks()));
ArrayList<HydraulicSegment> segments = fallingTransitions(SurfaceSegmentLabeler.label(
worldSeed, courseId, x, z, head, width, depth, surface.banks()), directOcean);
if (segments.isEmpty()) {
return SurfaceCourseResult.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, stations);
}
if (coastalDrop) {
int last = exposed - 1;
int receiverX;
int receiverZ;
if (exposed < centerline.size()) {
receiverX = centerline.x()[exposed];
receiverZ = centerline.z()[exposed];
} else {
receiverX = x[last] + (int) StrictMath.round(centerline.tangentX()[last]);
receiverZ = z[last] + (int) StrictMath.round(centerline.tangentZ()[last]);
if (receiverX == x[last] && receiverZ == z[last]) {
receiverX = x[last] + 1;
}
}
int drop = head[last] - seaLevel;
HydrologyFeatureType type = drop == 1 ? HydrologyFeatureType.RIFFLE
: drop >= surface.banks().waterfallMinimumDrop()
? HydrologyFeatureType.WATERFALL : HydrologyFeatureType.CASCADE;
boolean receiving = sampler.receivingWater(receiverX, receiverZ, seaLevel);
segments.add(new HydraulicSegment(HydrologyHash.mix(worldSeed, COASTAL_DROP_SALT, courseId),
courseId, type, head[last], seaLevel,
Math.max(1, (int) StrictMath.ceil(width[last])),
Math.max(1, (int) StrictMath.ceil(depth[last])), receiving, receiving,
List.of(new HydrologyPoint(x[last], head[last], z[last]),
new HydrologyPoint(receiverX, seaLevel, receiverZ)),
new HydraulicChannelProfile(new double[]{width[last], width[last]},
new double[]{depth[last], depth[last]})));
}
HydraulicSegment last = segments.getLast();
int lastStation = stations - 1;
return new SurfaceCourseResult(
segments,
head[lastStation],
last.downstreamHeadY(),
Math.max(1, (int) StrictMath.ceil(last.channelProfile().widthAt(last.centerline().size() - 1))),
Math.max(1, (int) StrictMath.ceil(last.channelProfile().depthAt(last.centerline().size() - 1))),
new HydrologyPoint(x[lastStation], head[lastStation], z[lastStation]),
last.end(),
null,
0
);
}
private List<HydraulicSegment> fallingTransitions(List<HydraulicSegment> labelled) {
private ArrayList<HydraulicSegment> fallingTransitions(List<HydraulicSegment> labelled, boolean directOcean) {
ArrayList<HydraulicSegment> segments = new ArrayList<>(labelled);
for (int index = 0; index + 1 < segments.size(); index++) {
HydraulicSegment fall = segments.get(index);
@@ -112,6 +127,9 @@ public final class SurfaceCourseBuilder {
continue;
}
int incision = terrain.surfacePolicy().maximumIncision(surface.maximumIncision());
if (directOcean && fall.downstreamHeadY() == seaLevel) {
incision = Math.max(incision, surface.banks().inlet().maximumIncision());
}
incision = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
if (terrain.naturalHeight() - fall.downstreamHeadY() + fall.depth() > incision) {
continue;
@@ -142,7 +160,7 @@ public final class SurfaceCourseBuilder {
receiver.fallingFluid(), receiver.receivingPool(),
receiver.centerline().subList(1, receiver.centerline().size()), receiverProfile));
}
return List.copyOf(segments);
return segments;
}
}
@@ -69,12 +69,11 @@ public final class SurfaceFootprintCompiler {
}
if (prepared.receivingSampler == null) {
prepared.receivingSampler = HydrologyOceanReceiver.forCourse(settings, sampler, course);
prepared.offsets = stationOffsets(course.segments());
prepared.inlet = SurfaceInletRange.forCourse(settings, course, prepared.receivingSampler);
}
HydrologyTerrainSampler receivingSampler = prepared.receivingSampler;
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, receivingSampler, geometry, course,
bounds == null ? null : bounds.expand(2));
int[] offsets = prepared.offsets;
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>();
int uncontained = 0;
long excavation = 0L;
@@ -90,7 +89,7 @@ public final class SurfaceFootprintCompiler {
while (after < course.segments().size() && exposedSegment(course.segments().get(after))) {
after++;
}
SurfaceFootprint run = compileRun(prepared, first, after, offsets[first], bounds, drops, pool, receivingSampler);
SurfaceFootprint run = compileRun(prepared, first, after, prepared.inlet.offset(first), bounds, drops, pool, receivingSampler);
columns.addAll(run.columns());
uncontained += run.uncontainedWetCells();
excavation += run.bankExcavation();
@@ -123,7 +122,7 @@ public final class SurfaceFootprintCompiler {
}
private PreparedRun prepareRun(RiverCourse course, int first, int after, boolean pool,
HydrologyTerrainSampler receivingSampler) {
HydrologyTerrainSampler receivingSampler, SurfaceInletRange inlet) {
List<HydraulicSegment> exposed = course.segments().subList(first, after);
Stations stations = stations(exposed);
boolean fallingEnd = after < course.segments().size() && course.segments().get(after).fallingFluid();
@@ -141,9 +140,9 @@ public final class SurfaceFootprintCompiler {
: new HydrologyPlannerSettings.Ponds(first == 0 ? settings.surface().banks().ponds().source() : disabled.source(),
fallingEnd ? disabled.terminal() : settings.surface().banks().ponds().terminal());
SurfaceRunBoundary boundary = runBoundary(course, first, after, centerline, channel);
ErosionFieldCompiler erosion = new ErosionFieldCompiler(settings.surface(), receivingSampler, settings.seaLevel());
ErosionFieldCompiler erosion = new ErosionFieldCompiler(settings, receivingSampler);
return new PreparedRun(exposed, stations, coastalChannel, centerline, channel, poolBiome, valley,
terminal, ponds, boundary, erosion, erosion.prepare(centerline, channel, valley, terminal));
terminal, ponds, boundary, erosion, erosion.prepare(centerline, channel, valley, terminal, inlet.firstStation(first)));
}
private SurfaceFootprint compileRun(PreparedCourse prepared, int first, int after, int stationOffset,
@@ -152,7 +151,7 @@ public final class SurfaceFootprintCompiler {
RiverCourse course = prepared.course;
PreparedRun run = prepared.runs[first];
if (run == null) {
run = prepareRun(course, first, after, pool, receivingSampler);
run = prepareRun(course, first, after, pool, receivingSampler, prepared.inlet);
prepared.runs[first] = run;
}
List<HydraulicSegment> exposed = run.exposed();
@@ -210,26 +209,6 @@ public final class SurfaceFootprintCompiler {
+ settings.surface().banks().maximumBlendWidth() + settings.surface().shoreWidth() + 2D;
}
private static int[] stationOffsets(List<HydraulicSegment> segments) {
int[] offsets = new int[segments.size()];
int station = 0;
HydrologyPoint previous = null;
for (int segmentIndex = 0; segmentIndex < segments.size(); segmentIndex++) {
List<HydrologyPoint> points = segments.get(segmentIndex).centerline();
for (int pointIndex = 0; pointIndex < points.size(); pointIndex++) {
HydrologyPoint point = points.get(pointIndex);
if (previous != null) {
station += Math.max(Math.abs(point.x() - previous.x()), Math.abs(point.z() - previous.z()));
}
if (pointIndex == 0) {
offsets[segmentIndex] = station;
}
previous = point;
}
}
return offsets;
}
private ChannelProfile acceptedProfile(Stations stations, SurfaceCenterline centerline) {
int count = centerline.size();
double[] halfWidth = new double[count];
@@ -406,7 +385,7 @@ public final class SurfaceFootprintCompiler {
private final RiverCourse course;
private final PreparedRun[] runs;
private HydrologyTerrainSampler receivingSampler;
private int[] offsets;
private SurfaceInletRange inlet;
private PreparedCourse(SurfaceFootprintCompiler compiler, RiverCourse course) {
this.compiler = compiler;
@@ -0,0 +1,104 @@
package art.arcane.iris.generation.hydrology.surface;
import art.arcane.iris.generation.hydrology.HydraulicSegment;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyPoint;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.generation.hydrology.RiverCourse;
import art.arcane.iris.generation.hydrology.RiverCourseType;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.List;
public final class SurfaceInletRange {
private final int[] offsets;
private final int firstStation;
private final Long2ObjectOpenHashMap<LongOpenHashSet> fallingStations;
private SurfaceInletRange(int[] offsets, int firstStation,
Long2ObjectOpenHashMap<LongOpenHashSet> fallingStations) {
this.offsets = offsets;
this.firstStation = firstStation;
this.fallingStations = fallingStations;
}
public static SurfaceInletRange forCourse(HydrologyPlannerSettings settings, RiverCourse course,
HydrologyTerrainSampler receivingSampler) {
List<HydraulicSegment> segments = course.segments();
int[] offsets = offsets(segments);
Long2ObjectOpenHashMap<LongOpenHashSet> fallingStations = new Long2ObjectOpenHashMap<>();
if (course.type() != RiverCourseType.SURFACE || segments.isEmpty()
|| segments.getLast().type() != HydrologyFeatureType.MOUTH) {
return new SurfaceInletRange(offsets, Integer.MAX_VALUE, fallingStations);
}
int exposed = exposedStations(settings.seaLevel(), segments, offsets, receivingSampler);
HydrologyPlannerSettings.Inlet inlet = settings.surface().banks().inlet();
int reach = Math.min(inlet.length(), (int) StrictMath.floor(exposed * inlet.courseFraction()));
int firstStation = Math.max(0, exposed - reach - reach / 2);
for (int index = 0; index < segments.size(); index++) {
HydraulicSegment segment = segments.get(index);
if (!segment.type().isSurface() || !segment.fallingFluid()) {
continue;
}
SurfaceCenterline centerline = SurfaceCenterline.densify(segment.centerline());
int first = Math.max(0, firstStation - offsets[index]);
if (first >= centerline.size()) {
continue;
}
LongOpenHashSet positions = new LongOpenHashSet(centerline.size() - first);
for (int station = first; station < centerline.size(); station++) {
positions.add(RiverFootprint.pack(centerline.x()[station], centerline.z()[station]));
}
fallingStations.put(segment.id(), positions);
}
return new SurfaceInletRange(offsets, firstStation, fallingStations);
}
public int offset(int segmentIndex) {
return offsets[segmentIndex];
}
public int firstStation(int segmentIndex) {
return Math.max(0, firstStation - offsets[segmentIndex]);
}
public boolean allowsFallingStation(long segmentId, int x, int z) {
LongOpenHashSet positions = fallingStations.get(segmentId);
return positions != null && positions.contains(RiverFootprint.pack(x, z));
}
private static int[] offsets(List<HydraulicSegment> segments) {
int[] offsets = new int[segments.size()];
int station = 0;
HydrologyPoint previous = null;
for (int index = 0; index < segments.size(); index++) {
List<HydrologyPoint> points = segments.get(index).centerline();
for (int pointIndex = 0; pointIndex < points.size(); pointIndex++) {
HydrologyPoint point = points.get(pointIndex);
if (previous != null) {
station += Math.max(Math.abs(point.x() - previous.x()), Math.abs(point.z() - previous.z()));
}
if (pointIndex == 0) {
offsets[index] = station;
}
previous = point;
}
}
return offsets;
}
private static int exposedStations(int seaLevel, List<HydraulicSegment> segments, int[] offsets,
HydrologyTerrainSampler receivingSampler) {
int mouth = segments.size() - 1;
SurfaceCenterline centerline = SurfaceCenterline.densify(segments.get(mouth).centerline());
for (int station = centerline.size() - 1; station >= 0; station--) {
if (!receivingSampler.receivingWater(centerline.x()[station], centerline.z()[station], seaLevel)) {
return offsets[mouth] + station + 1;
}
}
return offsets[mouth];
}
}
@@ -121,12 +121,12 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public int getOutputRadius() {
return hasCollisionRules() ? 0 : getRadius();
return 0;
}
@Override
public int getInputRadius() {
return hasCollisionRules() ? calculateInputRadius(getRadius(), true) : 0;
return calculateInputRadius(getRadius(), hasCollisionRules());
}
@Override
@@ -140,10 +140,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
if (!hasCollisionRules()) {
generateOrigin(writer, x, z, context);
return;
}
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
replaySourceChunks(
x,
@@ -169,12 +165,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
sourceChunkX,
sourceChunkZ
);
replaySourcePredecessors(
sourceChunkX,
sourceChunkZ,
getRadius(),
(predecessorX, predecessorZ) -> generateOrigin(scratch, predecessorX, predecessorZ, context)
);
if (hasCollisionRules()) {
replaySourcePredecessors(
sourceChunkX,
sourceChunkZ,
getRadius(),
(predecessorX, predecessorZ) -> generateOrigin(scratch, predecessorX, predecessorZ, context)
);
}
int checkpoint = scratch.mutationCheckpoint();
generateOrigin(scratch, sourceChunkX, sourceChunkZ, context);
return scratch.sourcePlanSince(checkpoint);
@@ -54,7 +54,9 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
void apply(ObjectSourcePlan plan) {
for (Mutation mutation : plan.mutations()) {
mutation.apply(this);
if (isDestination(mutation.x(), mutation.z())) {
mutation.apply(this);
}
}
}
@@ -38,7 +38,6 @@ import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.pack.value.IrisColor;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
@@ -424,15 +423,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisDimensionCarvingResolver.State state
) {
int worldY = y + getWorld().minHeight();
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
if (rootCarvingEntry != null) {
IrisDimensionCarvingEntry resolvedCarvingEntry = IrisDimensionCarvingResolver.resolveFromRoot(this, rootCarvingEntry, x, z, state);
IrisBiome resolvedCarvingBiome = IrisDimensionCarvingResolver.resolveEntryBiome(this, resolvedCarvingEntry, state);
if (resolvedCarvingBiome != null) {
return resolvedCarvingBiome;
}
}
return null;
return IrisDimensionCarvingResolver.resolveBiome(this, x, worldY, z, state);
}
private IrisBiome resolveDepthCaveBiome(
@@ -79,6 +79,8 @@ final class EngineHotloader {
previousGeneration.mantleStorageDirectory(),
previousGeneration.runtimeKernel(),
previousGeneration.transitionPlan());
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
assembly.target, assembly.runtimeKernel, assembly.transitionPlan, engine.isStudio());
engine.runtimeAssembly.set(assembly);
EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
@@ -86,6 +88,7 @@ final class EngineHotloader {
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
EngineRuntimeBuilder.enablePreparedCache(assembly, cacheIdentity, engine.isStudio());
GenerationRuntime nextGeneration = previousGeneration.withComplex(
assembly.cacheId,
assembly.complex,
@@ -57,13 +57,14 @@ final class EngineRuntimeBuilder {
this.engine = engine;
}
EngineRuntime buildRuntime() {
EngineRuntime buildRuntime(PreparedHydrologyCacheIdentity cacheIdentity) {
return buildRuntime(
engine.getTarget(),
engine.getInitialMantleStorageDirectory(),
engine.getInitialKernelVersion(),
engine.getInitialTransitionPlan(),
null);
null,
cacheIdentity);
}
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
@@ -73,7 +74,9 @@ final class EngineRuntimeBuilder {
active.mantleStorageDirectory(),
active.kernelVersion(),
active.transitionPlan(),
active.mantle());
active.mantle(),
PreparedHydrologyCacheIdentity.capture(runtimeTarget, selectRuntimeKernel(active.kernelVersion()),
active.transitionPlan(), engine.isStudio()));
}
private EngineRuntime buildRuntime(
@@ -81,7 +84,8 @@ final class EngineRuntimeBuilder {
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan,
EngineMantle transferredMantle
EngineMantle transferredMantle,
PreparedHydrologyCacheIdentity cacheIdentity
) {
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
@@ -135,6 +139,7 @@ final class EngineRuntimeBuilder {
}
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
BiomeMaxes biomeMaxes = computeBiomeMaxes();
enablePreparedCache(assembly, cacheIdentity, engine.isStudio());
return assembly.freezeRuntime(biomeMaxes);
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
@@ -167,6 +172,8 @@ final class EngineRuntimeBuilder {
mantleStorageDirectory,
selectRuntimeKernel(kernelVersion),
transitionPlan);
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
runtimeTarget, assembly.runtimeKernel, transitionPlan, engine.isStudio());
engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
@@ -184,7 +191,9 @@ final class EngineRuntimeBuilder {
assembly.mode,
assembly.dimensionStackContext
);
return assembly.freezeGeneration(computeBiomeMaxes());
BiomeMaxes biomeMaxes = computeBiomeMaxes();
enablePreparedCache(assembly, cacheIdentity, engine.isStudio());
return assembly.freezeGeneration(biomeMaxes);
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != e) {
@@ -196,6 +205,12 @@ final class EngineRuntimeBuilder {
}
}
static void enablePreparedCache(RuntimeAssembly assembly, PreparedHydrologyCacheIdentity identity, boolean studio) {
if (identity != null) {
identity.enableIfUnchanged(assembly.target, assembly.runtimeKernel, assembly.transitionPlan, assembly.complex, studio);
}
}
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(
previous,
@@ -560,7 +560,7 @@ public class IrisComplex implements DataProvider {
hydrologyNoiseCaches = List.of();
}
void enableStudioHydrologyCache(String runtimeIdentity, Path persistentRoot) {
void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) {
if (hydrologyRuntime != null) {
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
}
@@ -1934,6 +1934,7 @@ public class IrisComplex implements DataProvider {
private Engine engine;
private IrisGenerator[] generators;
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
private boolean integerCoordinates;
private boolean inUse;
private boolean isInUse() {
@@ -1945,6 +1946,7 @@ public class IrisComplex implements DataProvider {
this.engine = engine;
this.generators = generators;
this.cachedBounds = cachedBounds;
this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D<?>;
this.inUse = true;
if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear();
@@ -1967,10 +1969,12 @@ public class IrisComplex implements DataProvider {
@Override
public NoiseBounds noise(double xx, double zz) {
try {
IrisBiome bx = sampleCache.get(xx, zz);
double sampleX = integerCoordinates ? (int) xx : xx;
double sampleZ = integerCoordinates ? (int) zz : zz;
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
if (bx == null) {
bx = complex.baseBiomeStream.get(xx, zz);
sampleCache.put(xx, zz, bx);
sampleCache.put(sampleX, sampleZ, bx);
}
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds);
@@ -35,7 +35,6 @@ import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.spi.IrisLogging;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisPlatforms;
@@ -303,7 +302,8 @@ public class IrisEngine implements Engine {
getData().registerEngine(this);
_t0 = M.ms();
long phaseStartedAt = System.nanoTime();
String studioCacheIdentity = currentStudioCacheIdentity();
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
getTarget(), EngineRuntimeBuilder.selectRuntimeKernel(initialKernelVersion), initialTransitionPlan, studio);
getData().loadPrefetch(this);
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
diagnostics.logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
@@ -315,8 +315,7 @@ public class IrisEngine implements Engine {
IrisLogging.notice("Engine init: " + requiredTarget.getWorld().name() + "/" + requiredTarget.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed());
_t0 = M.ms();
phaseStartedAt = System.nanoTime();
EngineRuntime initialRuntime = runtimeBuilder.buildRuntime();
enableStableStudioCache(initialRuntime, studioCacheIdentity);
EngineRuntime initialRuntime = runtimeBuilder.buildRuntime(cacheIdentity);
runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
diagnostics.logPackCompatSummary();
@@ -359,47 +358,6 @@ public class IrisEngine implements Engine {
}
}
private String currentStudioCacheIdentity() {
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
return "";
}
try {
String contentFingerprint = PackValidationCache.contentFingerprint(
IrisPlatforms.get().packsFolderNoCreate());
String contextFingerprint = PackValidationCache.contextFingerprint();
if (contentFingerprint.isBlank() || contextFingerprint.isBlank()) {
return "";
}
return contentFingerprint + contextFingerprint;
} catch (RuntimeException failure) {
IrisLogging.warn("Studio runtime cache fingerprint failed: " + failure.getMessage());
return "";
}
}
private void enableStableStudioCache(EngineRuntime runtime, String initialIdentity) {
if (initialIdentity.isBlank()) {
return;
}
String finalIdentity = currentStudioCacheIdentity();
if (!initialIdentity.equals(finalIdentity)) {
IrisLogging.warn("Studio packs changed during runtime compilation; shared generation caches are disabled.");
return;
}
runtime.generation().complex().enableStudioHydrologyCache(initialIdentity, studioHydrologyCacheRoot());
IrisLogging.debug("Enabled shared Studio hydrology cache identity="
+ initialIdentity.substring(0, Math.min(12, initialIdentity.length())));
}
private Path studioHydrologyCacheRoot() {
Path packsRoot = IrisPlatforms.get().packsFolderNoCreate().toPath().toAbsolutePath().normalize();
Path packRoot = packsRoot.resolve(getDimension().getLoadKey()).normalize();
if (!packRoot.startsWith(packsRoot)) {
throw new IllegalStateException("Studio dimension key resolves outside the Iris packs folder.");
}
return packRoot.resolve(".iris").resolve("studio-hydrology");
}
public void startEntryHydrology(int blockX, int blockZ) {
GenerationRuntimeBinding binding = getActiveGenerationRuntimeBinding();
if (!backgroundTasks.scheduleTrackedTask(() -> prepareEntryHydrology(binding, blockX, blockZ))) {
@@ -0,0 +1,118 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.PackValidationCache;
import art.arcane.iris.pack.PackDirectoryResolver;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.world.history.GenerationKernelRegistry;
import art.arcane.iris.world.history.GenerationPackFingerprint;
import art.arcane.iris.world.history.TransitionGenerationPlan;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
record PreparedHydrologyCacheIdentity(String fingerprint) {
static PreparedHydrologyCacheIdentity capture(
EngineTarget target,
GenerationKernelRegistry.RuntimeKernel kernel,
TransitionGenerationPlan transition,
boolean studio
) {
if (!IrisPlatforms.isBound()) {
return null;
}
try {
String context = PackValidationCache.contextFingerprint();
if (context.isBlank()) {
return null;
}
String content = GenerationPackFingerprint.compute(
target.getData().getDataFolder().toPath(), GenerationPackFingerprint.CURRENT_VERSION);
String dependencies = target.getData().hasGenerationRegistryContract() ? "closed"
: dependencyFingerprint(IrisPlatforms.get().packsFolderNoCreate());
if (dependencies.isBlank()) {
return null;
}
int buffetSize = studio && "bukkit".equals(IrisPlatforms.get().platformName())
? target.getDimension().getStudioMode().biomeSizeChunks() : 0;
return fromInputs(content, dependencies, context, kernel, transition == null ? null : transition.specification(),
target.getWorld().minHeight(), target.getWorld().maxHeight(), buffetSize);
} catch (IOException | RuntimeException failure) {
IrisLogging.reportError("Prepared hydrology cache identity could not be captured.", failure);
return null;
}
}
static PreparedHydrologyCacheIdentity fromInputs(
String packContent,
String dependencies,
String platformContext,
GenerationKernelRegistry.RuntimeKernel kernel,
TransitionGenerationPlan.Specification transition,
int minimumHeight,
int maximumHeight,
int biomeBuffetSize
) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
update(digest, packContent);
update(digest, dependencies);
update(digest, platformContext);
update(digest, kernel.version().toString());
update(digest, kernel.implementationFingerprint());
update(digest, Integer.toString(minimumHeight));
update(digest, Integer.toString(maximumHeight));
update(digest, Integer.toString(biomeBuffetSize));
update(digest, Boolean.toString(transition != null));
if (transition != null) {
update(digest, Long.toString(transition.activationId()));
update(digest, transition.oldEpochId());
update(digest, transition.newEpochId());
update(digest, Integer.toString(transition.algorithmVersion()));
update(digest, Integer.toString(transition.widthBlocks()));
update(digest, transition.boundaryIdentity());
update(digest, transition.terrainSignatureIdentity());
}
return new PreparedHydrologyCacheIdentity(HexFormat.of().formatHex(digest.digest()));
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException("SHA-256 is unavailable.", failure);
}
}
void enableIfUnchanged(EngineTarget target, GenerationKernelRegistry.RuntimeKernel kernel,
TransitionGenerationPlan transition, IrisComplex complex, boolean studio) {
if (!equals(capture(target, kernel, transition, studio))) {
IrisLogging.warn("Generation inputs changed during compilation; prepared hydrology caching is disabled.");
return;
}
complex.enablePreparedHydrologyCache(fingerprint,
IrisPlatforms.get().dataFolderNoCreate("cache", "hydrology-plans").toPath());
}
private static void update(MessageDigest digest, String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
digest.update((byte) (bytes.length >>> 24));
digest.update((byte) (bytes.length >>> 16));
digest.update((byte) (bytes.length >>> 8));
digest.update((byte) bytes.length);
digest.update(bytes);
}
private static String dependencyFingerprint(File packsRoot) throws IOException {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (File pack : PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(packsRoot)) {
update(digest, pack.getName());
update(digest, GenerationPackFingerprint.compute(
pack.toPath().toRealPath(), GenerationPackFingerprint.CURRENT_VERSION));
}
return HexFormat.of().formatHex(digest.digest());
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException("SHA-256 is unavailable.", failure);
}
}
}
@@ -151,7 +151,8 @@ public final class HydrologyBankTerrainRuntime {
return new BankColumn(flat, flat);
}
if (base >= natural.topY()) {
return new BankColumn(natural, natural);
Terrain3DColumn filled = Terrain3DColumn.unshaped(base, options.height());
return new BankColumn(filled, filled);
}
double rise = Math.min(amplitude, natural.topY() - base);
IntArrayList boundaries = new IntArrayList(natural.spanCount() * 2 + 8);
@@ -28,6 +28,17 @@ public final class IrisDimensionCarvingResolver {
}
public static IrisBiome resolveBiome(Engine engine, int worldX, int worldY, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state;
IrisData data = resolvedState.bind(engine);
IrisDimensionCarvingEntry root = resolveBoundRootEntry(engine, worldY, data, resolvedState);
if (root == null) {
return null;
}
IrisDimensionCarvingEntry resolved = resolveBoundFromRoot(engine, root, worldX, worldZ, data, resolvedState);
return resolved == null ? null : resolveBoundEntryBiome(data, resolved, resolvedState);
}
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) {
return resolveRootEntry(engine, worldY, threadState());
}
@@ -35,6 +46,36 @@ public final class IrisDimensionCarvingResolver {
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
State resolvedState = state == null ? threadState() : state;
IrisData data = resolvedState.bind(engine);
return resolveBoundRootEntry(engine, worldY, data, resolvedState);
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState());
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state;
IrisData data = resolvedState.bind(engine);
return resolveBoundFromRoot(engine, rootEntry, worldX, worldZ, data, resolvedState);
}
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) {
return resolveEntryBiome(engine, entry, null);
}
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry, State state) {
if (entry == null) {
return null;
}
if (state == null) {
return entry.getRealBiome(engine.getData());
}
return resolveBoundEntryBiome(state.bind(engine), entry, state);
}
private static IrisDimensionCarvingEntry resolveBoundRootEntry(Engine engine, int worldY, IrisData data, State resolvedState) {
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
return resolvedState.rootEntriesByWorldY.get(worldY);
}
@@ -59,13 +100,7 @@ public final class IrisDimensionCarvingResolver {
return resolved;
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
return resolveFromRoot(engine, rootEntry, worldX, worldZ, threadState());
}
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state;
IrisData data = resolvedState.bind(engine);
private static IrisDimensionCarvingEntry resolveBoundFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, IrisData data, State resolvedState) {
if (rootEntry == null) {
return null;
}
@@ -100,22 +135,6 @@ public final class IrisDimensionCarvingResolver {
return current;
}
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry) {
return resolveEntryBiome(engine, entry, null);
}
public static IrisBiome resolveEntryBiome(Engine engine, IrisDimensionCarvingEntry entry, State state) {
if (entry == null) {
return null;
}
if (state == null) {
return entry.getRealBiome(engine.getData());
}
return resolveBoundEntryBiome(state.bind(engine), entry, state);
}
private static IrisBiome resolveBoundEntryBiome(IrisData data, IrisDimensionCarvingEntry entry, State state) {
if (state.biomeCache.containsKey(entry)) {
return state.biomeCache.get(entry);
@@ -92,9 +92,10 @@ public final class Terrain3DRuntime {
int gridX = Math.floorDiv(x, STEP) * STEP;
int gridZ = Math.floorDiv(z, STEP) * STEP;
Anchor northWest = anchor(gridX, gridZ);
Anchor northEast = anchor(gridX + STEP, gridZ);
Anchor southWest = anchor(gridX, gridZ + STEP);
Anchor southEast = anchor(gridX + STEP, gridZ + STEP);
Anchor northEast = x == gridX ? northWest : anchor(gridX + STEP, gridZ);
Anchor southWest = z == gridZ ? northWest : anchor(gridX, gridZ + STEP);
Anchor southEast = x == gridX ? southWest
: z == gridZ ? northEast : anchor(gridX + STEP, gridZ + STEP);
double dx = (x - gridX) / (double) STEP;
double dz = (z - gridZ) / (double) STEP;
double amplitude = interpolate(northWest.amplitude(), northEast.amplitude(),
@@ -49,14 +49,25 @@ public final class PackValidationRegistry {
if (packRoot == null || result == null) {
return;
}
publish(normalize(packRoot), new RootValidation(result, ""));
publish(normalize(packRoot), new RootValidation(result, "", ""));
}
public static void publish(Path packRoot, PackValidationResult result, String contentFingerprint) {
public static void publish(
Path packRoot,
PackValidationResult result,
String contentFingerprint,
String validatedContextFingerprint
) {
if (packRoot == null || result == null || contentFingerprint == null || contentFingerprint.isBlank()) {
return;
}
publish(normalize(packRoot), new RootValidation(result, contentFingerprint));
if (validatedContextFingerprint == null
|| !validatedContextFingerprint.equals(PackValidationCache.contextFingerprint())) {
publish(packRoot, result);
return;
}
publish(normalize(packRoot), new RootValidation(
result, contentFingerprint, validatedContextFingerprint));
}
public static PackValidationResult publishMatchingCopy(
@@ -118,7 +129,7 @@ public final class PackValidationRegistry {
return new RootState(
current.generation(),
false,
new RootValidation(result, ""));
new RootValidation(result, "", ""));
});
return published.get();
}
@@ -140,6 +151,14 @@ public final class PackValidationRegistry {
: state.validation().result();
}
public static PackValidationResult getMatching(Path packRoot, String contentFingerprint) {
if (packRoot == null || contentFingerprint == null || contentFingerprint.isBlank()) {
return null;
}
RootValidation validation = matchingValidation(packRoot, contentFingerprint);
return validation == null ? null : validation.result();
}
public static PackValidationResult requireLoadable(String packName) {
if (packName == null || packName.isBlank()) {
throw new IllegalArgumentException("Pack name is required for validation");
@@ -250,10 +269,11 @@ public final class PackValidationRegistry {
if (sourceState == null
|| sourceState.mutating()
|| sourceState.validation() == null
|| !copiedContentFingerprint.equals(sourceState.validation().contentFingerprint())) {
|| !copiedContentFingerprint.equals(sourceState.validation().contentFingerprint())
|| !sourceState.validation().contextFingerprint().equals(PackValidationCache.contextFingerprint())) {
return null;
}
return sourceState.validation();
return ROOT_STATES.get(normalizedSource) == sourceState ? sourceState.validation() : null;
}
private static long nextGeneration(RootState current) {
@@ -297,6 +317,7 @@ public final class PackValidationRegistry {
requireOpen();
pendingValidation = new RootValidation(
Objects.requireNonNull(result, "Pack validation result"),
"",
"");
}
@@ -345,7 +366,11 @@ public final class PackValidationRegistry {
}
}
private record RootValidation(PackValidationResult result, String contentFingerprint) {
private record RootValidation(
PackValidationResult result,
String contentFingerprint,
String contextFingerprint
) {
}
private record RootState(long generation, boolean mutating, RootValidation validation) {
@@ -300,6 +300,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
}
public boolean hasGenerationRegistryContract() {
return generationRegistryContract != null;
}
public synchronized void bindGenerationRegistryContract(GenerationRegistryContract contract) {
GenerationRegistryContract required = Objects.requireNonNull(contract, "contract");
if (generationRegistryContract != null && !generationRegistryContract.equals(required)) {
@@ -716,6 +716,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
} catch (IOException failure) {
throw new IllegalStateException("Unable to load the active Iris generation transition.", failure);
}
engineTarget.getData().bindGenerationRegistryContract(epoch.registryContract());
IrisEngine createdEngine = new IrisEngine(
engineTarget,
mode,
@@ -1319,7 +1320,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(x << 4, z << 4, blocks, biomes, engine.shouldGenerateMulticore());
engine.generate(x << 4, z << 4, blocks, biomes,
initialEntryPending || studioEntryBootstrapActive.get() || engine.shouldGenerateMulticore());
blocks.apply();
if (historyScope != null) {
historyScope.claimGeneratedSemantics();
@@ -362,7 +362,7 @@ public class StudioSVC implements IrisService {
packRoot,
GenerationPackFingerprint.CURRENT_VERSION
);
validatePublishedPack(packRoot);
validateGenerationCandidate(packRoot, fingerprint);
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
@@ -566,6 +566,17 @@ public class StudioSVC implements IrisService {
}
}
static PackValidationResult validateGenerationCandidate(Path packRoot, String contentFingerprint) {
PackValidationResult validation = PackValidationRegistry.getMatching(packRoot, contentFingerprint);
if (validation == null) {
return validatePublishedPack(packRoot);
}
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
}
return validation;
}
static PackValidationResult validatePublishedPack(
Path packRoot,
Path validatedSource,
@@ -29,6 +29,7 @@ import art.arcane.iris.world.IrisToolbelt;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -69,6 +70,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final long MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
private static final long ADMISSION_WAIT_BOUND_MS = 500L;
private static final int HYDROLOGY_PREFETCH_HALO_BLOCKS = 512;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -117,6 +119,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
private volatile Engine metricsEngine;
private volatile Mantle cachedMantle;
private volatile HydrologyTileCache.PregenerationScope hydrologyPrefetchScope;
private final PregenMantleBackpressure backpressure;
public AsyncPregenMethod(World world, int unusedThreads) {
@@ -298,7 +301,18 @@ public class AsyncPregenMethod implements PregeneratorMethod {
if (engine == null || engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
return;
}
engine.getComplex().getHydrologyRuntime().preparePregeneration(centerBlockX, centerBlockZ);
HydrologyTileCache.PregenerationArea area = new HydrologyTileCache.PregenerationArea(
centerBlockX, centerBlockZ,
(long) boundsMinRegionX * 512L - HYDROLOGY_PREFETCH_HALO_BLOCKS,
(long) boundsMinRegionZ * 512L - HYDROLOGY_PREFETCH_HALO_BLOCKS,
((long) boundsMaxRegionX + 1L) * 512L - 1L + HYDROLOGY_PREFETCH_HALO_BLOCKS,
((long) boundsMaxRegionZ + 1L) * 512L - 1L + HYDROLOGY_PREFETCH_HALO_BLOCKS);
HydrologyTileCache.PregenerationScope scope = engine.getComplex().getHydrologyRuntime().preparePregeneration(area);
hydrologyPrefetchScope = scope;
if (closing.get()) {
scope.close();
hydrologyPrefetchScope = null;
}
}
private boolean inBounds(int rx, int rz) {
@@ -827,6 +841,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
releaseWorkerThreadBoost();
}
} finally {
HydrologyTileCache.PregenerationScope scope = hydrologyPrefetchScope;
if (scope != null) {
scope.close();
hydrologyPrefetchScope = null;
}
if (interrupted) {
Thread.currentThread().interrupt();
}
@@ -196,9 +196,15 @@ public class PregenTask {
public void iterateRegions(Spiraled s) {
Bound bound = bounds.region();
new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> {
if (bound.check(x, z)) s.on(x, z);
})).setOffset(PowerOfTwoCoordinates.blockToRegionFloor(center.getX()), PowerOfTwoCoordinates.blockToRegionFloor(center.getZ())).drain();
int centerRegionX = PowerOfTwoCoordinates.blockToRegionFloor(center.getX());
int centerRegionZ = PowerOfTwoCoordinates.blockToRegionFloor(center.getZ());
int width = Math.max(centerRegionX - bound.minX(), bound.maxX() - centerRegionX) * 2 + 1;
int depth = Math.max(centerRegionZ - bound.minZ(), bound.maxZ() - centerRegionZ) * 2 + 1;
new Spiraler(width, depth, ((x, z) -> {
if (bound.check(x, z)) {
s.on(x, z);
}
})).setOffset(centerRegionX, centerRegionZ).drain();
}
public void iterateChunks(int rX, int rZ, Spiraled s) {
@@ -291,10 +297,10 @@ public class PregenTask {
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.CHUNK_BITS)
);
region = new Bound(
PowerOfTwoCoordinates.blockToRegionFloor(minX),
PowerOfTwoCoordinates.blockToRegionFloor(minZ),
PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.REGION_BITS),
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.REGION_BITS)
PowerOfTwoCoordinates.chunkToRegion(chunk.minX()),
PowerOfTwoCoordinates.chunkToRegion(chunk.minZ()),
PowerOfTwoCoordinates.chunkToRegion(chunk.maxX()),
PowerOfTwoCoordinates.chunkToRegion(chunk.maxZ())
);
requireSaneSpan(region);
}