mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Tunabelly
This commit is contained in:
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,
|
||||
|
||||
+5
-1
@@ -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()) {
|
||||
|
||||
+18
-1
@@ -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,
|
||||
|
||||
+280
@@ -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];
|
||||
}
|
||||
}
|
||||
+3
@@ -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();
|
||||
}
|
||||
|
||||
+3
-3
@@ -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(
|
||||
|
||||
+47
-2
@@ -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) {
|
||||
|
||||
-125
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -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)),
|
||||
|
||||
+33
-40
@@ -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) {
|
||||
}
|
||||
}
|
||||
-514
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-559
@@ -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);
|
||||
}
|
||||
|
||||
+86
-93
@@ -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"
|
||||
|
||||
+3
-503
@@ -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
-4
@@ -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) {
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+2
-2
@@ -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.")
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+14
-9
@@ -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) {
|
||||
}
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+96
-42
@@ -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) {
|
||||
}
|
||||
}
|
||||
+169
@@ -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) {
|
||||
}
|
||||
}
|
||||
+44
-26
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+7
-28
@@ -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;
|
||||
|
||||
+104
@@ -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);
|
||||
|
||||
+3
-1
@@ -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))) {
|
||||
|
||||
+118
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+42
-23
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user