mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
Tunabelly
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ public class HydrologyCacheBudgetTest {
|
||||
HydrologyCacheBudget budget = HydrologyCacheBudget.forHeap(heap, regionalEnabled);
|
||||
long total = budget.tileBytes() + budget.columnBytes() + budget.diagnosticBytes()
|
||||
+ budget.routingBytes() + budget.ownerBytes() + budget.edgeBytes() + budget.regionalDraftBytes()
|
||||
+ budget.regionalCoarseBytes() + budget.regionalTerrainBytes() + budget.regionalReachBytes();
|
||||
+ budget.regionalCoarseBytes() + budget.regionalTerrainBytes();
|
||||
assertTrue(total <= Math.clamp(heap / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE));
|
||||
assertTrue(total >= 8L * MEBIBYTE);
|
||||
assertEquals(budget.tileBytes() / 6L, budget.columnBytes());
|
||||
@@ -40,7 +40,7 @@ public class HydrologyCacheBudgetTest {
|
||||
assertEquals(24L * MEBIBYTE, localOnly.routingBytes());
|
||||
assertEquals(8L * MEBIBYTE, localOnly.edgeBytes());
|
||||
assertEquals(0L, localOnly.regionalDraftBytes() + localOnly.regionalCoarseBytes()
|
||||
+ localOnly.regionalTerrainBytes() + localOnly.regionalReachBytes());
|
||||
+ localOnly.regionalTerrainBytes());
|
||||
assertThrows(IllegalArgumentException.class, () -> HydrologyCacheBudget.forHeap(0L, false));
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public class HydrologyCacheBudgetTest {
|
||||
HydrologyRegionalGraph.Label second = new HydrologyRegionalGraph.Label(
|
||||
new HydrologyRegionalGraph.LabelState(3, 2, 63, 1D, 1D, 0), downstream);
|
||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(
|
||||
new HydrologyRegionalGraph.Label[]{first, second, null}, new int[4], List.of());
|
||||
new HydrologyRegionalGraph.Label[]{first, second, null}, new int[4], List.of(), new int[4]);
|
||||
assertEquals(3, tree.retainedLabelCount());
|
||||
}
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyCoastalFallAdmissionTest {
|
||||
private static final HydrologyPlannerSettings SETTINGS = HydrologyPlannerSettings.defaults();
|
||||
private static final HydrologyTerrainSampler TERRAIN = (x, z) -> x >= 80
|
||||
? HydrologyTerrainSample.ocean(54, "ocean") : HydrologyTerrainSample.openLand(95, 0D, "land");
|
||||
|
||||
@Test
|
||||
public void aConnectedMouthAdmitsTheCoastalFallAtTheNaturalSeaSurface() {
|
||||
RiverCourse course = course(HydrologyFeatureType.MOUTH, 80);
|
||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
assertTrue(compiler.surfaceFootprint(course).toString(), compiler.surfaceFootprint(course).accepted());
|
||||
HydrologyCaveCourseFilter.Result result = filter().filter(List.of(), List.of(), List.of(), List.of(course),
|
||||
compiler.compileValidation(List.of(course)), diagnostics);
|
||||
|
||||
assertEquals(diagnostics.toString(), List.of(course), result.courses());
|
||||
assertEquals(1, result.cavePlans().size());
|
||||
assertTrue(result.cavePlans().getFirst().accepted());
|
||||
assertTrue(diagnostics.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anInlandTerminalOrDisconnectedMouthCannotOpenTheFallAtSeaLevel() {
|
||||
for (RiverCourse course : List.of(course(HydrologyFeatureType.SURFACE_POOL, 80),
|
||||
course(HydrologyFeatureType.MOUTH, 81))) {
|
||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyCaveCourseFilter.Result result = filter().filter(List.of(), List.of(), List.of(), List.of(course),
|
||||
compiler.compileValidation(List.of(course)), diagnostics);
|
||||
|
||||
assertTrue(result.courses().isEmpty());
|
||||
assertFalse(diagnostics.isEmpty());
|
||||
assertEquals(HydrologyCandidateRejection.CAVE_CONTAINMENT, diagnostics.getFirst().rejection());
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyCaveCourseFilter filter() {
|
||||
return new HydrologyCaveCourseFilter(new HydrologyTerrainCaveVoxelView(TERRAIN, SETTINGS.seaLevel(), -64, 256),
|
||||
new HydrologyCaveCourseFilter.Options(false, 8192, 8192));
|
||||
}
|
||||
|
||||
private static RiverCourse course(HydrologyFeatureType terminal, int receiverX) {
|
||||
int sea = SETTINGS.seaLevel();
|
||||
HydraulicSegment approach = new HydraulicSegment(1L, 4L, HydrologyFeatureType.SURFACE_POOL,
|
||||
93, 93, 4, 2, false, false, List.of(new HydrologyPoint(0, 93, 0), new HydrologyPoint(79, 93, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 2));
|
||||
HydraulicSegment fall = new HydraulicSegment(2L, 4L, HydrologyFeatureType.WATERFALL,
|
||||
93, sea, 4, 2, true, true, List.of(new HydrologyPoint(79, 93, 0), new HydrologyPoint(80, sea, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 2));
|
||||
HydraulicSegment receiving = new HydraulicSegment(3L, 4L, terminal, sea, sea, 4, 2, false, false,
|
||||
List.of(new HydrologyPoint(receiverX, sea, 0)), HydraulicChannelProfile.uniform(4, 2));
|
||||
return new RiverCourse(4L, RiverCourseType.SURFACE, OptionalLong.of(5L), OptionalLong.of(6L),
|
||||
"default", 1, List.of(), List.of(approach, fall, receiving));
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
public class HydrologyDrainageGeometryTest {
|
||||
@Test
|
||||
public void sourceBranchesShareExactlyTheSameDescendingTrunk() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(120 - x / 8);
|
||||
Fixture fixture = fixture(terrain);
|
||||
HydrologyDrainageGeometry geometry = fixture.geometry();
|
||||
List<HydrologyPoint> first = geometry.edge(0);
|
||||
List<HydrologyPoint> tributary = geometry.edge(3);
|
||||
List<HydrologyPoint> trunk = geometry.edge(1);
|
||||
assertFalse(first.isEmpty());
|
||||
assertFalse(tributary.isEmpty());
|
||||
assertFalse(trunk.isEmpty());
|
||||
assertEquals(first.getLast(), trunk.getFirst());
|
||||
assertEquals(tributary.getLast(), trunk.getFirst());
|
||||
for (List<HydrologyPoint> edge : List.of(first, tributary, trunk)) {
|
||||
for (int index = 1; index < edge.size(); index++) {
|
||||
assertTrue(edge.get(index - 1).y() >= edge.get(index).y());
|
||||
}
|
||||
}
|
||||
assertEquals(geometry.head(1), trunk.getFirst().y());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedAndConcurrentSourcesReuseOneGeometryComputation() {
|
||||
AtomicInteger sampled = new AtomicInteger();
|
||||
Fixture fixture = fixture((x, z) -> {
|
||||
sampled.incrementAndGet();
|
||||
return land(120 - x / 8);
|
||||
});
|
||||
ArrayList<CompletableFuture<List<HydrologyPoint>>> tasks = new ArrayList<>();
|
||||
for (int index = 0; index < 16; index++) {
|
||||
tasks.add(CompletableFuture.supplyAsync(() -> fixture.geometry().edge(0)));
|
||||
}
|
||||
List<HydrologyPoint> expected = tasks.getFirst().join();
|
||||
assertFalse(expected.isEmpty());
|
||||
for (CompletableFuture<List<HydrologyPoint>> task : tasks) {
|
||||
assertSame(expected, task.join());
|
||||
}
|
||||
int samples = sampled.get();
|
||||
assertSame(expected, fixture.geometry().edge(0));
|
||||
assertEquals(samples, sampled.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aFailedRequiredSampleCanBeRetriedWithoutCachingItsFailure() {
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
IllegalStateException unavailable = new IllegalStateException("Terrain unavailable");
|
||||
Fixture fixture = fixture((x, z) -> {
|
||||
if (x == 31 && attempts.incrementAndGet() == 1) {
|
||||
throw unavailable;
|
||||
}
|
||||
return land(120 - x / 8);
|
||||
});
|
||||
assertSame(unavailable, assertThrows(IllegalStateException.class, () -> fixture.geometry().edge(0)));
|
||||
assertFalse(fixture.geometry().edge(0).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detailedReliefDoesNotSendTheSharedRiverSearchingForAnotherRoute() {
|
||||
Fixture flat = fixture((x, z) -> land(120 - x / 8));
|
||||
Fixture broken = fixture((x, z) -> land(x > 24 && x < 40 ? 90 : 120 - x / 8));
|
||||
assertEquals(flat.geometry().edge(0), broken.geometry().edge(0));
|
||||
assertFalse(broken.geometry().edge(0).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aThinExcludedPolicyBandStillBlocksAnEdge() {
|
||||
HydrologyTerrainSample blocked = spy(land(116));
|
||||
doReturn(false).when(blocked).transitAllowed();
|
||||
Fixture fixture = fixture((x, z) -> x == 31 ? blocked : land(120 - x / 8));
|
||||
assertTrue(fixture.geometry().edge(0).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coastalRootKeepsItsLandwardPointAndDescendingHeadAboveTheSea() {
|
||||
Fixture fixture = fixture((x, z) -> land(120 - x / 8));
|
||||
HydrologyPoint root = fixture.geometry().anchor(2);
|
||||
assertEquals(128, root.x());
|
||||
assertEquals(0, root.z());
|
||||
assertTrue(root.y() >= fixture.planner().settings.seaLevel());
|
||||
assertEquals(root, fixture.geometry().edge(1).getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyOutletTrialReusesTheSameGridCostsAndPolicyEdges() {
|
||||
Fixture fixture = fixture((x, z) -> land(120 - x / 8));
|
||||
HydrologySourcePlanner sources = fixture.planner().sourcePlanner;
|
||||
HydrologyDrainageGraph graph = fixture.grid().drainage(sources);
|
||||
sources.buildRouting(fixture.grid(), List.of(fixture.outlet()), true);
|
||||
sources.buildRouting(fixture.grid(), List.of(fixture.outlet()), false);
|
||||
assertSame(graph, fixture.grid().drainage(sources));
|
||||
for (HydrologyGridNode downstream : fixture.grid().nodes()) {
|
||||
for (int direction = 0; direction < HydrologySourcePlanner.ROUTING_OFFSETS.size(); direction++) {
|
||||
HydrologyGridOffset offset = HydrologySourcePlanner.ROUTING_OFFSETS.get(direction);
|
||||
HydrologyGridNode upstream = fixture.grid().nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
|
||||
if (upstream == null) {
|
||||
assertEquals(-1, graph.upstream(downstream.index(), direction, false));
|
||||
} else {
|
||||
assertEquals(upstream.index(), graph.upstream(downstream.index(), direction, false));
|
||||
assertEquals(sources.routeCost(upstream, downstream, offset,
|
||||
sources.minimumNeighborHeight(fixture.grid(), downstream)), graph.cost(downstream.index(), direction), 0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCuttableRidgeDoesNotLiftTheSharedUpstreamRiverAboveItsSpring() {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Surface surface = base.surface();
|
||||
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(), base.routing(),
|
||||
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(),
|
||||
surface.maximumWidth(), surface.minimumDepth(), surface.maximumDepth(), 64,
|
||||
surface.shoreWidth(), surface.banks()), base.hydraulics(), base.underground(),
|
||||
base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
|
||||
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(x == 64 ? 154 : 120 - x / 8);
|
||||
Fixture fixture = fixture(terrain, settings, terrain);
|
||||
HydrologyDrainageGeometry geometry = fixture.geometry();
|
||||
int depth = (surface.minimumDepth() + surface.maximumDepth()) / 2;
|
||||
|
||||
assertEquals(120 - surface.banks().sink() - depth, geometry.head(0));
|
||||
assertTrue(geometry.head(0) >= geometry.head(1));
|
||||
assertTrue(geometry.head(1) >= geometry.head(2));
|
||||
assertTrue(154 - geometry.head(1) + depth <= 64);
|
||||
assertEquals(geometry.edge(0).getLast(), geometry.edge(1).getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSourceExcludedDepressionDoesNotLowerTheSharedOutletHead() {
|
||||
HydrologyTerrainSample excluded = spy(land(90));
|
||||
doReturn(false).when(excluded).surfaceSourceAllowed();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> z == 0 ? land(120 - x / 8) : excluded;
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
Fixture fixture = fixture(terrain, settings, terrain);
|
||||
HydrologyPlannerSettings.Surface surface = settings.surface();
|
||||
int depth = (surface.minimumDepth() + surface.maximumDepth()) / 2;
|
||||
|
||||
assertEquals(104 - surface.banks().sink() - depth, fixture.geometry().head(2));
|
||||
assertTrue(fixture.geometry().head(0) >= fixture.geometry().head(1));
|
||||
assertTrue(fixture.geometry().head(1) >= fixture.geometry().head(2));
|
||||
assertEquals(fixture.geometry().edge(0).getLast(), fixture.geometry().edge(1).getFirst());
|
||||
}
|
||||
|
||||
private Fixture fixture(HydrologyTerrainSampler terrain) {
|
||||
return fixture(terrain, HydrologyPlannerSettings.defaults(), (x, z) -> land(120 - x / 8));
|
||||
}
|
||||
|
||||
private Fixture fixture(HydrologyTerrainSampler terrain, HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler coarseTerrain) {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, settings, terrain);
|
||||
ArrayList<HydrologyGridNode> nodes = new ArrayList<>();
|
||||
for (int index = 0; index < 9; index++) {
|
||||
int x = index % 3 * 64;
|
||||
int z = index / 3 * 64;
|
||||
nodes.add(new HydrologyGridNode(index, index % 3, index / 3, x, z, index + 1L, coarseTerrain.sample(x, z)));
|
||||
}
|
||||
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 192, 3, 64, nodes);
|
||||
OutletCandidate outlet = new OutletCandidate(2, -1, new RiverOutlet(10L, HydrologyFeatureType.MOUTH,
|
||||
3L, new HydrologyPoint(128, 104, 0), new HydrologyPoint(129, 63, 0), 63, true));
|
||||
int[] parent = {1, 2, -1, 1, 1, 2, 3, 4, 5};
|
||||
HydrologyDrainageGeometry geometry = new HydrologyDrainageGeometry(planner,
|
||||
new HydrologyDrainageGeometry.Basin(grid, parent, new int[9], List.of(outlet)));
|
||||
return new Fixture(planner, grid, outlet, geometry);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(int height) {
|
||||
return HydrologyTerrainSample.openLand(height, 0D, "land");
|
||||
}
|
||||
|
||||
private record Fixture(HydrologyPlanner planner, HydrologySampledGrid grid,
|
||||
OutletCandidate outlet, HydrologyDrainageGeometry geometry) {
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -154,7 +154,7 @@ public class HydrologyFallbackDiagnosticsTest {
|
||||
}
|
||||
|
||||
private static HydrologyRoutingPlan routing(List<OutletCandidate> outlets) {
|
||||
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true);
|
||||
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true, null);
|
||||
}
|
||||
|
||||
private static OutletCandidate outlet(int index) {
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ public class HydrologyFallbackParallelismTest {
|
||||
}
|
||||
|
||||
private static HydrologyRoutingPlan routing(List<OutletCandidate> outlets) {
|
||||
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true);
|
||||
return new HydrologyRoutingPlan(new double[]{0D}, new int[]{-1}, new int[]{0}, new int[]{0}, outlets, true, null);
|
||||
}
|
||||
|
||||
private static OutletCandidate outlet(int index) {
|
||||
|
||||
+13
-12
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
@@ -1508,7 +1509,7 @@ public class HydrologyFootprintCompilerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lowBanksKeepTheirNaturalHeightAndReportUncontainedWater() {
|
||||
public void lowBanksFillWithinTheirPolicyAndReportRemainingUncontainedWater() {
|
||||
HydraulicSegment channel = new HydraulicSegment(
|
||||
529L,
|
||||
528L,
|
||||
@@ -1539,25 +1540,25 @@ public class HydrologyFootprintCompilerTest {
|
||||
HydrologyFootprintCompiler lowBankCompiler = compiler(lowBankTerrain);
|
||||
RiverFootprint lowBankFootprint = lowBankCompiler.compile(List.of(course));
|
||||
HydrologyColumnSample lowBank = lowBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
|
||||
x == 12 && z == lowBankZ ? 68 : 82,
|
||||
0D,
|
||||
"parent"
|
||||
);
|
||||
SurfaceRiverPolicy oneBlockFill = new SurfaceRiverPolicy("restricted-bank",
|
||||
null, null, null, null, null, null, 1);
|
||||
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> x == 12 && z == lowBankZ
|
||||
? HydrologyTerrainSample.openLand(68, 0D, "parent").withSurfacePolicy(oneBlockFill)
|
||||
: HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||
RiverFootprint deepBankFootprint = compiler(deepBankTerrain).compile(List.of(course));
|
||||
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||
|
||||
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
|
||||
// A low bank remains below the water and is reported without raising terrain.
|
||||
assertEquals(69, lowBank.naturalHeight());
|
||||
assertEquals(69, lowBank.terrainHeight());
|
||||
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
|
||||
assertEquals(70, lowBank.terrainHeight());
|
||||
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
|
||||
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
|
||||
assertTrue(sample.terrainHeight() <= sample.naturalHeight());
|
||||
HydrologyColumnLayer layer = sample.primarySurfaceLayerOrNull();
|
||||
int fillLimit = layer != null && layer.channel() ? 0 : 8;
|
||||
assertTrue(sample.terrainHeight() - sample.naturalHeight() <= fillLimit);
|
||||
}
|
||||
// Two blocks short: the bank keeps its natural height and the edge is reported.
|
||||
assertEquals(68, deepBank.naturalHeight());
|
||||
assertEquals(68, deepBank.terrainHeight());
|
||||
assertEquals(69, deepBank.terrainHeight());
|
||||
assertTrue(unsupportedBankCells(deepBankFootprint, 528L) > 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -394,6 +394,30 @@ public class HydrologyModelTest {
|
||||
assertEquals(72, sample.terrainHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overlappingRaisedBanksRetainOwnershipWithoutAccumulatingFill() {
|
||||
for (boolean sameFeature : List.of(true, false)) {
|
||||
for (boolean reverse : List.of(true, false)) {
|
||||
FootprintMutableColumn column = new FootprintMutableColumn(0, 0,
|
||||
HydrologyTerrainSample.openLand(77, 0D, "parent"), 63);
|
||||
int[] beds = reverse ? new int[]{81, 80, 77, 75} : new int[]{75, 77, 80, 81};
|
||||
for (int index = 0; index < beds.length; index++) {
|
||||
HydrologyFeatureRef feature = feature(HydrologyFeatureType.SURFACE_POOL,
|
||||
sameFeature ? 30L : 30L + index, 80);
|
||||
column.add(surfaceLayer(feature, beds[index], beds[index],
|
||||
false, false, true, beds[index] != 77, false));
|
||||
}
|
||||
HydrologyColumnSample merged = column.build();
|
||||
|
||||
assertEquals(77, merged.naturalHeight());
|
||||
assertEquals(81, merged.terrainHeight());
|
||||
assertEquals(4, merged.terrainHeight() - merged.naturalHeight());
|
||||
assertTrue(merged.primarySurfaceLayerOrNull().terrainOwned());
|
||||
assertEquals(sameFeature ? 1 : 4, merged.layers().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncutShoreRetainsItsBiomeWithoutOverridingAnOwnedBank() {
|
||||
HydrologyColumnLayer shore = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 30L, 75),
|
||||
|
||||
@@ -17,7 +17,7 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyPlanHashTest {
|
||||
private static final String EXPECTED_PLAN_DIGEST = "9ec4c9cf7dbdad16976041a70853b909816655170cd694058bf359092c2d5a10";
|
||||
private static final String EXPECTED_PLAN_DIGEST = "e54461c339854842f7fc7b2e85bd700b57942a2c691f5a249d8e498729378296";
|
||||
|
||||
private static final List<HydrologyTileKey> TILES = List.of(
|
||||
new HydrologyTileKey(0, 0),
|
||||
|
||||
@@ -17,7 +17,6 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -35,7 +34,6 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -669,25 +667,18 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceRouteMemoPreservesCompleteOwnerOutputAcrossPublicationPasses() throws Exception {
|
||||
public void sharedDrainagePreservesCompleteOwnerOutputAcrossPublicationPasses() {
|
||||
for (boolean organic : new boolean[]{false, true}) {
|
||||
HydrologyPlannerSettings settings = organic ? organicShapeSettings()
|
||||
: standardSettings(4D, 2D, true, false, List.of());
|
||||
HydrologyTerrainSampler terrain = organic ? organicShapeTerrain() : rollingCoast(112);
|
||||
SurfaceRouteMemos uncached = new SurfaceRouteMemos(false);
|
||||
SurfaceRouteMemos cached = new SurfaceRouteMemos(true);
|
||||
HydrologyTile expected = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), uncached);
|
||||
HydrologyTile actual = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), cached);
|
||||
|
||||
HydrologyPlanner planner = new HydrologyPlanner(642L, settings, terrain);
|
||||
HydrologyTile expected = planner.plan(TILE);
|
||||
planner.clearOwnerDrafts();
|
||||
HydrologyTile actual = planner.plan(TILE);
|
||||
assertEquals(expected, actual);
|
||||
assertEquals(expected.courses(), actual.courses());
|
||||
assertEquals(expected.localDiagnosticCandidates(), actual.localDiagnosticCandidates());
|
||||
assertTrue(cached.computations <= uncached.computations);
|
||||
if (organic) {
|
||||
assertTrue("hits=" + cached.hits, cached.hits > 0);
|
||||
assertTrue("uncached=" + uncached.computations + " cached=" + cached.computations,
|
||||
cached.computations < uncached.computations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,7 +708,10 @@ public class HydrologyPlannerTest {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.drop() > 0) {
|
||||
assertTrue(segment.type().isDrop());
|
||||
assertFalse(segment.fallingFluid());
|
||||
if (segment.fallingFluid()) {
|
||||
assertEquals(HydrologyFeatureType.WATERFALL, segment.type());
|
||||
assertTrue(tile.cavePlan(course.id()).orElseThrow().accepted());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1124,7 +1118,9 @@ public class HydrologyPlannerTest {
|
||||
);
|
||||
for (HydraulicSegment segment : segments(cliff, HydrologyFeatureType.WATERFALL)) {
|
||||
assertTrue(segment.drop() > 0);
|
||||
assertFalse(segment.fallingFluid());
|
||||
assertTrue(segment.fallingFluid());
|
||||
assertTrue(segment.receivingPool());
|
||||
assertTrue(cliff.cavePlan(segment.courseId()).orElseThrow().accepted());
|
||||
assertTrue(segment.centerline().size() >= 2);
|
||||
assertTrue(segment.width() >= 1);
|
||||
}
|
||||
@@ -1139,7 +1135,8 @@ public class HydrologyPlannerTest {
|
||||
HydraulicSegment receiver = course.segments().get(index + 1);
|
||||
assertTrue(approach.type().isSurface());
|
||||
assertTrue(receiver.type().isSurface());
|
||||
assertEquals(approach.end(), waterfall.start());
|
||||
assertEquals(approach.downstreamHeadY(), waterfall.upstreamHeadY());
|
||||
assertTrue(approach.end().distanceSquared2D(waterfall.start()) <= 2L);
|
||||
assertEquals(waterfall.end(), receiver.start());
|
||||
sawBlendedWaterfall = true;
|
||||
}
|
||||
@@ -1206,6 +1203,13 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
}
|
||||
assertTrue("courses=" + surfaceCourses(tile), aggregateTransition);
|
||||
for (HydrologyColumnSample column : tile.footprint().columns().values()) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.feature().type().isSurface() && layer.channel() && layer.terrainOwned()) {
|
||||
assertTrue(column.naturalHeight() - layer.bedY() <= settings.surface().maximumIncision());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1879,7 +1883,10 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
assertTrue(bed);
|
||||
assertEquals(drop.fallingFluid(), falling);
|
||||
assertFalse(drop.fallingFluid());
|
||||
if (drop.fallingFluid()) {
|
||||
assertEquals(HydrologyFeatureType.WATERFALL, drop.type());
|
||||
assertTrue(tile.cavePlan(drop.courseId()).orElseThrow().accepted());
|
||||
}
|
||||
if (!receiving) {
|
||||
HydrologyColumnSample endColumn = tile.columnAt(drop.end().x(), drop.end().z()).orElseThrow();
|
||||
receiving = endColumn.layers().stream().anyMatch((HydrologyColumnLayer layer) ->
|
||||
@@ -2210,6 +2217,15 @@ public class HydrologyPlannerTest {
|
||||
).orElseThrow();
|
||||
assertEquals(course.id(), located.courseId());
|
||||
assertEquals(sinkhole.id(), located.segmentId());
|
||||
for (HydrologyColumnSample column : first.footprint().columns().values()) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.feature().courseId() == course.id() && layer.feature().type().isSurface()
|
||||
&& layer.channel()) {
|
||||
assertTrue("Surface pond exceeded the channel incision limit at " + column.x() + "," + column.z(),
|
||||
column.naturalHeight() - layer.bedY() <= settings.surface().maximumIncision());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -3446,57 +3462,6 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
private HydrologyTile planWithSurfaceRouteMemos(HydrologyPlanner planner, SurfaceRouteMemos memos) throws Exception {
|
||||
Class<?> samplesClass = Class.forName(HydrologyPlanner.class.getName() + "$PlanningSamples");
|
||||
Constructor<?> constructor = samplesClass.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object samples = constructor.newInstance();
|
||||
Field routes = samplesClass.getDeclaredField("surfaceRoutes");
|
||||
routes.setAccessible(true);
|
||||
routes.set(samples, memos);
|
||||
Field scope = HydrologyPlanner.class.getDeclaredField("planningSamples");
|
||||
scope.setAccessible(true);
|
||||
ThreadLocal<Object> local = (ThreadLocal<Object>) scope.get(planner);
|
||||
local.set(samples);
|
||||
try {
|
||||
return planner.plan(TILE);
|
||||
} finally {
|
||||
local.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SurfaceRouteMemos extends IdentityHashMap<Object, HashMap<Object, List<HydrologyPoint>>> {
|
||||
private final boolean retain;
|
||||
private int hits;
|
||||
private int computations;
|
||||
|
||||
private SurfaceRouteMemos(boolean retain) {
|
||||
this.retain = retain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Object, List<HydrologyPoint>> computeIfAbsent(Object key,
|
||||
Function<? super Object, ? extends HashMap<Object, List<HydrologyPoint>>> ignored) {
|
||||
return super.computeIfAbsent(key, unused -> new HashMap<>() {
|
||||
@Override
|
||||
public List<HydrologyPoint> computeIfAbsent(Object route,
|
||||
Function<? super Object, ? extends List<HydrologyPoint>> computation) {
|
||||
if (!retain) {
|
||||
clear();
|
||||
}
|
||||
if (containsKey(route)) {
|
||||
hits++;
|
||||
}
|
||||
return super.computeIfAbsent(route, missing -> {
|
||||
computations++;
|
||||
return computation.apply(missing);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyPlanner countedPlanner(CountingNaturalSampler sampler) {
|
||||
return new HydrologyPlanner(91L, standardSettings(4D, 0D, true, false, List.of()),
|
||||
sampler.delegate, sampler, HydrologyGeometrySampler.deterministic(sampler.delegate),
|
||||
|
||||
+9
-62
@@ -31,9 +31,7 @@ public class HydrologyRegionalBankSupportTest {
|
||||
HydrologyTerrainSampler terrain = depression(false);
|
||||
List<HydrologyPoint> path = path();
|
||||
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path, "default", false, terrain);
|
||||
|
||||
assertEquals(path, refined);
|
||||
assertNull(validate(planner, path, terrain).rejection());
|
||||
SurfaceCourseResult built = build(path, terrain);
|
||||
assertTrue(built.accepted());
|
||||
@@ -44,7 +42,7 @@ public class HydrologyRegionalBankSupportTest {
|
||||
assertEquals(0, footprint.uncontainedWetCells());
|
||||
assertTrue(course.hydraulicallyNonRising());
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
assertTrue(column.layer().bedY() <= column.terrain().naturalHeight());
|
||||
assertTrue(column.layer().bedY() - column.terrain().naturalHeight() <= SETTINGS.surface().maximumIncision());
|
||||
}
|
||||
SurfaceLayerColumn center = footprint.columns().stream()
|
||||
.filter(column -> column.x() == 132 && column.z() == 0).findFirst().orElseThrow();
|
||||
@@ -58,67 +56,16 @@ public class HydrologyRegionalBankSupportTest {
|
||||
HydrologyTerrainSampler terrain = depression(true);
|
||||
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
||||
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
validate(planner, path(), terrain).rejection());
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path(), "default", false, terrain);
|
||||
if (!refined.isEmpty()) {
|
||||
assertFalse(build(refined, terrain).accepted());
|
||||
}
|
||||
assertNull(validate(planner, path(), terrain).rejection());
|
||||
SurfaceCourseResult built = build(path(), terrain);
|
||||
assertFalse(built.accepted());
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
|
||||
assertEquals(17, built.rejectionDetail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void levelCentersNeedNoBankSamplesAndSupportedDipsCannotRaiseIncomingHead() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
assertTrue(Math.abs(x - 132) <= 5 && Math.abs(z) <= 5);
|
||||
samples.incrementAndGet();
|
||||
return depression(false).sample(x, z);
|
||||
};
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
|
||||
|
||||
assertEquals(79, hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
|
||||
132, 0, 1D, 0D, HydrologyTerrainSample.openLand(80, 0D, "land"), 79), terrain));
|
||||
assertEquals(0, samples.get());
|
||||
assertEquals(78, hydraulics.supportedHead(station(78), terrain));
|
||||
assertTrue(samples.get() > 0 && samples.get() < 128);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableCrossSectionOrPerimeterCannotCertifyAContainedDip() {
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
|
||||
HydrologyTerrainSampler crossMissing = (x, z) -> x == 132 && z == 2 ? null : depression(false).sample(x, z);
|
||||
HydrologyTerrainSampler perimeterMissing = (x, z) -> x == 133 ? null : depression(false).sample(x, z);
|
||||
|
||||
assertEquals(65, hydraulics.supportedHead(station(80), crossMissing));
|
||||
assertEquals(65, hydraulics.supportedHead(station(80), perimeterMissing));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bankSupportCannotSamplePastTheExistingSearchBudget() throws Exception {
|
||||
AtomicInteger requests = new AtomicInteger();
|
||||
HydrologyTerrainSample ground = HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
requests.incrementAndGet();
|
||||
return ground;
|
||||
};
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(new HydrologyPlanner(71L, SETTINGS, terrain));
|
||||
HydrologyRegionalTerrainRefiner.Samples sampled = refiner.new Samples();
|
||||
for (int index = 0; index < 65536; index++) {
|
||||
sampled.sample(index, 100000);
|
||||
if (built.accepted()) {
|
||||
RiverCourse course = new RiverCourse(91L, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(2L),
|
||||
"default", 1, List.of(), built.segments());
|
||||
SurfaceFootprint footprint = new SurfaceFootprintCompiler(SETTINGS, terrain, GEOMETRY).compile(course);
|
||||
assertFalse(footprint.accepted());
|
||||
} else {
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
|
||||
}
|
||||
requests.set(0);
|
||||
|
||||
assertEquals(65, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(80), sampled));
|
||||
assertEquals(0, requests.get());
|
||||
}
|
||||
|
||||
private static HydrologyRegionalHydraulics.HeadStation station(int incomingHead) {
|
||||
return new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D,
|
||||
HydrologyTerrainSample.openLand(65, 0D, "land"), incomingHead);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler depression(boolean acrossBanks) {
|
||||
|
||||
+5
-54
@@ -14,34 +14,6 @@ import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalCacheTest {
|
||||
@Test
|
||||
public void searchBudgetDoesNotLimitSubsequentSearchesOrTerrainResults() {
|
||||
AtomicInteger reads = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
reads.incrementAndGet();
|
||||
return HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
|
||||
HydrologyRegionalTerrainRefiner.Samples first = refiner.new Samples();
|
||||
for (int x = 0; x < 65536; x++) {
|
||||
assertNotNull(first.sample(x, 0));
|
||||
}
|
||||
assertNull(first.sample(65536, 0));
|
||||
HydrologyRegionalTerrainRefiner.Samples second = refiner.new Samples();
|
||||
for (int x = 65536; x < 70000; x++) {
|
||||
assertNotNull(second.sample(x, 0));
|
||||
}
|
||||
assertEquals(70000, reads.get());
|
||||
for (int x = 0; x < 70000; x++) {
|
||||
assertEquals(70, refiner.sample(x, 0).naturalHeight());
|
||||
}
|
||||
int readsBeforeClear = reads.get();
|
||||
refiner.clear();
|
||||
assertEquals(70, refiner.sample(0, 0).naturalHeight());
|
||||
assertEquals(readsBeforeClear + 1, reads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingTilesRefreshesChangedRegionalTerrain() throws Exception {
|
||||
AtomicInteger height = new AtomicInteger(70);
|
||||
@@ -51,7 +23,7 @@ public class HydrologyRegionalCacheTest {
|
||||
return HydrologyTerrainSample.openLand(height.get(), 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
|
||||
HydrologyRegionalTerrain refiner = refiner(planner);
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
||||
assertEquals(70, refiner.sample(0, 0).naturalHeight());
|
||||
height.set(85);
|
||||
@@ -71,7 +43,7 @@ public class HydrologyRegionalCacheTest {
|
||||
return available.get() ? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
|
||||
HydrologyRegionalTerrain refiner = refiner(planner);
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
||||
assertNull(refiner.sample(0, 0));
|
||||
available.set(true);
|
||||
@@ -82,32 +54,11 @@ public class HydrologyRegionalCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingTilesReplansRegionalReachesAgainstCurrentTerrain() throws Exception {
|
||||
AtomicBoolean flooded = new AtomicBoolean();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> flooded.get() && x >= 96 && x <= 160 && Math.abs(z) < 32
|
||||
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
||||
assertEquals(guide, refiner.refine(guide, "default", false, terrain));
|
||||
flooded.set(true);
|
||||
cache.clear();
|
||||
List<HydrologyPoint> actual = refiner.refine(guide, "default", false, terrain);
|
||||
List<HydrologyPoint> fresh = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
|
||||
|
||||
assertFalse(fresh.isEmpty());
|
||||
assertTrue(fresh.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
assertEquals(fresh, actual);
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyRegionalTerrainRefiner refiner(HydrologyPlanner planner) throws Exception {
|
||||
private static HydrologyRegionalTerrain refiner(HydrologyPlanner planner) throws Exception {
|
||||
Field routes = HydrologyRegionalPlanner.class.getDeclaredField("routes");
|
||||
routes.setAccessible(true);
|
||||
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrainRefiner");
|
||||
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrain");
|
||||
refiner.setAccessible(true);
|
||||
return (HydrologyRegionalTerrainRefiner) refiner.get(routes.get(planner.regional));
|
||||
return (HydrologyRegionalTerrain) refiner.get(routes.get(planner.regional));
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -19,7 +19,7 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalCandidateSelectionTest {
|
||||
@Test
|
||||
public void lowerMeanderCandidatePassesTheFullPlannerAfterTheFirstBankCutFails() throws Exception {
|
||||
public void plannedHeadCrossesAChannelDepressionWithoutLoweringTheWholeCourse() throws Exception {
|
||||
HydrologyTerrainSampler terrain = coast(true);
|
||||
HydrologyPlanner planner = planner(terrain, 16, 2);
|
||||
List<HydrologyPoint> guide = guide(terrain);
|
||||
@@ -33,9 +33,8 @@ public class HydrologyRegionalCandidateSelectionTest {
|
||||
});
|
||||
|
||||
assertTrue(selected.toString(), selected.accepted());
|
||||
assertEquals(2, attempted.size());
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, attempted.getFirst().rejection().rejection());
|
||||
assertEquals(18, attempted.getFirst().rejection().detail());
|
||||
assertEquals(1, attempted.size());
|
||||
assertNull(attempted.getFirst().rejection());
|
||||
assertEquals(1, selected.network().courses().size());
|
||||
assertEquals(selected, attempted.getLast());
|
||||
RiverCourse course = selected.network().courses().getFirst();
|
||||
@@ -124,8 +123,10 @@ public class HydrologyRegionalCandidateSelectionTest {
|
||||
}
|
||||
int[] contributions = new int[9];
|
||||
Arrays.fill(contributions, 1);
|
||||
int[] heads = new int[9];
|
||||
Arrays.fill(heads, 80);
|
||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, contributions,
|
||||
List.of(new OutletCandidate(8, 8, outlet())));
|
||||
List.of(new OutletCandidate(8, 8, outlet())), heads);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
Method build = HydrologyRegionalPlanner.class.getDeclaredMethod("build", HydrologySampledGrid.class,
|
||||
HydrologyRegionalGraph.Tree.class, int.class, OutletCandidate.class, long.class, List.class);
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ public final class HydrologyRegionalFallsTest {
|
||||
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 1L, SETTINGS.fingerprint(),
|
||||
SETTINGS.routing().tileSize(), network.nodes(), network.edges(), network.outlets(),
|
||||
List.of(regional, local), Set.of(regional.id()), clipped.cavePlans(), List.of(), materialized.footprint());
|
||||
StudioHydrologyTileStore store = new StudioHydrologyTileStore(temporaryFolder.newFolder().toPath(),
|
||||
PreparedHydrologyTileStore store = new PreparedHydrologyTileStore(temporaryFolder.newFolder().toPath(),
|
||||
new HydrologyTileCache.SharedCacheScope("regional-ownership", 1L, 128, "overworld", SETTINGS.fingerprint()),
|
||||
SETTINGS.routing().tileSize());
|
||||
store.save(original);
|
||||
|
||||
+5
-5
@@ -28,8 +28,8 @@ public class HydrologyRegionalMouthRefinementTest {
|
||||
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forOutlet(settings, terrain, outlet);
|
||||
HydrologyPlanner planner = new HydrologyPlanner(1L, settings, terrain);
|
||||
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
|
||||
new HydrologyPoint(768, 80, 0), landward);
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(767, 63, 0),
|
||||
new HydrologyPoint(768, 63, 0), landward);
|
||||
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class,
|
||||
boolean.class, HydrologyTerrainSampler.class);
|
||||
validate.setAccessible(true);
|
||||
@@ -40,7 +40,7 @@ public class HydrologyRegionalMouthRefinementTest {
|
||||
|
||||
assertNull(accepted.toString(), accepted.rejection());
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, unproved.rejection());
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).receivingTerminal(landward, "default", false, receiver));
|
||||
assertTrue(new HydrologyRegionalTerrain(planner).receivingTerminal(landward, "default", false, receiver));
|
||||
assertFalse(terrain.sample(800, 0).ocean());
|
||||
assertTrue(receiver.receivingWater(800, 0, 63));
|
||||
}
|
||||
@@ -171,8 +171,8 @@ public class HydrologyRegionalMouthRefinementTest {
|
||||
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 768 ? 80 : 65, 0D, "land");
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
|
||||
new HydrologyPoint(768, 80, 0), new HydrologyPoint(799, 80, 0), new HydrologyPoint(800, 63, 0));
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(767, 63, 0),
|
||||
new HydrologyPoint(768, 63, 0), new HydrologyPoint(799, 63, 0), new HydrologyPoint(800, 63, 0));
|
||||
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
|
||||
case SURFACE_WIDTH -> 6;
|
||||
case SURFACE_DEPTH -> 2;
|
||||
|
||||
+4
-175
@@ -38,7 +38,7 @@ public class HydrologyRegionalSamplingTest {
|
||||
RecordingSampler serial = new RecordingSampler(terrain);
|
||||
SurfaceBankSupport.CrossSection cross = support.crossSection(serial, station, true);
|
||||
SurfaceBankSupport.Perimeter perimeter = support.perimeter(serial, station, 80);
|
||||
RecordingSampler parallel = new RecordingSampler(refiner(terrain).new Samples());
|
||||
RecordingSampler parallel = new RecordingSampler(refiner(terrain));
|
||||
pool.submit(() -> {
|
||||
assertEquals(cross, support.crossSection(parallel, station, true));
|
||||
assertEquals(perimeter, support.perimeter(parallel, station, 80));
|
||||
@@ -49,48 +49,6 @@ public class HydrologyRegionalSamplingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchLimitCountsNullsAndRetainsAlreadyAdmittedCoordinates() {
|
||||
AtomicInteger reads = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
reads.incrementAndGet();
|
||||
return x == 0 ? null : HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrainRefiner refiner = refiner(terrain);
|
||||
refiner.sample(100000, 0);
|
||||
HydrologyRegionalTerrainRefiner.Samples samples = refiner.new Samples();
|
||||
assertNull(samples.sample(0, 0));
|
||||
for (int x = 1; x < 65536; x++) {
|
||||
assertEquals(70, samples.sample(x, 0).naturalHeight());
|
||||
}
|
||||
assertNull(samples.sample(0, 0));
|
||||
assertEquals(70, samples.sample(1, 0).naturalHeight());
|
||||
assertNull(samples.sample(65536, 0));
|
||||
assertNull(samples.sample(100000, 0));
|
||||
assertEquals(65537, reads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedRequiredSampleRetainsItsExceptionAndDoesNotConsumeAdmission() {
|
||||
AtomicInteger failedReads = new AtomicInteger();
|
||||
IllegalStateException failure = new IllegalStateException("Required terrain unavailable");
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
if (x == -1 && failedReads.incrementAndGet() == 1) {
|
||||
throw failure;
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrainRefiner.Samples samples = refiner(terrain).new Samples();
|
||||
for (int x = 0; x < 65535; x++) {
|
||||
samples.sample(x, 0);
|
||||
}
|
||||
assertSame(failure, assertThrows(IllegalStateException.class, () -> samples.sample(-1, 0)));
|
||||
assertEquals(70, samples.sample(-1, 0).naturalHeight());
|
||||
assertEquals(70, samples.sample(0, 0).naturalHeight());
|
||||
assertNull(samples.sample(-2, 0));
|
||||
assertEquals(2, failedReads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableBankStopsBeforeALaterProviderFailure() throws Exception {
|
||||
IllegalStateException failure = new IllegalStateException("Later bank sample");
|
||||
@@ -103,7 +61,7 @@ public class HydrologyRegionalSamplingTest {
|
||||
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
|
||||
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(0, 0, 1D, 0D, 4D);
|
||||
SurfaceBankSupport.CrossSection expected = support.crossSection(terrain, station, true);
|
||||
HydrologyRegionalTerrainRefiner.Samples samples = refiner(terrain).new Samples();
|
||||
HydrologyRegionalTerrain samples = refiner(terrain);
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
assertEquals(expected, pool.submit(() -> support.crossSection(samples, station, true)).get(10, TimeUnit.SECONDS));
|
||||
}
|
||||
@@ -111,137 +69,8 @@ public class HydrologyRegionalSamplingTest {
|
||||
assertSame(failure, assertThrows(IllegalStateException.class, () -> samples.sample(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hydraulicBudgetAndLazyWidthsPreserveConsumptionOrder() throws Exception {
|
||||
HydrologyPlannerSettings.Surface surface = SETTINGS.surface();
|
||||
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(SETTINGS.seaLevel(), SETTINGS.routing(),
|
||||
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), 256,
|
||||
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), surface.banks()),
|
||||
SETTINGS.hydraulics(), SETTINGS.underground(), SETTINGS.outlets(), SETTINGS.geometry(), SETTINGS.deepFluids(),
|
||||
SETTINGS.surfacePools(), SETTINGS.widestShoreBiomeWidth(), SETTINGS.seaCaves(), SETTINGS.surfacePolicyBounds());
|
||||
HydrologyTerrainSample ground = new HydrologyTerrainSample(65, 0D, false, false, 0, 0,
|
||||
true, true, true, false, true, false, 0D, 1D, 1D, 0.75D, 1D, 1D, 1D, 1D,
|
||||
"land", "land", "land", "land", "land", "land", List.of("default"), List.of(),
|
||||
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x == 133 ? null : x == 132 && z == 0 ? ground
|
||||
: HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(settings);
|
||||
HydrologyRegionalHydraulics.HeadStation station = new HydrologyRegionalHydraulics.HeadStation(
|
||||
132, 0, 1D, 0D, terrain.sample(132, 0), 80);
|
||||
RecordingSampler serial = new RecordingSampler(terrain);
|
||||
int expected = hydraulics.supportedHead(station, serial);
|
||||
RecordingSampler parallel = new RecordingSampler(refiner(terrain).new Samples());
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
assertEquals(expected, pool.submit(() -> hydraulics.supportedHead(station, parallel)).get(10, TimeUnit.SECONDS).intValue());
|
||||
}
|
||||
assertEquals(serial.requests, parallel.requests);
|
||||
assertTrue(serial.requests.size() > 2048);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void routeRefinementMatchesSerialTerrainAndHydraulics() throws Exception {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 95 : 70, 0D, "land");
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
List<HydrologyPoint> expected = refiner(terrain).refine(guide, "default", false, terrain);
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
List<HydrologyPoint> actual = pool.submit(() -> refiner(terrain).refine(guide, "default", false, terrain))
|
||||
.get(15, TimeUnit.SECONDS);
|
||||
assertFalse(expected.isEmpty());
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coastalCenterlineRetainsNegativeAndFractionalTraversalOrder() throws Exception {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(64, 0D, "land");
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(-173, 64, -117), new HydrologyPoint(174, 64, 138));
|
||||
List<HydrologyPoint> expected = refiner(terrain).refine(guide, "default", true, terrain);
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
assertFalse(expected.isEmpty());
|
||||
assertEquals(expected, pool.submit(() -> refiner(terrain).refine(guide, "default", true, terrain))
|
||||
.get(15, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectedTraversalDoesNotReadALaterProviderFailure() throws Exception {
|
||||
IllegalStateException failure = new IllegalStateException("Later centerline sample");
|
||||
AtomicInteger failures = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
if (x == 32 && z == 0) {
|
||||
failures.incrementAndGet();
|
||||
throw failure;
|
||||
}
|
||||
return z == 0 && (x == 0 || x == 128)
|
||||
? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
|
||||
};
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0));
|
||||
assertTrue(refiner(terrain).refine(guide, "default", false, terrain).isEmpty());
|
||||
assertEquals(0, failures.get());
|
||||
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||
assertTrue(pool.submit(() -> refiner(terrain).refine(guide, "default", false, terrain))
|
||||
.get(15, TimeUnit.SECONDS).isEmpty());
|
||||
assertEquals(0, failures.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingAnActiveReachCannotPublishIntoTheNewCache() throws Exception {
|
||||
AtomicBoolean changed = new AtomicBoolean();
|
||||
AtomicBoolean blocked = new AtomicBoolean();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
boolean elevated = changed.get();
|
||||
if (x == 64 && z == 0 && blocked.compareAndSet(false, true)) {
|
||||
started.countDown();
|
||||
await(release);
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(elevated && x == 64 && z == 0 ? 100 : 70, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrainRefiner refiner = refiner(terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
|
||||
Future<List<HydrologyPoint>> previous = executor.submit(() -> refiner.refine(guide, "default", false, terrain));
|
||||
try {
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
changed.set(true);
|
||||
refiner.clear();
|
||||
assertEquals(100, refiner.sample(64, 0).naturalHeight());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
previous.get(15, TimeUnit.SECONDS);
|
||||
assertEquals(refiner(terrain).refine(guide, "default", false, terrain),
|
||||
refiner.refine(guide, "default", false, terrain));
|
||||
assertEquals(100, refiner.sample(64, 0).naturalHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signedCoordinateTiesRemainStableInEveryDirection() throws Exception {
|
||||
try (ForkJoinPool pool = new ForkJoinPool(8)) {
|
||||
for (int[] direction : new int[][]{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}) {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
int along = x * direction[0] + z * direction[1];
|
||||
int across = -x * direction[1] + z * direction[0];
|
||||
return HydrologyTerrainSample.openLand(
|
||||
along >= 96 && along <= 160 && Math.abs(across) < 32 ? 95 : 70, 0D, "land");
|
||||
};
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0),
|
||||
new HydrologyPoint(256 * direction[0], 70, 256 * direction[1]));
|
||||
List<HydrologyPoint> expected = refiner(terrain).refine(guide, "default", false, terrain);
|
||||
List<HydrologyPoint> actual = pool.submit(() -> refiner(terrain).refine(guide, "default", false, terrain))
|
||||
.get(15, TimeUnit.SECONDS);
|
||||
assertFalse(expected.isEmpty());
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyRegionalTerrainRefiner refiner(HydrologyTerrainSampler terrain) {
|
||||
return new HydrologyRegionalTerrainRefiner(new HydrologyPlanner(71L, SETTINGS, terrain));
|
||||
private static HydrologyRegionalTerrain refiner(HydrologyTerrainSampler terrain) {
|
||||
return new HydrologyRegionalTerrain(new HydrologyPlanner(71L, SETTINGS, terrain));
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalSkippedWaypointTest {
|
||||
@Test
|
||||
public void aSkippableHillDoesNotBlockAnEarlierValleyAcceptedByTheFullPlanner() throws Exception {
|
||||
Set<Integer> hills = Set.of(4);
|
||||
HydrologyTerrainSampler terrain = terrain(hills, true);
|
||||
HydrologyPlanner planner = planner(terrain);
|
||||
List<HydrologyPoint> guide = guide(terrain, hills);
|
||||
assertCoarseFeasible(planner, terrain, guide);
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
|
||||
|
||||
assertFalse(refined.isEmpty());
|
||||
assertFalse(refined.contains(guide.get(4)));
|
||||
Object context = context(guide, terrain);
|
||||
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
|
||||
candidate -> attempt(planner, context, candidate));
|
||||
|
||||
assertTrue(selected.toString(), selected.accepted());
|
||||
assertEquals(1, selected.network().courses().size());
|
||||
assertTrue(selected.network().courses().getFirst().hydraulicallyNonRising());
|
||||
assertEquals(16, planner.settings.surface().maximumIncision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theTerminalHeadRemainsMandatory() {
|
||||
HydrologyTerrainSampler highTerminal = terrain(Set.of(8), false);
|
||||
HydrologyPlanner planner = planner(highTerminal);
|
||||
List<HydrologyPoint> guide = guide(highTerminal, Set.of(8));
|
||||
assertCoarseFeasible(planner, highTerminal, guide);
|
||||
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, highTerminal).isEmpty());
|
||||
|
||||
HydrologyTerrainSampler lowTerminal = terrain(Set.of(), false);
|
||||
assertFalse(new HydrologyRegionalTerrainRefiner(planner(lowTerminal))
|
||||
.refine(guide(lowTerminal, Set.of()), "default", false, lowTerminal).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void threeConsecutiveHighWaypointsCannotBeSkipped() {
|
||||
Set<Integer> hills = Set.of(4, 5, 6);
|
||||
HydrologyTerrainSampler terrain = terrain(hills, true);
|
||||
HydrologyPlanner planner = planner(terrain);
|
||||
List<HydrologyPoint> guide = guide(terrain, hills);
|
||||
assertCoarseFeasible(planner, terrain, guide);
|
||||
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain).isEmpty());
|
||||
|
||||
Set<Integer> twoHills = Set.of(4, 5);
|
||||
HydrologyTerrainSampler skippable = terrain(twoHills, true);
|
||||
assertFalse(new HydrologyRegionalTerrainRefiner(planner(skippable))
|
||||
.refine(guide(skippable, twoHills), "default", false, skippable).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aSampledRidgeCannotRaiseTheHeadAfterAnEarlierValley() {
|
||||
HydrologyTerrainSampler base = terrain(Set.of(), true);
|
||||
HydrologyTerrainSampler ridge = (x, z) -> x >= 200 && x <= 224
|
||||
? HydrologyTerrainSample.openLand(95, 0D, "land") : base.sample(x, z);
|
||||
HydrologyPlanner planner = planner(ridge);
|
||||
List<HydrologyPoint> guide = guide(ridge, Set.of());
|
||||
assertCoarseFeasible(planner, ridge, guide);
|
||||
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, ridge).isEmpty());
|
||||
AtomicInteger admitted = new AtomicInteger();
|
||||
HydrologyRegionalRoute.Attempt selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, ridge,
|
||||
candidate -> {
|
||||
admitted.incrementAndGet();
|
||||
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
|
||||
});
|
||||
|
||||
assertFalse(selected.accepted());
|
||||
assertEquals(0, admitted.get());
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, selected.refinement().rejection());
|
||||
}
|
||||
|
||||
private static void assertCoarseFeasible(HydrologyPlanner planner, HydrologyTerrainSampler terrain, List<HydrologyPoint> guide) {
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
int available = Integer.MAX_VALUE;
|
||||
for (HydrologyPoint point : guide) {
|
||||
HydrologyTerrainSample sample = terrain.sample(point.x(), point.z());
|
||||
available = Math.min(available, hydraulics.maximumHead(sample));
|
||||
assertTrue(point.toString(), available >= hydraulics.minimumHead(sample));
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyRegionalRoute.Attempt attempt(HydrologyPlanner planner, Object context,
|
||||
HydrologyRegionalRoute.Refinement candidate) {
|
||||
try {
|
||||
Method attempt = HydrologyRegionalPlanner.class.getDeclaredMethod("attempt", context.getClass(), HydrologyRegionalRoute.Refinement.class);
|
||||
attempt.setAccessible(true);
|
||||
return (HydrologyRegionalRoute.Attempt) attempt.invoke(planner.regional, context, candidate);
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object context(List<HydrologyPoint> guide, HydrologyTerrainSampler terrain) throws Exception {
|
||||
Class<?> type = Class.forName(HydrologyRegionalPlanner.class.getName() + "$BuildContext");
|
||||
Constructor<?> constructor = type.getDeclaredConstructors()[0];
|
||||
constructor.setAccessible(true);
|
||||
int[] contributions = new int[guide.size()];
|
||||
Arrays.fill(contributions, 1);
|
||||
RiverOutlet outlet = new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 8L, new HydrologyPoint(1024, 63, 0),
|
||||
new HydrologyPoint(1025, 63, 0), 63, true);
|
||||
return constructor.newInstance(new HydrologyGridNode(0, 0, 0, 0, 0, 100L, terrain.sample(0, 0)),
|
||||
null, 123L, outlet, "default", new HydrologyRegionalFlow(guide, contributions),
|
||||
HydrologyRegionalMorphology.sample(guide, terrain, HydrologyRegionalPlannerTest.settings(false, 1)));
|
||||
}
|
||||
|
||||
private static List<HydrologyPoint> guide(HydrologyTerrainSampler terrain, Set<Integer> hills) {
|
||||
ArrayList<HydrologyPoint> guide = new ArrayList<>();
|
||||
for (int index = 0; index < 9; index++) {
|
||||
int x = index * 128;
|
||||
int z = index != 8 && hills.contains(index) ? 128 : 0;
|
||||
guide.add(new HydrologyPoint(x, terrain.sample(x, z).naturalHeight(), z));
|
||||
}
|
||||
return List.copyOf(guide);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler terrain(Set<Integer> hills, boolean coast) {
|
||||
return (x, z) -> {
|
||||
if (coast && x >= 1025) {
|
||||
return HydrologyTerrainSample.ocean(60, "ocean");
|
||||
}
|
||||
int height = coast ? 80 - Math.max(0, x - 752) / 16 : 80;
|
||||
if (x >= 32 && x <= 96) {
|
||||
height = 75;
|
||||
}
|
||||
for (int hill : hills) {
|
||||
if (Math.abs(x - hill * 128) <= 24 && Math.abs(z - (hill == 8 ? 0 : 128)) <= 24) {
|
||||
height = 93;
|
||||
}
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(height, 0D, "land");
|
||||
};
|
||||
}
|
||||
|
||||
private static HydrologyPlanner planner(HydrologyTerrainSampler terrain) {
|
||||
HydrologyPlannerSettings base = HydrologyRegionalPlannerTest.settings(false, 1);
|
||||
HydrologyPlannerSettings.Surface surface = base.surface();
|
||||
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(base.seaLevel(), base.routing(),
|
||||
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
|
||||
surface.minimumDepth(), surface.maximumDepth(), 16, surface.shoreWidth(), surface.banks()),
|
||||
base.hydraulics(), base.underground(), base.outlets(), base.geometry(), base.deepFluids(), base.surfacePools(),
|
||||
base.widestShoreBiomeWidth(), base.seaCaves(), base.surfacePolicyBounds());
|
||||
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
|
||||
case SURFACE_WIDTH -> 8;
|
||||
case SURFACE_DEPTH -> 2;
|
||||
default -> request.minimum();
|
||||
};
|
||||
return new HydrologyPlanner(71L, settings, terrain, geometry, -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(terrain, 63, -4096, 4096));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -209,7 +209,7 @@ public class HydrologyRegionalSourceParallelismTest {
|
||||
sources.put(RiverFootprint.pack(node.x(), node.z()), ordinal++);
|
||||
}
|
||||
assertEquals(secondCatchmentSize + 4, ordinal);
|
||||
return new HydrologyRegionalGraph.Tree(labels, contributions, outlets);
|
||||
return new HydrologyRegionalGraph.Tree(labels, contributions, outlets, new int[contributions.length]);
|
||||
});
|
||||
when(graph.path(any(), anyInt())).thenAnswer(invocation -> List.of(invocation.<Integer>getArgument(1), 0));
|
||||
replace(planner.regional, "graph", graph);
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ public class HydrologyRegionalSourceTrialsTest {
|
||||
new RiverOutlet(100L + root, HydrologyFeatureType.MOUTH, root, point, point, 63, true)));
|
||||
}
|
||||
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 16384, sources.size(), 1, nodes);
|
||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, new int[sources.size()], outlets);
|
||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, new int[sources.size()], outlets, new int[sources.size()]);
|
||||
return new Fixture(grid, tree, indices);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@ public class HydrologyRegionalSurfacePolicyTest {
|
||||
candidates.add(new OutletCandidate(node.index(), -1, new RiverOutlet(node.index() + 1000L,
|
||||
HydrologyFeatureType.MOUTH, node.id(), point, point, 63, true)));
|
||||
}
|
||||
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false);
|
||||
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false, null);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings withSources(HydrologyPlannerSettings.Source sources) {
|
||||
|
||||
-541
@@ -1,541 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalTerrainRefinerTest {
|
||||
@Test(timeout = 10000)
|
||||
public void clearingDuringSamplingCannotPublishOldTerrain() throws Exception {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
int call = calls.incrementAndGet();
|
||||
if (call == 1) {
|
||||
started.countDown();
|
||||
try {
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(interrupted);
|
||||
}
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(call == 1 ? 70 : 80, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(
|
||||
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
|
||||
try (ExecutorService workers = Executors.newSingleThreadExecutor()) {
|
||||
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
|
||||
try {
|
||||
assertTrue(started.await(3, TimeUnit.SECONDS));
|
||||
refiner.clear();
|
||||
assertEquals(80, refiner.sample(0, 0).naturalHeight());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
|
||||
assertEquals(80, refiner.sample(0, 0).naturalHeight());
|
||||
assertEquals(2, calls.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10000)
|
||||
public void independentDiagonalSamplesDoNotWaitForEachOthersTerrain() throws Exception {
|
||||
CountDownLatch sampling = new CountDownLatch(2);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
calls.incrementAndGet();
|
||||
sampling.countDown();
|
||||
try {
|
||||
assertTrue("Independent terrain samples were serialized", sampling.await(3, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(interrupted);
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(70 + x, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(
|
||||
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
|
||||
try (ExecutorService workers = Executors.newFixedThreadPool(2)) {
|
||||
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
|
||||
Future<HydrologyTerrainSample> second = workers.submit(() -> refiner.sample(1, 1));
|
||||
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
|
||||
assertEquals(71, second.get(5, TimeUnit.SECONDS).naturalHeight());
|
||||
assertEquals(first.get(), refiner.sample(0, 0));
|
||||
assertEquals(second.get(), refiner.sample(1, 1));
|
||||
assertEquals(2, calls.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aHigherHeadDetourSurvivesACheaperInfeasibleArrival() {
|
||||
int[][] heights = {
|
||||
{86, 94, 77, 91, 75, 85, 82, 88, 74},
|
||||
{84, 94, 83, 85, 72, 76, 73, 80, 72},
|
||||
{73, 85, 78, 77, 73, 82, 84, 81, 79},
|
||||
{83, 77, 92, 81, 84, 80, 93, 94, 96},
|
||||
{90, 73, 76, 84, 74, 73, 91, 80, 85},
|
||||
{91, 96, 90, 75, 91, 84, 94, 96, 95},
|
||||
{84, 76, 80, 92, 72, 73, 96, 75, 86},
|
||||
{94, 93, 86, 81, 80, 89, 75, 93, 77},
|
||||
{85, 95, 85, 73, 88, 82, 81, 94, 92}
|
||||
};
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
int gridX = Math.floorDiv(x, 16);
|
||||
int gridZ = Math.floorDiv(z, 16) + 4;
|
||||
return gridX < 0 || gridX >= 9 || gridZ < 0 || gridZ >= 9
|
||||
? HydrologyTerrainSample.ocean(60, "outside")
|
||||
: HydrologyTerrainSample.openLand(heights[gridZ][gridX], 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(1L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(128, 85, 0));
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
|
||||
List<HydrologyPoint> path = refiner.refine(guide, "default", false, terrain);
|
||||
|
||||
assertFalse(path.isEmpty());
|
||||
assertEquals(guide.getFirst(), path.getFirst());
|
||||
assertEquals(guide.getLast(), path.getLast());
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
int available = 90;
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
|
||||
assertFalse(sample.ocean());
|
||||
int supported = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
|
||||
centerline.x()[station], centerline.z()[station], centerline.tangentX()[station],
|
||||
centerline.tangentZ()[station], sample, available), terrain);
|
||||
assertTrue(supported <= available);
|
||||
available = supported;
|
||||
assertTrue("station=" + station, available >= hydraulics.minimumHead(sample));
|
||||
}
|
||||
assertEquals(path, refiner.refine(guide, "default", false, terrain));
|
||||
assertEquals(path, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shortRepairedPrefixDoesNotHideALongEnoughGuide() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 406 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 400 ? 60 : 70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(0, 70, 96),
|
||||
new HydrologyPoint(128, 70, 96), new HydrologyPoint(256, 70, 96), new HydrologyPoint(256, 70, 0),
|
||||
new HydrologyPoint(512, 70, 0));
|
||||
HydrologyRegionalRoute.Refinement selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
|
||||
candidate -> {
|
||||
double length = 0D;
|
||||
for (int index = 1; index < candidate.points().size(); index++) {
|
||||
length += StrictMath.sqrt(candidate.points().get(index - 1).distanceSquared2D(candidate.points().get(index)));
|
||||
}
|
||||
assertTrue(length >= planner.settings.routing().regional().minimumLength());
|
||||
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
|
||||
}).refinement();
|
||||
|
||||
assertNull(selected.toString(), selected.rejection());
|
||||
assertEquals(new HydrologyPoint(399, 63, 0), selected.oceanEntry().landward());
|
||||
assertEquals(new HydrologyPoint(406, 63, 0), selected.oceanEntry().receiving());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyTheRetainedDryPrefixNeedsTerrainRepair() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 806 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 800 ? 60 : x >= 96 && x <= 160 && Math.abs(z) < 32 ? 86 : 70,
|
||||
0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0),
|
||||
new HydrologyPoint(512, 70, 0), new HydrologyPoint(1024, 70, 0));
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain).isEmpty());
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
assertEquals(new HydrologyPoint(799, 63, 0), repaired.oceanEntry().landward());
|
||||
assertEquals(new HydrologyPoint(806, 63, 0), repaired.oceanEntry().receiving());
|
||||
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
assertTrue(repaired.points().stream().allMatch(point -> point.x() < 800));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCoarseOceanHitBeforeMinimumLengthStillAllowsADryTerrainDetour() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 || x >= 96 && x <= 160 && Math.abs(z) < 32
|
||||
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0),
|
||||
new HydrologyPoint(512, 70, 0), new HydrologyPoint(799, 70, 0));
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
assertEquals(guide.getLast(), repaired.points().getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturallyFloodedLandCannotBeUsedAsAnOwnedDryReach() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 60 : 70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, terrain);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
HydrologyTerrainSample sampled = terrain.sample(centerline.x()[station], centerline.z()[station]);
|
||||
assertFalse(sampled.ocean());
|
||||
assertTrue(sampled.naturalHeight() >= planner.settings.seaLevel());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedCoarseAnchorsCannotCreateRetracedWaterways() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(128, 70, 128), new HydrologyPoint(128, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
|
||||
|
||||
assertEquals(List.of(guide.getFirst(), guide.getLast()), refined);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void redundantCoarseAnchorHairpinsUseTheClearTerrainShortcut() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(160, 70, 32), new HydrologyPoint(144, 70, 48), new HydrologyPoint(160, 70, 48),
|
||||
new HydrologyPoint(192, 70, 16), new HydrologyPoint(256, 70, 0));
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
|
||||
|
||||
assertEquals(guide.getFirst(), refined.getFirst());
|
||||
assertEquals(guide.getLast(), refined.getLast());
|
||||
assertTrue(refined.size() <= 3);
|
||||
for (int index = 1; index < refined.size(); index++) {
|
||||
assertTrue(refined.get(index).x() > refined.get(index - 1).x());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repairsUnsampledOceanPocketsAtPositiveAndNegativeCoordinates() {
|
||||
for (int origin : new int[]{0, -512}) {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= origin + 96 && x <= origin + 160 && Math.abs(z) < 32
|
||||
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(origin, 70, 0), new HydrologyPoint(origin + 256, 70, 0));
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
|
||||
List<HydrologyPoint> first = refiner.refine(guide, "default", false, planner.sampler);
|
||||
|
||||
assertFalse(first.isEmpty());
|
||||
assertEquals(guide.getFirst(), first.getFirst());
|
||||
assertEquals(guide.getLast(), first.getLast());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(first);
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
assertFalse(terrain.sample(centerline.x()[station], centerline.z()[station]).ocean());
|
||||
}
|
||||
assertEquals(first, refiner.refine(guide, "default", false, planner.sampler));
|
||||
assertEquals(first, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aWaterCourseDetoursAroundALavaOnlyBiome() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(x >= 96 && x <= 160 && Math.abs(z) < 32 ? "lava" : "water");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "water", false, planner.sampler);
|
||||
|
||||
assertFalse(refined.isEmpty());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(refined);
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repairedTerrainRouteStillPassesOrganicShapeValidation() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 96 && x <= 160 && Math.abs(z) < 32
|
||||
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
HydrologyRegionalRoute.Refinement refined = new HydrologyRegionalRoute(planner).select(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(refined.toString(), refined.rejection());
|
||||
assertTrue(refined.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnavoidableOceanBarrierRejectsWithinTheTerrainSampleBudget() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
calls.incrementAndGet();
|
||||
return x >= 96 && x <= 160 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
|
||||
|
||||
assertTrue(refined.isEmpty());
|
||||
assertTrue("terrain samples=" + calls.get(), calls.get() <= 65536);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableTerrainIsSampledOnceAndCountsTowardTheBudget() {
|
||||
Map<Long, Integer> unavailable = new HashMap<>();
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
calls.incrementAndGet();
|
||||
if (x >= 96 && x <= 160 && Math.abs(z) < 32) {
|
||||
unavailable.merge(RiverFootprint.pack(x, z), 1, Integer::sum);
|
||||
return null;
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
assertFalse(unavailable.isEmpty());
|
||||
assertTrue(unavailable.values().stream().allMatch(count -> count == 1));
|
||||
assertTrue(calls.get() <= 65536);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUnsampledRidgeUsesTheLowerValleyWithoutIncreasingIncision() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 94 : 70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0));
|
||||
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
assertEquals(70, terrain.sample(centerline.x()[station], centerline.z()[station]).naturalHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anImpossibleIntermediateAnchorCanBeSkippedWithinTheSameValley() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x < 48 || x >= 120 && x <= 136 && Math.abs(z) < 8 ? 90 : 65, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 90, 0), new HydrologyPoint(128, 90, 0),
|
||||
new HydrologyPoint(256, 65, 0));
|
||||
HydrologyRegionalTerrainRefiner refiner = new HydrologyRegionalTerrainRefiner(planner);
|
||||
List<HydrologyPoint> repaired = refiner.refine(guide, "default", false, planner.sampler);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
assertEquals(guide.getFirst(), repaired.getFirst());
|
||||
assertEquals(guide.getLast(), repaired.getLast());
|
||||
assertFalse(repaired.contains(guide.get(1)));
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
int head = Integer.MAX_VALUE;
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
|
||||
head = Math.min(head, hydraulics.maximumHead(sample));
|
||||
assertTrue(hydraulics.minimumHead(sample) <= head);
|
||||
}
|
||||
assertEquals(repaired, refiner.refine(guide, "default", false, planner.sampler));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aLowFineDetourCannotResetTheHeadBeforeTheNextRidge() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 65 : x >= 384 ? 92 : 80, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(256, 80, 0),
|
||||
new HydrologyPoint(512, 92, 0));
|
||||
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
assertTrue(repaired.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
int head = Integer.MAX_VALUE;
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
HydrologyTerrainSample sample = terrain.sample(centerline.x()[station], centerline.z()[station]);
|
||||
head = Math.min(head, hydraulics.maximumHead(sample));
|
||||
assertTrue(hydraulics.minimumHead(sample) <= head);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ridgeRepairRespectsTheLocalIncisionPolicy() {
|
||||
SurfaceRiverPolicy limited = new SurfaceRiverPolicy("valley", null, null, null, null, null, null, 4);
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 96 && x <= 160 && Math.abs(z) < 32 ? 76 : 70, 0D, "land").withSurfacePolicy(limited);
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
assertTrue(repaired.stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void detourLengthRejectsBeforeDetailedSamplingOfTheRemainingCourse() {
|
||||
AtomicInteger maximumX = new AtomicInteger(Integer.MIN_VALUE);
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
boolean coarseAnchor = Math.floorMod(x + 1, 256) <= 2 && Math.abs(z) <= 1;
|
||||
if (!coarseAnchor) {
|
||||
maximumX.accumulateAndGet(x, Math::max);
|
||||
}
|
||||
return x >= 96 && x <= 160 && Math.abs(z) < 32 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
ArrayList<HydrologyPoint> guide = new ArrayList<>();
|
||||
for (int x = 0; x <= planner.settings.routing().maximumRouteLength(); x += 256) {
|
||||
guide.add(new HydrologyPoint(x, 70, 0));
|
||||
}
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(guide, "default", false, planner.sampler).isEmpty());
|
||||
assertTrue("A rejected first detour must not sample later reaches", maximumX.get() < 512);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anAlreadyOverlongGuideDoesNotSampleTerrain() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
calls.incrementAndGet();
|
||||
return HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
int before = calls.get();
|
||||
assertTrue(new HydrologyRegionalTerrainRefiner(planner).refine(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(4096, 70, 0)), "default", false, planner.sampler).isEmpty());
|
||||
assertEquals(before, calls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearancePreferenceDoesNotExcludeTheOnlyNarrowCorridor() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
boolean horizontal = Math.abs(z) <= 3 && (x <= 80 || x >= 176);
|
||||
boolean detour = Math.abs(z - 16) <= 3 && x >= 61 && x <= 195;
|
||||
boolean vertical = (Math.abs(x - 64) <= 3 || Math.abs(x - 192) <= 3) && z >= -3 && z <= 19;
|
||||
return land(horizontal || detour || vertical ? "water" : "lava");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> repaired = new HydrologyRegionalTerrainRefiner(planner).refine(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "water", false, planner.sampler);
|
||||
|
||||
assertFalse(repaired.isEmpty());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired);
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oceanDetourPreservesTheShippingTwentyDegreeTurnLimit() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 96 && x <= 160 && Math.abs(z) < 32
|
||||
? HydrologyTerrainSample.ocean(60, "ocean") : HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 20D)), terrain);
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(
|
||||
List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(256, 70, 0)), "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
assertTrue(repaired.points().stream().anyMatch(point -> Math.abs(point.z()) >= 32));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shortRepairReversalsReceiveEnoughReachToMeetTwentyDegrees() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 1, 20D)),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(160, 70, 32), new HydrologyPoint(160, 70, 16),
|
||||
new HydrologyPoint(192, 70, 16), new HydrologyPoint(192, 70, 128), new HydrologyPoint(512, 70, 128));
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
assertEquals(guide.getFirst(), repaired.points().getFirst());
|
||||
assertEquals(guide.getLast(), repaired.points().getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundedCurveTurnLimitIsMeasuredBeforeBlockQuantization() throws Exception {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 20D)),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
|
||||
Method interpolate = HydrologyRegionalRoute.class.getDeclaredMethod("interpolateLinear", List.class);
|
||||
Method round = HydrologyRegionalRoute.class.getDeclaredMethod("roundBends", List.class, double.class);
|
||||
Method resample = HydrologyRegionalRoute.class.getDeclaredMethod("resample", List.class);
|
||||
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateRounded", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
|
||||
interpolate.setAccessible(true);
|
||||
round.setAccessible(true);
|
||||
resample.setAccessible(true);
|
||||
validate.setAccessible(true);
|
||||
Object linear = interpolate.invoke(route, guide);
|
||||
Object rounded = round.invoke(route, linear, 128D);
|
||||
Object curve = resample.invoke(route, rounded);
|
||||
HydrologyRegionalRoute.Refinement accepted = (HydrologyRegionalRoute.Refinement) validate.invoke(route, curve, "default", false, planner.sampler);
|
||||
|
||||
assertNull(accepted.toString(), accepted.rejection());
|
||||
assertEquals(guide.getFirst(), accepted.points().getFirst());
|
||||
assertEquals(guide.getLast(), accepted.points().getLast());
|
||||
Object sharper = resample.invoke(route, round.invoke(route, linear, 64D));
|
||||
HydrologyRegionalRoute.Refinement rejected = (HydrologyRegionalRoute.Refinement) validate.invoke(route, sharper, "default", false, planner.sampler);
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, rejected.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splineOvershootCannotDiscardAValidNarrowDetour() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
boolean first = Math.abs(z) <= 8 && x >= -8 && x <= 136;
|
||||
boolean middle = Math.abs(x - 128) <= 8 && z >= -8 && z <= 136;
|
||||
boolean last = Math.abs(z - 128) <= 8 && x >= 120 && x <= 264;
|
||||
return land(first || middle || last ? "water" : "lava");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 150D)), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
|
||||
assertEquals(guide, new HydrologyRegionalTerrainRefiner(planner).refine(guide, "water", false, planner.sampler));
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "water", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired.points());
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings withMeanders(HydrologyPlannerSettings.Meanders meanders) {
|
||||
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
|
||||
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
|
||||
return new HydrologyPlannerSettings(settings.seaLevel(), settings.routing(), settings.surface(), settings.hydraulics(),
|
||||
settings.underground(), settings.outlets(), new HydrologyPlannerSettings.Geometry(meanders,
|
||||
geometry.surface(), geometry.underground(), geometry.grottos(), geometry.drops()), settings.deepFluids(),
|
||||
settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(), settings.surfacePolicyBounds());
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(String profile) {
|
||||
return new HydrologyTerrainSample(70, 0D, false, false, 38, 40,
|
||||
true, true, true, false, false, false,
|
||||
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
|
||||
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
|
||||
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyRegionalTerrainTest {
|
||||
@Test(timeout = 10000)
|
||||
public void clearingDuringSamplingCannotPublishOldTerrain() throws Exception {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
int call = calls.incrementAndGet();
|
||||
if (call == 1) {
|
||||
started.countDown();
|
||||
try {
|
||||
assertTrue(release.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(interrupted);
|
||||
}
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(call == 1 ? 70 : 80, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrain refiner = new HydrologyRegionalTerrain(
|
||||
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
|
||||
try (ExecutorService workers = Executors.newSingleThreadExecutor()) {
|
||||
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
|
||||
try {
|
||||
assertTrue(started.await(3, TimeUnit.SECONDS));
|
||||
refiner.clear();
|
||||
assertEquals(80, refiner.sample(0, 0).naturalHeight());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
|
||||
assertEquals(80, refiner.sample(0, 0).naturalHeight());
|
||||
assertEquals(2, calls.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10000)
|
||||
public void independentDiagonalSamplesDoNotWaitForEachOthersTerrain() throws Exception {
|
||||
CountDownLatch sampling = new CountDownLatch(2);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
calls.incrementAndGet();
|
||||
sampling.countDown();
|
||||
try {
|
||||
assertTrue("Independent terrain samples were serialized", sampling.await(3, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(interrupted);
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(70 + x, 0D, "land");
|
||||
};
|
||||
HydrologyRegionalTerrain refiner = new HydrologyRegionalTerrain(
|
||||
new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain));
|
||||
try (ExecutorService workers = Executors.newFixedThreadPool(2)) {
|
||||
Future<HydrologyTerrainSample> first = workers.submit(() -> refiner.sample(0, 0));
|
||||
Future<HydrologyTerrainSample> second = workers.submit(() -> refiner.sample(1, 1));
|
||||
assertEquals(70, first.get(5, TimeUnit.SECONDS).naturalHeight());
|
||||
assertEquals(71, second.get(5, TimeUnit.SECONDS).naturalHeight());
|
||||
assertEquals(first.get(), refiner.sample(0, 0));
|
||||
assertEquals(second.get(), refiner.sample(1, 1));
|
||||
assertEquals(2, calls.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shortRepairedPrefixDoesNotHideALongEnoughGuide() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 406 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 400 ? 60 : 70, 0D, "land");
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(0, 70, 96),
|
||||
new HydrologyPoint(128, 70, 96), new HydrologyPoint(256, 70, 96), new HydrologyPoint(256, 70, 0),
|
||||
new HydrologyPoint(512, 70, 0));
|
||||
HydrologyRegionalRoute.Refinement selected = new HydrologyRegionalRoute(planner).select(guide, "default", false, terrain,
|
||||
candidate -> {
|
||||
double length = 0D;
|
||||
for (int index = 1; index < candidate.points().size(); index++) {
|
||||
length += StrictMath.sqrt(candidate.points().get(index - 1).distanceSquared2D(candidate.points().get(index)));
|
||||
}
|
||||
assertTrue(length >= planner.settings.routing().regional().minimumLength());
|
||||
return new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
|
||||
}).refinement();
|
||||
|
||||
assertNull(selected.toString(), selected.rejection());
|
||||
assertEquals(new HydrologyPoint(399, 63, 0), selected.oceanEntry().landward());
|
||||
assertEquals(new HydrologyPoint(406, 63, 0), selected.oceanEntry().receiving());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shortRepairReversalsReceiveEnoughReachToMeetTwentyDegrees() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 1, 20D)),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(160, 70, 32), new HydrologyPoint(160, 70, 16),
|
||||
new HydrologyPoint(192, 70, 16), new HydrologyPoint(192, 70, 128), new HydrologyPoint(512, 70, 128));
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "default", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
assertEquals(guide.getFirst(), repaired.points().getFirst());
|
||||
assertEquals(guide.getLast(), repaired.points().getLast());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundedCurveTurnLimitIsMeasuredBeforeBlockQuantization() throws Exception {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 20D)),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(70, 0D, "land"));
|
||||
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
|
||||
Method interpolate = HydrologyRegionalRoute.class.getDeclaredMethod("interpolateLinear", List.class);
|
||||
Method round = HydrologyRegionalRoute.class.getDeclaredMethod("roundBends", List.class, double.class);
|
||||
Method resample = HydrologyRegionalRoute.class.getDeclaredMethod("resample", List.class);
|
||||
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateRounded", List.class, String.class, boolean.class, HydrologyTerrainSampler.class);
|
||||
interpolate.setAccessible(true);
|
||||
round.setAccessible(true);
|
||||
resample.setAccessible(true);
|
||||
validate.setAccessible(true);
|
||||
Object linear = interpolate.invoke(route, guide);
|
||||
Object rounded = round.invoke(route, linear, 128D);
|
||||
Object curve = resample.invoke(route, rounded);
|
||||
HydrologyRegionalRoute.Refinement accepted = (HydrologyRegionalRoute.Refinement) validate.invoke(route, curve, "default", false, planner.sampler);
|
||||
|
||||
assertNull(accepted.toString(), accepted.rejection());
|
||||
assertEquals(guide.getFirst(), accepted.points().getFirst());
|
||||
assertEquals(guide.getLast(), accepted.points().getLast());
|
||||
Object sharper = resample.invoke(route, round.invoke(route, linear, 64D));
|
||||
HydrologyRegionalRoute.Refinement rejected = (HydrologyRegionalRoute.Refinement) validate.invoke(route, sharper, "default", false, planner.sampler);
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, rejected.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void splineOvershootCannotDiscardAValidNarrowDetour() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
boolean first = Math.abs(z) <= 8 && x >= -8 && x <= 136;
|
||||
boolean middle = Math.abs(x - 128) <= 8 && z >= -8 && z <= 136;
|
||||
boolean last = Math.abs(z - 128) <= 8 && x >= 120 && x <= 264;
|
||||
return land(first || middle || last ? "water" : "lava");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(15L, withMeanders(
|
||||
new HydrologyPlannerSettings.Meanders(64, 12, 0D, 0D, 0D, 0, 150D)), terrain);
|
||||
List<HydrologyPoint> guide = List.of(new HydrologyPoint(0, 70, 0), new HydrologyPoint(128, 70, 0),
|
||||
new HydrologyPoint(128, 70, 128), new HydrologyPoint(256, 70, 128));
|
||||
HydrologyRegionalRoute.Refinement repaired = new HydrologyRegionalRoute(planner).select(guide, "water", false, planner.sampler, candidate -> new HydrologyRegionalRoute.Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null)).refinement();
|
||||
|
||||
assertNull(repaired.toString(), repaired.rejection());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(repaired.points());
|
||||
for (int station = 0; station < centerline.size(); station++) {
|
||||
assertEquals(List.of("water"), terrain.sample(centerline.x()[station], centerline.z()[station]).preferredProfileKeys());
|
||||
}
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings withMeanders(HydrologyPlannerSettings.Meanders meanders) {
|
||||
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
|
||||
HydrologyPlannerSettings.Geometry geometry = settings.geometry();
|
||||
return new HydrologyPlannerSettings(settings.seaLevel(), settings.routing(), settings.surface(), settings.hydraulics(),
|
||||
settings.underground(), settings.outlets(), new HydrologyPlannerSettings.Geometry(meanders,
|
||||
geometry.surface(), geometry.underground(), geometry.grottos(), geometry.drops()), settings.deepFluids(),
|
||||
settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves(), settings.surfacePolicyBounds());
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(String profile) {
|
||||
return new HydrologyTerrainSample(70, 0D, false, false, 38, 40,
|
||||
true, true, true, false, false, false,
|
||||
0D, 1D, 1D, 1D, 1D, 1D, 1D, 1D,
|
||||
"land", "land", "land", "land", "land", "land", List.of(profile), List.of(),
|
||||
Double.NaN, null, Double.NaN, true, SurfaceRiverPolicy.INHERIT);
|
||||
}
|
||||
}
|
||||
+3
-127
@@ -27,8 +27,6 @@ public class HydrologyRegionalWidthSupportTest {
|
||||
public void aWiderChannelContainsTheCenterDepressionWithoutRaisingLand() throws Exception {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 128 && x <= 136 && Math.abs(z) <= 1 ? 65 : x >= 256 && x <= 288 ? 86 : 80, 0D, "land");
|
||||
HydrologyRegionalHydraulics hydraulics = new HydrologyRegionalHydraulics(SETTINGS);
|
||||
assertEquals(80, hydraulics.supportedHead(station(terrain, 80), terrain));
|
||||
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
||||
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateHydraulics",
|
||||
HydrologyRegionalRoute.Refinement.class, String.class, boolean.class, HydrologyTerrainSampler.class);
|
||||
@@ -48,7 +46,7 @@ public class HydrologyRegionalWidthSupportTest {
|
||||
assertTrue(course.hydraulicallyNonRising());
|
||||
SurfaceLayerColumn center = null;
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
assertTrue(column.layer().bedY() <= column.terrain().naturalHeight());
|
||||
assertTrue(column.layer().bedY() - column.terrain().naturalHeight() <= SETTINGS.surface().maximumIncision());
|
||||
assertTrue(column.terrain().naturalHeight() - column.layer().bedY() <= SETTINGS.surface().maximumIncision());
|
||||
if (column.x() == 132 && column.z() == 0) {
|
||||
center = column;
|
||||
@@ -59,112 +57,8 @@ public class HydrologyRegionalWidthSupportTest {
|
||||
assertEquals(80, center.layer().fluidHeadY());
|
||||
|
||||
SurfaceCourseResult narrow = build(terrain, geometry(4));
|
||||
assertFalse(narrow.accepted());
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, narrow.rejection());
|
||||
assertEquals(23, narrow.rejectionDetail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anInteriorWidthCanHoldHeadWhenBothAuthoredExtremaFail() {
|
||||
HydrologyTerrainSampler terrain = narrowBanks();
|
||||
assertEquals(65, bankHead(terrain, 4));
|
||||
assertEquals(80, bankHead(terrain, 6));
|
||||
assertEquals(65, bankHead(terrain, 8));
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
HydrologyTerrainSampler bounded = (x, z) -> {
|
||||
assertTrue("A supported interior width must stop wider probes", Math.abs(z) <= 7);
|
||||
samples.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
};
|
||||
|
||||
assertEquals(80, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), bounded));
|
||||
assertTrue(samples.get() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fractionalLocalWidthIsCheckedBeforeUnneededWiderCandidates() {
|
||||
HydrologyTerrainSampler source = narrowBanks();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> withWidthMultiplier(source.sample(x, z), 0.8125D);
|
||||
assertEquals(80, bankHead(terrain, 6.5D));
|
||||
HydrologyTerrainSampler bounded = (x, z) -> {
|
||||
assertTrue("The local width6.5 already supports the incoming head", Math.abs(z) <= 6);
|
||||
return terrain.sample(x, z);
|
||||
};
|
||||
|
||||
assertEquals(80, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), bounded));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void minimumWidthSupportStaysLazyAndCannotRaiseIncomingHead() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 128 && x <= 136 && z == 0 ? 65 : 80, 0D, "land");
|
||||
AtomicInteger expectedSamples = new AtomicInteger();
|
||||
bankHead((x, z) -> {
|
||||
expectedSamples.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
}, 4);
|
||||
AtomicInteger actualSamples = new AtomicInteger();
|
||||
HydrologyTerrainSampler counted = (x, z) -> {
|
||||
actualSamples.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
};
|
||||
|
||||
assertEquals(78, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 78), counted));
|
||||
assertEquals(expectedSamples.get(), actualSamples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteWiderPerimetersCannotCertifyAHighHead() {
|
||||
HydrologyTerrainSampler terrain = narrowBanks();
|
||||
HydrologyTerrainSampler incomplete = (x, z) -> x == 133 ? null : terrain.sample(x, z);
|
||||
|
||||
assertEquals(65, new HydrologyRegionalHydraulics(SETTINGS).supportedHead(station(terrain, 80), incomplete));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void additionalSamplingAndRadiusStayBoundedWhenWiderPerimetersAreIncomplete() {
|
||||
HydrologyPlannerSettings settings = wideSettings();
|
||||
HydrologyTerrainSample ground = withWidthMultiplier(HydrologyTerrainSample.openLand(65, 0D, "land"), 0.75D);
|
||||
HydrologyTerrainSample bank = HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
HydrologyTerrainSampler incomplete = (x, z) -> x == 133 ? null : x == 132 && z == 0 ? ground : bank;
|
||||
AtomicInteger minimumSamples = new AtomicInteger();
|
||||
HydrologyTerrainSampler minimum = (x, z) -> {
|
||||
minimumSamples.incrementAndGet();
|
||||
return incomplete.sample(x, z);
|
||||
};
|
||||
SurfaceBankSupport support = new SurfaceBankSupport(settings.surface(), settings.seaLevel());
|
||||
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(132, 0, 1D, 0D, 2D);
|
||||
SurfaceBankSupport.CrossSection cross = support.crossSection(minimum, station, true);
|
||||
assertFalse(support.perimeter(minimum, station, cross.minimum()).complete());
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> {
|
||||
assertTrue(Math.abs(x - 132) <= 96 && Math.abs(z) <= 96);
|
||||
samples.incrementAndGet();
|
||||
return incomplete.sample(x, z);
|
||||
};
|
||||
|
||||
assertEquals(65, new HydrologyRegionalHydraulics(settings).supportedHead(
|
||||
new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D, ground, 80), terrain));
|
||||
assertEquals(minimumSamples.get() + 2048, samples.get());
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler narrowBanks() {
|
||||
return (x, z) -> HydrologyTerrainSample.openLand(
|
||||
x >= 128 && x <= 136 && Math.abs(z) <= 1 || Math.abs(z) >= 7 ? 65 : 80, 0D, "land");
|
||||
}
|
||||
|
||||
private static int bankHead(HydrologyTerrainSampler terrain, double width) {
|
||||
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
|
||||
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(132, 0, 1D, 0D, width / 2D);
|
||||
SurfaceBankSupport.CrossSection cross = support.crossSection(terrain, station, true);
|
||||
assertFalse(cross.blocked());
|
||||
SurfaceBankSupport.Perimeter perimeter = support.perimeter(terrain, station, cross.minimum());
|
||||
assertTrue(perimeter.complete());
|
||||
return perimeter.minimum();
|
||||
}
|
||||
|
||||
private static HydrologyRegionalHydraulics.HeadStation station(HydrologyTerrainSampler terrain, int incoming) {
|
||||
return new HydrologyRegionalHydraulics.HeadStation(132, 0, 1D, 0D, terrain.sample(132, 0), incoming);
|
||||
assertTrue(narrow.accepted());
|
||||
assertTrue(narrow.segments().stream().allMatch(segment -> segment.upstreamHeadY() >= segment.downstreamHeadY()));
|
||||
}
|
||||
|
||||
private static HydrologyGeometrySampler geometry(int width) {
|
||||
@@ -180,22 +74,4 @@ public class HydrologyRegionalWidthSupportTest {
|
||||
.build(71L, 91L, "default", PATH, SurfaceTerminal.SINKHOLE, 40, 256);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample withWidthMultiplier(HydrologyTerrainSample source, double multiplier) {
|
||||
return new HydrologyTerrainSample(source.naturalHeight(), source.slope(), source.ocean(), source.caveAvailable(),
|
||||
source.caveFloorY(), source.caveFluidY(), source.transitAllowed(), source.outletAllowed(), source.surfaceSourceAllowed(),
|
||||
source.surfaceSourceRequired(), source.undergroundSourceAllowed(), source.undergroundSourceRequired(), source.routingCost(),
|
||||
source.surfaceSourceWeight(), source.undergroundSourceWeight(), multiplier, source.depthMultiplier(), source.incisionMultiplier(),
|
||||
source.routingMultiplier(), source.bankMultiplier(), source.parentBiomeKey(), source.surfaceBiomeKey(), source.mouthBiomeKey(),
|
||||
source.shoreBiomeKey(), source.bankBiomeKey(), source.floodedCaveBiomeKey(), source.preferredProfileKeys(), source.surfacePoolKeys(),
|
||||
source.shoreBiomeWidth(), source.confinesKey(), source.shoreWidth(), source.erosion(), source.surfacePolicy());
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings wideSettings() {
|
||||
HydrologyPlannerSettings.Surface surface = SETTINGS.surface();
|
||||
return new HydrologyPlannerSettings(SETTINGS.seaLevel(), SETTINGS.routing(),
|
||||
new HydrologyPlannerSettings.Surface(surface.enabled(), surface.sources(), surface.minimumWidth(), 256,
|
||||
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), surface.banks()),
|
||||
SETTINGS.hydraulics(), SETTINGS.underground(), SETTINGS.outlets(), SETTINGS.geometry(), SETTINGS.deepFluids(), SETTINGS.surfacePools(),
|
||||
SETTINGS.widestShoreBiomeWidth(), SETTINGS.seaCaves(), SETTINGS.surfacePolicyBounds());
|
||||
}
|
||||
}
|
||||
|
||||
-22
@@ -10,28 +10,6 @@ import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
public class HydrologyRouteHeightProbeTest {
|
||||
@Test
|
||||
public void crossDropUsesScalarHeightsIncludingMissingAndExtremeValues() {
|
||||
HydrologyTerrainSample center = land(87);
|
||||
HydrologyTerrainSample[] sides = {null, HydrologyTerrainSample.ocean(20, "ocean"),
|
||||
land(20), land(100), land(Integer.MIN_VALUE), land(Integer.MAX_VALUE)};
|
||||
for (HydrologyTerrainSample side : sides) {
|
||||
Fixture fixture = fixture((x, z) -> side);
|
||||
int lowest = side == null || side.ocean() ? 87 : Math.min(87, side.naturalHeight());
|
||||
double expected = (double) (87 - lowest) * fixture.planner().settings.routing().valleyPreference()
|
||||
* HydrologyRouteGeometry.CROSS_DROP_WEIGHT;
|
||||
double actual = fixture.planner().routeGeometry.crossDropPenalty(
|
||||
new HydrologyPoint(21, 87, -37), new RouteDirection(3D, 4D), center);
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
assertEquals(4, fixture.scalarCalls().get());
|
||||
assertEquals(0, fixture.basisCalls().get());
|
||||
}
|
||||
Fixture untouched = fixture((x, z) -> land(20));
|
||||
assertEquals(0D, untouched.planner().routeGeometry.crossDropPenalty(
|
||||
new HydrologyPoint(0, 87, 0), new RouteDirection(0D, 0D), center), 0D);
|
||||
assertEquals(0, untouched.scalarCalls().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pitChecksRetainMissingAndIntegerDifferenceSemantics() {
|
||||
int threshold = HydrologyPlannerSettings.defaults().hydraulics().waterfallMinimumDrop();
|
||||
|
||||
-254
@@ -1,254 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class HydrologyRouteTurnCacheTest {
|
||||
@Test
|
||||
public void repeatedRefinementMatchesExhaustiveUncachedRoutes() throws Exception {
|
||||
Random random = new Random(712319L);
|
||||
for (int scenario = 0; scenario < 40; scenario++) {
|
||||
Solver solver = new Solver();
|
||||
int count = 4 + random.nextInt(4);
|
||||
HydrologyPoint[][] points = new HydrologyPoint[count][];
|
||||
for (int layer = 0; layer < count; layer++) {
|
||||
points[layer] = new HydrologyPoint[layer == 0 || layer == count - 1 ? 1 : 1 + random.nextInt(4)];
|
||||
for (int candidate = 0; candidate < points[layer].length; candidate++) {
|
||||
points[layer][candidate] = new HydrologyPoint(layer * 12 + random.nextInt(5), 80,
|
||||
candidate * 9 + random.nextInt(7));
|
||||
}
|
||||
}
|
||||
double maximumTurn = scenario % 2 == 0 ? 35D : 115D;
|
||||
double[][][][] cache = new double[count][][][];
|
||||
for (int refinement = 0; refinement < 8; refinement++) {
|
||||
Fixture fixture = fixture(points, random);
|
||||
assertArrayEquals("scenario=" + scenario + " refinement=" + refinement,
|
||||
exhaustive(fixture, maximumTurn), solver.solve(fixture, maximumTurn, cache));
|
||||
assertCacheGeometry(fixture, maximumTurn, cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoLayerRefinementResetsReusedCostsAndKeepsFiniteRoutes() throws Exception {
|
||||
Random random = new Random(173L);
|
||||
HydrologyPoint[][] points = {
|
||||
{new HydrologyPoint(0, 80, 0), new HydrologyPoint(0, 80, 4)},
|
||||
{new HydrologyPoint(12, 80, 0), new HydrologyPoint(12, 80, 4)}
|
||||
};
|
||||
Solver solver = new Solver();
|
||||
double[][][][] cache = new double[2][][][];
|
||||
for (int refinement = 0; refinement < 32; refinement++) {
|
||||
Fixture fixture = fixture(points, random);
|
||||
assertArrayEquals(exhaustive(fixture, 75D), solver.solve(fixture, 75D, cache));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void straightTiesAndDegenerateSegmentsKeepOriginalFirstChoice() throws Exception {
|
||||
HydrologyPoint[][] points = {
|
||||
{new HydrologyPoint(0, 80, 0)},
|
||||
{new HydrologyPoint(0, 90, 0), new HydrologyPoint(0, 70, 0)},
|
||||
{new HydrologyPoint(12, 80, 0), new HydrologyPoint(12, 80, 0)},
|
||||
{new HydrologyPoint(24, 80, 0)}
|
||||
};
|
||||
Fixture fixture = fixture(points, new Random(2L));
|
||||
for (double[] scores : fixture.scores()) {
|
||||
Arrays.fill(scores, 0D);
|
||||
}
|
||||
for (double[] penalties : fixture.penalties()) {
|
||||
Arrays.fill(penalties, 0D);
|
||||
}
|
||||
for (double[][] transitions : fixture.transitions()) {
|
||||
if (transitions != null) {
|
||||
for (double[] row : transitions) {
|
||||
Arrays.fill(row, 0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
assertArrayEquals(new int[]{0, 0, 0, 0}, new Solver().solve(fixture, 75D, new double[4][][][]));
|
||||
}
|
||||
|
||||
private static Fixture fixture(HydrologyPoint[][] coordinates, Random random) {
|
||||
int count = coordinates.length;
|
||||
HydrologyPoint[][] points = new HydrologyPoint[count][];
|
||||
double[][] scores = new double[count][];
|
||||
double[][] penalties = new double[count][];
|
||||
double[][][] transitions = new double[count][][];
|
||||
for (int layer = 0; layer < count; layer++) {
|
||||
int size = coordinates[layer].length;
|
||||
points[layer] = new HydrologyPoint[size];
|
||||
scores[layer] = new double[size];
|
||||
penalties[layer] = new double[size];
|
||||
for (int candidate = 0; candidate < size; candidate++) {
|
||||
HydrologyPoint point = coordinates[layer][candidate];
|
||||
points[layer][candidate] = new HydrologyPoint(point.x(), random.nextInt(200), point.z());
|
||||
scores[layer][candidate] = random.nextDouble() * 20D;
|
||||
penalties[layer][candidate] = random.nextDouble() * 10D;
|
||||
}
|
||||
if (layer > 0) {
|
||||
transitions[layer] = new double[coordinates[layer - 1].length][size];
|
||||
for (double[] row : transitions[layer]) {
|
||||
for (int current = 0; current < size; current++) {
|
||||
row[current] = random.nextInt(8) == 0 ? Double.POSITIVE_INFINITY : random.nextDouble() * 30D;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Fixture(points, scores, penalties, transitions);
|
||||
}
|
||||
|
||||
private static int[] exhaustive(Fixture fixture, double maximumTurn) {
|
||||
ArrayList<int[]> routes = new ArrayList<>();
|
||||
enumerate(fixture.points(), new int[fixture.points().length], 0, routes);
|
||||
int[] selected = new int[0];
|
||||
double selectedCost = Double.POSITIVE_INFINITY;
|
||||
for (int[] route : routes) {
|
||||
double cost = fixture.scores()[0][route[0]] + fixture.scores()[1][route[1]]
|
||||
+ fixture.penalties()[1][route[1]] + fixture.transitions()[1][route[0]][route[1]];
|
||||
for (int layer = 2; layer < route.length && Double.isFinite(cost); layer++) {
|
||||
double turn = turn(fixture.points()[layer - 2][route[layer - 2]],
|
||||
fixture.points()[layer - 1][route[layer - 1]], fixture.points()[layer][route[layer]]);
|
||||
if (turn > maximumTurn) {
|
||||
cost = Double.POSITIVE_INFINITY;
|
||||
break;
|
||||
}
|
||||
double excess = Math.max(0D, turn - 8D);
|
||||
cost = cost + fixture.scores()[layer][route[layer]] + fixture.penalties()[layer][route[layer]]
|
||||
+ fixture.transitions()[layer][route[layer - 1]][route[layer]]
|
||||
+ excess * excess * 12D / maximumTurn;
|
||||
}
|
||||
if (cost < selectedCost || Double.isFinite(cost) && cost == selectedCost && precedes(route, selected)) {
|
||||
selectedCost = cost;
|
||||
selected = route;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static boolean precedes(int[] candidate, int[] selected) {
|
||||
int last = candidate.length - 1;
|
||||
if (candidate[last - 1] != selected[last - 1]) {
|
||||
return candidate[last - 1] < selected[last - 1];
|
||||
}
|
||||
if (candidate[last] != selected[last]) {
|
||||
return candidate[last] < selected[last];
|
||||
}
|
||||
for (int index = last - 2; index >= 0; index--) {
|
||||
if (candidate[index] != selected[index]) {
|
||||
return candidate[index] < selected[index];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void enumerate(HydrologyPoint[][] points, int[] route, int layer, List<int[]> routes) {
|
||||
if (layer == route.length) {
|
||||
routes.add(route.clone());
|
||||
return;
|
||||
}
|
||||
for (int candidate = 0; candidate < points[layer].length; candidate++) {
|
||||
route[layer] = candidate;
|
||||
enumerate(points, route, layer + 1, routes);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertCacheGeometry(Fixture fixture, double maximumTurn, double[][][][] cache) {
|
||||
int populated = 0;
|
||||
for (int layer = 2; layer < cache.length; layer++) {
|
||||
for (int previous = 0; previous < cache[layer].length; previous++) {
|
||||
for (int current = 0; current < cache[layer][previous].length; current++) {
|
||||
double[] costs = cache[layer][previous][current];
|
||||
if (costs == null) {
|
||||
continue;
|
||||
}
|
||||
for (int before = 0; before < costs.length; before++) {
|
||||
if (Double.isNaN(costs[before])) {
|
||||
continue;
|
||||
}
|
||||
double turn = turn(fixture.points()[layer - 2][before],
|
||||
fixture.points()[layer - 1][previous], fixture.points()[layer][current]);
|
||||
double excess = Math.max(0D, turn - 8D);
|
||||
double expected = turn > maximumTurn ? Double.POSITIVE_INFINITY : excess * excess * 12D / maximumTurn;
|
||||
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(costs[before]));
|
||||
populated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue(populated > 0);
|
||||
}
|
||||
|
||||
private static double turn(HydrologyPoint before, HydrologyPoint previous, HydrologyPoint current) {
|
||||
double incomingX = previous.x() - before.x();
|
||||
double incomingZ = previous.z() - before.z();
|
||||
double outgoingX = current.x() - previous.x();
|
||||
double outgoingZ = current.z() - previous.z();
|
||||
double incomingLength = StrictMath.hypot(incomingX, incomingZ);
|
||||
double outgoingLength = StrictMath.hypot(outgoingX, outgoingZ);
|
||||
if (incomingLength == 0D || outgoingLength == 0D) {
|
||||
return 0D;
|
||||
}
|
||||
return StrictMath.toDegrees(StrictMath.acos(Math.max(-1D, Math.min(1D,
|
||||
(incomingX * outgoingX + incomingZ * outgoingZ) / (incomingLength * outgoingLength)))));
|
||||
}
|
||||
|
||||
private record Fixture(HydrologyPoint[][] points, double[][] scores, double[][] penalties, double[][][] transitions) {
|
||||
}
|
||||
|
||||
private static final class Solver {
|
||||
private final HydrologyRouteGeometry geometry = mock(HydrologyRouteGeometry.class, CALLS_REAL_METHODS);
|
||||
private final Constructor<?> candidateConstructor;
|
||||
private final Object tangent;
|
||||
private final Method solve;
|
||||
private final Method indices;
|
||||
private HydrologyRouteGeometry.CurvatureWorkspace workspace;
|
||||
|
||||
private Solver() throws Exception {
|
||||
Class<?> direction = RouteDirection.class;
|
||||
Constructor<?> directionConstructor = direction.getDeclaredConstructor(double.class, double.class);
|
||||
directionConstructor.setAccessible(true);
|
||||
tangent = directionConstructor.newInstance(1D, 0D);
|
||||
Class<?> candidate = RouteCandidate.class;
|
||||
candidateConstructor = candidate.getDeclaredConstructor(HydrologyPoint.class, double.class, double.class,
|
||||
double.class, double.class, double.class, direction, boolean.class);
|
||||
candidateConstructor.setAccessible(true);
|
||||
solve = HydrologyRouteGeometry.class.getDeclaredMethod("selectCurvatureAwareTerrainRoute", List.class,
|
||||
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class,
|
||||
HydrologyRouteGeometry.CurvatureWorkspace.class);
|
||||
solve.setAccessible(true);
|
||||
indices = solve.getReturnType().getDeclaredMethod("indices");
|
||||
indices.setAccessible(true);
|
||||
}
|
||||
|
||||
private int[] solve(Fixture fixture, double maximumTurn, double[][][][] cache) throws Exception {
|
||||
ArrayList<List<Object>> layers = new ArrayList<>();
|
||||
for (int layer = 0; layer < fixture.points().length; layer++) {
|
||||
ArrayList<Object> candidates = new ArrayList<>();
|
||||
for (int candidate = 0; candidate < fixture.points()[layer].length; candidate++) {
|
||||
HydrologyPoint point = fixture.points()[layer][candidate];
|
||||
candidates.add(candidateConstructor.newInstance(point, point.x(), point.z(), 0D,
|
||||
fixture.scores()[layer][candidate], 0D, tangent, true));
|
||||
}
|
||||
layers.add(candidates);
|
||||
}
|
||||
if (workspace == null) {
|
||||
workspace = new HydrologyRouteGeometry.CurvatureWorkspace(layers);
|
||||
}
|
||||
return (int[]) indices.invoke(solve.invoke(geometry, layers, 12D, maximumTurn, 12D,
|
||||
fixture.penalties(), fixture.transitions(), cache, workspace));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -259,7 +259,7 @@ public class HydrologySourceSpacingTest {
|
||||
candidates.add(new OutletCandidate(node.index(), -1, new RiverOutlet(node.index() + 1000L,
|
||||
HydrologyFeatureType.MOUTH, node.id(), point, point, 63, true)));
|
||||
}
|
||||
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false);
|
||||
return new HydrologyRoutingPlan(potential, parent, outlets, lengths, List.copyOf(candidates), false, null);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample land(int naturalHeight, double weight) {
|
||||
|
||||
+1
@@ -64,6 +64,7 @@ public class HydrologyStyledGeometryTest {
|
||||
Set.of(
|
||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
|
||||
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
|
||||
HydrologyGeometrySampler.Field.SURFACE_BLEND_WIDTH,
|
||||
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
||||
HydrologyGeometrySampler.Field.UNDERGROUND_WIDTH,
|
||||
HydrologyGeometrySampler.Field.UNDERGROUND_DEPTH,
|
||||
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.NavigableSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologySurfaceBankProbeTest {
|
||||
@Test
|
||||
public void raisingShoreWidthWidensTheBankProbeBandInsteadOfSlidingIt() {
|
||||
NavigableSet<Integer> narrow = probedBankOffsets(1.5D);
|
||||
NavigableSet<Integer> wide = probedBankOffsets(3D);
|
||||
int innerRing = innerRing();
|
||||
|
||||
assertTrue("narrow band " + narrow, narrow.contains(innerRing));
|
||||
assertTrue("wide band " + wide, wide.contains(innerRing));
|
||||
assertTrue("band " + wide + " must still cover " + narrow, wide.containsAll(narrow));
|
||||
assertTrue("band " + wide + " must reach past " + narrow, wide.size() > narrow.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void theInnerProbeRingDoesNotMoveWithShoreWidth() {
|
||||
int innerRing = innerRing();
|
||||
for (double shoreWidth : new double[]{0D, 1.5D, 3D, 16D}) {
|
||||
NavigableSet<Integer> band = probedBankOffsets(shoreWidth);
|
||||
assertEquals("band " + band + " at shoreWidth " + shoreWidth,
|
||||
innerRing, band.first().intValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bankHeightQueriesPreserveExactPenaltiesWithoutBankPolicySamples() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x + z == 7 ? null
|
||||
: x < -3 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||
: HydrologyTerrainSample.openLand(80 + Math.floorMod(x * 3 + z * 7, 31), 0D, "land");
|
||||
AtomicInteger basisCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
HydrologyNaturalTerrainSampler natural = new HydrologyNaturalTerrainSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int x, int z) {
|
||||
basisCalls.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double sampleLandHeight(int x, int z) {
|
||||
heightCalls.incrementAndGet();
|
||||
HydrologyTerrainSample sampled = terrain.sample(x, z);
|
||||
return sampled == null || sampled.ocean() ? Double.NaN : sampled.naturalHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
throw new AssertionError("Bank probes do not require a routing grid");
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int x, int z) {
|
||||
throw new AssertionError("Bank probes only require land heights");
|
||||
}
|
||||
};
|
||||
for (double angle : new double[]{0D, 0.37D, 0.7853981633974483D}) {
|
||||
HydrologyPlannerSettings settings = settings(3D);
|
||||
HydrologyPlanner reference = new HydrologyPlanner(91L, settings, terrain);
|
||||
HydrologyPlanner optimized = new HydrologyPlanner(91L, settings, terrain, natural,
|
||||
HydrologyGeometrySampler.deterministic(terrain), -4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
|
||||
HydrologyPoint point = new HydrologyPoint(0, 80, 0);
|
||||
RouteCandidate candidate = new RouteCandidate(point, 0D, 0D, 0D, 0D, 0D,
|
||||
new RouteDirection(StrictMath.cos(angle), StrictMath.sin(angle)), true);
|
||||
basisCalls.set(0);
|
||||
heightCalls.set(0);
|
||||
double expected = reference.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate);
|
||||
double actual = optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate);
|
||||
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(actual));
|
||||
assertEquals(1, basisCalls.get());
|
||||
assertTrue(heightCalls.get() > 0);
|
||||
optimized.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
assertEquals(expected, optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate), 0D);
|
||||
int firstPassHeightCalls = heightCalls.get();
|
||||
assertEquals(expected, optimized.surfaceCourses.surfaceRouteCandidateBankPenalty(candidate), 0D);
|
||||
assertEquals(firstPassHeightCalls, heightCalls.get());
|
||||
} finally {
|
||||
optimized.planningSamples.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** One block outside half the nominal maximum width - see HydrologySurfaceCoursePlanner. */
|
||||
private int innerRing() {
|
||||
return (int) StrictMath.ceil(
|
||||
HydrologyPlannerSettings.defaults().surface().maximumWidth() / 2D) + 1;
|
||||
}
|
||||
|
||||
private NavigableSet<Integer> probedBankOffsets(double shoreWidth) {
|
||||
HydrologyPoint center = new HydrologyPoint(0, 84, 0);
|
||||
NavigableSet<Integer> probed = new TreeSet<>();
|
||||
HydrologyTerrainSampler sampler = (x, z) -> {
|
||||
if (x == center.x()) {
|
||||
probed.add(Math.abs(z - center.z()));
|
||||
}
|
||||
return HydrologyTerrainSample.openLand(84, 0D, "land");
|
||||
};
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, settings(shoreWidth), sampler);
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
|
||||
planner.surfaceCourses.surfaceRouteCandidateBankPenalty(new RouteCandidate(
|
||||
center, center.x(), center.z(), 0D, 0D, 0D, new RouteDirection(1D, 0D), true));
|
||||
|
||||
probed.remove(0);
|
||||
return probed;
|
||||
}
|
||||
|
||||
private HydrologyPlannerSettings settings(double shoreWidth) {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Surface surface = base.surface();
|
||||
return new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
base.routing(),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
surface.enabled(),
|
||||
surface.sources(),
|
||||
surface.minimumWidth(),
|
||||
surface.maximumWidth(),
|
||||
surface.minimumDepth(),
|
||||
surface.maximumDepth(),
|
||||
surface.maximumIncision(),
|
||||
shoreWidth,
|
||||
surface.banks()),
|
||||
base.hydraulics(),
|
||||
base.underground(),
|
||||
base.outlets(),
|
||||
base.geometry(),
|
||||
base.deepFluids(),
|
||||
base.surfacePools(),
|
||||
base.widestShoreBiomeWidth(),
|
||||
base.seaCaves(),
|
||||
base.surfacePolicyBounds());
|
||||
}
|
||||
}
|
||||
+70
-1
@@ -51,6 +51,75 @@ public final class HydrologySurfaceDropRasterTest {
|
||||
assertThrows(UnsupportedOperationException.class, () -> raster.columns().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagonalDropCoordinatesResolveExactlyInBothDirectionsFromTheOrigin() {
|
||||
for (int origin : new int[]{-128, 128}) {
|
||||
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
|
||||
90, 80, 4, 3, true, true,
|
||||
List.of(new HydrologyPoint(origin, 90, origin), new HydrologyPoint(origin + 12, 80, origin + 12)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
RiverCourse course = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
|
||||
"water", 1, List.of(), List.of(fall));
|
||||
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, TERRAIN, GEOMETRY, course);
|
||||
|
||||
assertFalse(raster.columns().isEmpty());
|
||||
for (HydrologyColumnSample column : raster.columns()) {
|
||||
assertSame(column, raster.sample(column.x(), column.z()).orElseThrow());
|
||||
}
|
||||
assertTrue(raster.connects(origin, origin, 90));
|
||||
assertTrue(raster.connects(origin + 12, origin + 12, 80));
|
||||
assertTrue(raster.sample(origin + 512, origin + 512).isEmpty());
|
||||
assertFalse(raster.connects(origin + 512, origin + 512, 80));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receivingDropBedsUseInletIncisionOnlyAtAuthorizedCourseStations() {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 200
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(107, 0D, "parent");
|
||||
for (int start : new int[]{50, 100, 150}) {
|
||||
int end = start == 100 ? 110 : start + 1;
|
||||
HydraulicSegment approach = new HydraulicSegment(1L, 1L, HydrologyFeatureType.SURFACE_POOL,
|
||||
122, 122, 4, 3, false, false,
|
||||
List.of(new HydrologyPoint(0, 122, 0), new HydrologyPoint(start, 122, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
|
||||
122, 78, 4, 3, true, true,
|
||||
List.of(new HydrologyPoint(start, 122, 0), new HydrologyPoint(end, 78, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
HydraulicSegment downstream = new HydraulicSegment(3L, 1L, HydrologyFeatureType.CASCADE,
|
||||
78, SETTINGS.seaLevel(), 4, 3, false, false,
|
||||
List.of(new HydrologyPoint(end, 78, 0), new HydrologyPoint(199, SETTINGS.seaLevel(), 0)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
HydraulicSegment mouth = new HydraulicSegment(4L, 1L, HydrologyFeatureType.MOUTH,
|
||||
SETTINGS.seaLevel(), SETTINGS.seaLevel(), 4, 3, false, false,
|
||||
List.of(new HydrologyPoint(199, SETTINGS.seaLevel(), 0), new HydrologyPoint(200, SETTINGS.seaLevel(), 0)),
|
||||
HydraulicChannelProfile.uniform(4, 3));
|
||||
RiverCourse course = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(3L), OptionalLong.of(4L),
|
||||
"water", 1, List.of(), List.of(approach, fall, downstream, mouth));
|
||||
HydrologySurfaceDropRaster raster = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, GEOMETRY, course);
|
||||
|
||||
assertEquals(start != 50, raster.connects(end, 0, 78));
|
||||
if (start != 50) {
|
||||
HydrologyColumnSample receiver = raster.sample(end, 0).orElseThrow();
|
||||
HydrologyColumnLayer water = receiver.layers().stream()
|
||||
.filter(layer -> layer.channel() && layer.fluidOwned()).findFirst().orElseThrow();
|
||||
int cut = receiver.naturalHeight() - water.bedY();
|
||||
assertTrue(cut > SETTINGS.surface().maximumIncision());
|
||||
assertTrue(cut <= SETTINGS.surface().banks().inlet().maximumIncision());
|
||||
}
|
||||
if (start == 100) {
|
||||
assertFalse(raster.connects(102, 0, 78));
|
||||
assertTrue(raster.connects(108, 0, 78));
|
||||
}
|
||||
SurfaceBounds bounds = new SurfaceBounds(start - 2, -8, end + 2, 8);
|
||||
HydrologySurfaceDropRaster bounded = HydrologySurfaceDropRaster.compile(SETTINGS, terrain, GEOMETRY, course, bounds);
|
||||
assertEquals(raster.columns().stream().filter(column -> bounds.contains(column.x(), column.z())).toList(),
|
||||
bounded.columns());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceDropsDoNotOwnSubmergedGroundOrRaiseWaterOnSeaLevelLand() {
|
||||
RiverCourse course = course(true);
|
||||
@@ -97,7 +166,7 @@ public final class HydrologySurfaceDropRasterTest {
|
||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
|
||||
HydrologyPlannerSettings.Excavation limits = SETTINGS.surface().banks().erosion().excavation();
|
||||
FootprintLayerShape shape = new FootprintLayerShape(2, 2D, 40D, 79, 90, 90,
|
||||
false, false, false, false, true);
|
||||
false, false, false, false, true, false);
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(5L, segment.type(), course.id(), segment.id(),
|
||||
0, 90, 0, 1, 0, false);
|
||||
HydrologyTerrainSample highTerrain = HydrologyTerrainSample.openLand(160, 0D, "parent");
|
||||
|
||||
+2
-87
@@ -5,54 +5,9 @@ import org.junit.Test;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class HydrologySurfaceRouteGeometryTest {
|
||||
@Test
|
||||
public void lazyTransitionGeometryIsReusedUntilResolvedHeightsChange() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
|
||||
(x, z) -> {
|
||||
throw new AssertionError("Geometry lookup must not sample terrain.");
|
||||
});
|
||||
RouteCandidate previous = candidate(0D, 0D);
|
||||
RouteCandidate current = spy(candidate(4D, 0D));
|
||||
HydrologyRouteGeometry.CurvatureWorkspace workspace = new HydrologyRouteGeometry.CurvatureWorkspace(
|
||||
List.of(List.of(previous), List.of(current)));
|
||||
double expected = planner.routePaths.routeTransitionGeometryCost(previous, current, 4D);
|
||||
clearInvocations(current);
|
||||
|
||||
for (int iteration = 0; iteration < 8; iteration++) {
|
||||
double actual = planner.routeGeometry.resolvedTransitionCost(
|
||||
previous, current, 4D, Double.NaN, workspace, 1, 0, 0);
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
verify(current, times(1)).offset();
|
||||
|
||||
RouteCandidate raised = spy(new RouteCandidate(new HydrologyPoint(4, 91, 0),
|
||||
current.continuousX(), current.continuousZ(), current.offset(), current.localScore(),
|
||||
current.terrainScore(), current.tangent(), true));
|
||||
double raisedExpected = planner.routePaths.routeTransitionGeometryCost(previous, raised, 4D);
|
||||
assertTrue(raisedExpected > expected);
|
||||
clearInvocations(raised);
|
||||
for (int iteration = 0; iteration < 8; iteration++) {
|
||||
double actual = planner.routeGeometry.resolvedTransitionCost(
|
||||
previous, raised, 4D, Double.NaN, workspace, 1, 0, 0);
|
||||
assertEquals(Double.doubleToRawLongBits(raisedExpected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
verify(raised, times(1)).offset();
|
||||
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(
|
||||
planner.routeGeometry.resolvedTransitionCost(previous, raised, 4D, -0D,
|
||||
workspace, 1, 0, 0)));
|
||||
assertEquals(Double.POSITIVE_INFINITY,
|
||||
planner.routeGeometry.resolvedTransitionCost(previous, raised, 4D,
|
||||
Double.POSITIVE_INFINITY, workspace, 1, 0, 0), 0D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void surfaceAnchorKeepsTheCoarseValleyWhenSmoothGroundIsMuchHigher() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
@@ -85,7 +40,7 @@ public class HydrologySurfaceRouteGeometryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void steepSurfaceEdgesCanDetourAroundAContainedTerrainPit() {
|
||||
public void undergroundEdgesRejectAContainedTerrainPit() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
||||
128 - Math.floorDiv(x, 2) - (x >= 30 && x <= 34 && Math.abs(z) <= 1 ? 14 : 0),
|
||||
0D, "land");
|
||||
@@ -94,47 +49,7 @@ public class HydrologySurfaceRouteGeometryTest {
|
||||
HydrologyPoint end = new HydrologyPoint(64, 96, 0);
|
||||
HydrologyPoint continuation = new HydrologyPoint(96, 80, 0);
|
||||
|
||||
List<HydrologyPoint> cave = planner.routeGeometry.refineEdge(1L, 2L, start, end, continuation, 4, false);
|
||||
List<HydrologyPoint> surface = planner.routeGeometry.refineEdge(1L, 2L, start, end, continuation, 4, true);
|
||||
|
||||
List<HydrologyPoint> cave = planner.routeGeometry.refineUndergroundEdge(1L, 2L, start, end, continuation, 4);
|
||||
assertTrue(cave.isEmpty());
|
||||
assertFalse(surface.isEmpty());
|
||||
assertTrue(surface.stream().anyMatch(point -> Math.abs(point.z()) > 1));
|
||||
assertTrue(surface.stream().allMatch(point -> Math.abs(point.z()) <= 16));
|
||||
assertTrue(planner.routePaths.traversableRoute(surface));
|
||||
assertFalse(planner.routePaths.containsTerrainPit(surface));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void curvatureUsesTheContinuousCurveBeforeBlockRounding() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(84, 0D, "land"));
|
||||
RouteCandidate first = candidate(0D, 0D);
|
||||
RouteCandidate middle = candidate(3.6D, 0.49D);
|
||||
RouteCandidate last = candidate(7D, 2.1D);
|
||||
|
||||
assertTrue(HydrologyRoutePath.routeTurnDegrees(first.point(), middle.point(), last.point()) > 20D);
|
||||
assertTrue(planner.routeGeometry.continuousTurnDegrees(first, middle, last) < 20D);
|
||||
RouteCandidate[] route = planner.routeGeometry.selectCurvatureAwareTerrainRoute(
|
||||
List.of(List.of(first), List.of(middle), List.of(last)), 4D, 20D, 2.5D);
|
||||
|
||||
assertEquals(3, route.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void continuousCurvatureStillRejectsASharpTurn() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(91L, HydrologyPlannerSettings.defaults(),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(84, 0D, "land"));
|
||||
RouteCandidate[] route = planner.routeGeometry.selectCurvatureAwareTerrainRoute(
|
||||
List.of(List.of(candidate(0D, 0D)), List.of(candidate(4D, 0D)), List.of(candidate(4D, 4D))),
|
||||
4D, 20D, 2.5D);
|
||||
|
||||
assertEquals(0, route.length);
|
||||
}
|
||||
|
||||
private RouteCandidate candidate(double x, double z) {
|
||||
return new RouteCandidate(
|
||||
new HydrologyPoint((int) StrictMath.round(x), 84, (int) StrictMath.round(z)),
|
||||
x, z, 0D, 0D, 0D, new RouteDirection(1D, 0D), true);
|
||||
}
|
||||
}
|
||||
|
||||
+167
-11
@@ -6,6 +6,9 @@ import art.arcane.iris.testsupport.Await;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import org.junit.Test;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
@@ -50,6 +53,55 @@ import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class HydrologyTileCacheTest {
|
||||
@Rule
|
||||
public TemporaryFolder preparedFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void preparedPlansRestoreFarFromOriginWithoutTerrainPlanning() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileCache.SharedCacheScope scope = sharedScope();
|
||||
HydrologyTileKey key = new HydrologyTileKey(501, -703);
|
||||
HydrologyTile tile = preparedTile(key, scope);
|
||||
Path root = preparedFolder.newFolder().toPath();
|
||||
new PreparedHydrologyTileStore(root, scope, 64).save(tile);
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
|
||||
cache.enableSharedCache(scope, root);
|
||||
cache.setTerrainPreparation(() -> { throw new AssertionError("Restoration must not sample terrain"); });
|
||||
HydrologyTile restored = cache.get(key);
|
||||
assertEquals(tile.key(), restored.key());
|
||||
assertEquals(tile.footprint(), restored.footprint());
|
||||
verify(planner, never()).plan(any(HydrologyTileKey.class));
|
||||
verify(planner).reuseResolvedTile(restored);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tileDemandAutomaticallyPersistsTheCompletedPlan() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileCache.SharedCacheScope scope = sharedScope();
|
||||
HydrologyTileKey key = new HydrologyTileKey(125, -141);
|
||||
HydrologyTile tile = preparedTile(key, scope);
|
||||
when(planner.plan(key)).thenReturn(tile);
|
||||
Path root = preparedFolder.newFolder().toPath();
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 64)) {
|
||||
cache.enableSharedCache(scope, root);
|
||||
assertSame(tile, cache.get(key));
|
||||
verify(planner).plan(key);
|
||||
}
|
||||
HydrologyTile persisted = new PreparedHydrologyTileStore(root, scope, 64).load(key).orElseThrow();
|
||||
assertEquals(tile.key(), persisted.key());
|
||||
assertEquals(tile.worldSeed(), persisted.worldSeed());
|
||||
assertEquals(tile.settingsFingerprint(), persisted.settingsFingerprint());
|
||||
assertEquals(tile.footprint(), persisted.footprint());
|
||||
}
|
||||
|
||||
private static HydrologyTile preparedTile(HydrologyTileKey key, HydrologyTileCache.SharedCacheScope scope) {
|
||||
return new HydrologyTile(key, scope.worldSeed(), scope.settingsFingerprint(), 64,
|
||||
List.of(), List.of(), List.of(), List.of(), Set.of(), List.of(), List.of(), RiverFootprint.empty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void weightedTileCapacityStopsSpeculationWithoutBlockingDemand() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
@@ -400,7 +452,7 @@ public class HydrologyTileCacheTest {
|
||||
monitor.set(queue.get(cache));
|
||||
|
||||
if (pregeneration) {
|
||||
cache.preparePregeneration(262144, 262144);
|
||||
cache.preparePregeneration(pregenArea(262144, 262144));
|
||||
} else {
|
||||
cache.prefetchArea(261120, 261120, 263168, 263168, 262144, 262144);
|
||||
}
|
||||
@@ -409,8 +461,12 @@ public class HydrologyTileCacheTest {
|
||||
assertEquals(prefetchRectangle(254, 257, 254, 257), new HashSet<>(planned.subList(0, 16)));
|
||||
assertEquals(List.of(new HydrologyTileKey(255, 255), new HydrologyTileKey(256, 255),
|
||||
new HydrologyTileKey(257, 255)), planned.subList(1, 4));
|
||||
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned.subList(16, planned.size())));
|
||||
assertEquals(22, planned.size());
|
||||
if (pregeneration) {
|
||||
assertEquals(16, planned.size());
|
||||
} else {
|
||||
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned.subList(16, planned.size())));
|
||||
assertEquals(22, planned.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +503,7 @@ public class HydrologyTileCacheTest {
|
||||
Runnable demand = queued.poll(5, TimeUnit.SECONDS);
|
||||
assertNotNull(demand);
|
||||
|
||||
cache.preparePregeneration(262144, 262144);
|
||||
cache.preparePregeneration(pregenArea(262144, 262144));
|
||||
|
||||
assertFalse(active.isDone());
|
||||
assertFalse(demanded.isDone());
|
||||
@@ -485,9 +541,9 @@ public class HydrologyTileCacheTest {
|
||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
|
||||
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
|
||||
cache.preparePregeneration(262144, 262144);
|
||||
cache.preparePregeneration(263168, 262144);
|
||||
cache.preparePregeneration(263168, 262144);
|
||||
cache.preparePregeneration(pregenArea(262144, 262144));
|
||||
cache.preparePregeneration(pregenArea(263168, 262144));
|
||||
cache.preparePregeneration(pregenArea(263168, 262144));
|
||||
drainPrefetchTasks(queued);
|
||||
|
||||
Set<HydrologyTileKey> expected = new HashSet<>(prefetchRectangle(255, 258, 254, 257));
|
||||
@@ -496,7 +552,7 @@ public class HydrologyTileCacheTest {
|
||||
assertEquals(17, planned.size());
|
||||
cache.clear();
|
||||
planned.clear();
|
||||
cache.preparePregeneration(263168, 262144);
|
||||
cache.preparePregeneration(pregenArea(263168, 262144));
|
||||
drainPrefetchTasks(queued);
|
||||
assertEquals(prefetchRectangle(255, 258, 254, 257), new HashSet<>(planned));
|
||||
assertEquals(16, planned.size());
|
||||
@@ -516,13 +572,113 @@ public class HydrologyTileCacheTest {
|
||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
|
||||
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
|
||||
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(Integer.MAX_VALUE, 0));
|
||||
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(0, Integer.MIN_VALUE));
|
||||
assertThrows(IllegalArgumentException.class, () -> cache.preparePregeneration(
|
||||
new HydrologyTileCache.PregenerationArea(0, 0, 1, -1, 2, 1)));
|
||||
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(
|
||||
new HydrologyTileCache.PregenerationArea(0, 0, Long.MIN_VALUE, -1, 1, 1)));
|
||||
drainPrefetchTasks(queued);
|
||||
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned));
|
||||
assertEquals(6, planned.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenBoundsLimitSpeculationWithoutLimitingRequestedChunks() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlannerSettings settings = stalePrefetchSettings();
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
planned.add(invocation.getArgument(0));
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
|
||||
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
|
||||
new HydrologyTileCache.PregenerationArea(0, 0, -1536, -1536, 2047, 2047))) {
|
||||
cache.prefetchArea(-4096, -4096, 4095, 4095, 0, 0);
|
||||
assertEquals(prefetchRectangle(-2, 2, -2, 2), new HashSet<>(planned));
|
||||
planned.clear();
|
||||
HydrologyTileKey demanded = new HydrologyTileKey(8, 8);
|
||||
assertSame(tile, cache.get(demanded));
|
||||
assertEquals(List.of(demanded), planned);
|
||||
planned.clear();
|
||||
assertFalse(cache.isPlanned(10_240, 10_240));
|
||||
assertTrue(cache.isPlanned(10_240, 10_240));
|
||||
assertEquals(prefetchRectangle(9, 10, 9, 10), new HashSet<>(planned));
|
||||
planned.clear();
|
||||
cache.prepareChunkColumns(-10_240, -10_240);
|
||||
assertEquals(prefetchRectangle(-11, -10, -11, -10), new HashSet<>(planned));
|
||||
}
|
||||
planned.clear();
|
||||
cache.prepareChunkColumns(20_480, 20_480);
|
||||
assertEquals(prefetchRectangle(18, 21, 18, 21), new HashSet<>(planned));
|
||||
cache.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenScopesRestoreStudioPrefetchAndIgnoreSupersededClose() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlannerSettings settings = stalePrefetchSettings();
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
planned.add(invocation.getArgument(0));
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
HydrologyTileCache.PregenerationScope first = cache.preparePregeneration(pregenArea(0, 0));
|
||||
HydrologyTileCache.PregenerationScope second = cache.preparePregeneration(pregenArea(262144, 262144));
|
||||
first.close();
|
||||
planned.clear();
|
||||
cache.prefetchArea(16384, 16384, 16384, 16384, 16384, 16384);
|
||||
assertTrue(planned.isEmpty());
|
||||
second.close();
|
||||
cache.prepareChunkColumns(16384, 16384);
|
||||
assertEquals(prefetchRectangle(15, 16, 15, 16), new HashSet<>(planned));
|
||||
HydrologyTileCache.PregenerationScope cleared = cache.preparePregeneration(pregenArea(0, 0));
|
||||
cache.clear();
|
||||
cleared.close();
|
||||
planned.clear();
|
||||
cache.prepareChunkColumns(32768, 32768);
|
||||
assertEquals(prefetchRectangle(31, 32, 31, 32), new HashSet<>(planned));
|
||||
HydrologyTileCache.PregenerationScope closed = cache.preparePregeneration(pregenArea(0, 0));
|
||||
cache.close();
|
||||
closed.close();
|
||||
cache.prefetchArea(0, 0, 0, 0, 0, 0);
|
||||
assertThrows(CancellationException.class, () -> cache.preparePregeneration(pregenArea(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void largePregenBoundsKeepInitialWarmupLocal() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
HydrologyPlannerSettings settings = stalePrefetchSettings();
|
||||
when(planner.settings()).thenReturn(settings);
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
|
||||
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
planned.add(invocation.getArgument(0));
|
||||
return tile;
|
||||
}).when(planner).plan(any(HydrologyTileKey.class));
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
|
||||
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
|
||||
new HydrologyTileCache.PregenerationArea(0, 0,
|
||||
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE))) {
|
||||
assertEquals(prefetchRectangle(-2, 1, -2, 1), new HashSet<>(planned));
|
||||
assertEquals(16, planned.size());
|
||||
}
|
||||
cache.close();
|
||||
}
|
||||
|
||||
private static HydrologyTileCache.PregenerationArea pregenArea(int x, int z) {
|
||||
return new HydrologyTileCache.PregenerationArea(x, z, (long) x - 1024, (long) z - 1024,
|
||||
(long) x + 1024, (long) z + 1024);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings stalePrefetchSettings() {
|
||||
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
|
||||
when(settings.routing()).thenReturn(new HydrologyPlannerSettings.Routing(
|
||||
@@ -1100,7 +1256,7 @@ public class HydrologyTileCacheTest {
|
||||
order.clear();
|
||||
cache.setNeighbourPrefetchEnabled(false);
|
||||
|
||||
cache.preparePregeneration(0, 0);
|
||||
cache.preparePregeneration(pregenArea(0, 0));
|
||||
|
||||
assertEquals(new HydrologyTileKey(1, -1), order.getFirst());
|
||||
assertTrue(order.contains(new HydrologyTileKey(1, 0)));
|
||||
|
||||
+22
-9
@@ -35,14 +35,14 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StudioHydrologyTileStoreTest {
|
||||
public class PreparedHydrologyTileStoreTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void roundTripsValidatedEntryTile() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
StudioHydrologyTileStore store = store();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
|
||||
store.save(original);
|
||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||
@@ -93,7 +93,7 @@ public class StudioHydrologyTileStoreTest {
|
||||
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 91L, 17L, 64,
|
||||
List.of(), List.of(), List.of(), List.of(course), Set.of(), List.of(plan), List.of(),
|
||||
new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), column)));
|
||||
StudioHydrologyTileStore store = store();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||
assertEquals(original, restored);
|
||||
@@ -118,7 +118,7 @@ public class StudioHydrologyTileStoreTest {
|
||||
@Test
|
||||
public void entryRequiresRegionalOwnershipMetadata() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
StudioHydrologyTileStore store = store();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
Path file = store.file(original.key());
|
||||
JsonObject persisted;
|
||||
@@ -136,7 +136,7 @@ public class StudioHydrologyTileStoreTest {
|
||||
@Test
|
||||
public void corruptEntryFallsBackToCacheMiss() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
StudioHydrologyTileStore store = store();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
Path file = store.file(original.key());
|
||||
Files.writeString(file, "invalid", StandardCharsets.UTF_8);
|
||||
@@ -149,15 +149,28 @@ public class StudioHydrologyTileStoreTest {
|
||||
Path root = temporaryFolder.newFolder().toPath();
|
||||
HydrologyTileCache.SharedCacheScope scope = scope();
|
||||
HydrologyTile original = tile();
|
||||
StudioHydrologyTileStore matching = new StudioHydrologyTileStore(root, scope, 64);
|
||||
PreparedHydrologyTileStore matching = new PreparedHydrologyTileStore(root, scope, 64);
|
||||
matching.save(original);
|
||||
StudioHydrologyTileStore mismatched = new StudioHydrologyTileStore(root, scope, 128);
|
||||
PreparedHydrologyTileStore mismatched = new PreparedHydrologyTileStore(root, scope, 128);
|
||||
|
||||
assertTrue(mismatched.load(original.key()).isEmpty());
|
||||
}
|
||||
|
||||
private StudioHydrologyTileStore store() throws Exception {
|
||||
return new StudioHydrologyTileStore(
|
||||
@Test
|
||||
public void copiedTileCannotCrossRuntimeIdentityScopes() throws Exception {
|
||||
Path root = temporaryFolder.newFolder().toPath();
|
||||
PreparedHydrologyTileStore source = new PreparedHydrologyTileStore(root, scope(), 64);
|
||||
PreparedHydrologyTileStore target = new PreparedHydrologyTileStore(root,
|
||||
new HydrologyTileCache.SharedCacheScope("another-kernel", 91L, 384, "overworld", 17L), 64);
|
||||
HydrologyTile tile = tile();
|
||||
source.save(tile);
|
||||
Files.createDirectories(target.file(tile.key()).getParent());
|
||||
Files.copy(source.file(tile.key()), target.file(tile.key()));
|
||||
assertTrue(target.load(tile.key()).isEmpty());
|
||||
}
|
||||
|
||||
private PreparedHydrologyTileStore store() throws Exception {
|
||||
return new PreparedHydrologyTileStore(
|
||||
temporaryFolder.newFolder().toPath(),
|
||||
scope(),
|
||||
64
|
||||
+193
-9
@@ -13,9 +13,11 @@ import art.arcane.iris.generation.hydrology.IrisRiverBedProfile;
|
||||
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -31,6 +33,89 @@ public class ErosionFieldCompilerTest {
|
||||
default -> request.minimum();
|
||||
};
|
||||
|
||||
@Test
|
||||
public void geometricExclusionAvoidsTerrainSamplesWithoutChangingAsymmetricBanks() {
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
|
||||
AtomicInteger referenceSamples = new AtomicInteger();
|
||||
AtomicInteger boundedSamples = new AtomicInteger();
|
||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(z > 4 ? 100 : 80, 0D, "land");
|
||||
ErosionField reference = rasterWithShoreBound(surface, (x, z) -> {
|
||||
referenceSamples.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
}, 128D, null);
|
||||
ErosionField bounded = rasterWithShoreBound(surface, (x, z) -> {
|
||||
boundedSamples.incrementAndGet();
|
||||
return terrain.sample(x, z);
|
||||
}, surface.shoreWidth(), null);
|
||||
|
||||
assertEqualFields(reference, bounded);
|
||||
assertNull(bounded.rejection());
|
||||
assertTrue("sample counts " + boundedSamples.get() + "/" + referenceSamples.get(),
|
||||
boundedSamples.get() < referenceSamples.get() * 0.8D);
|
||||
assertNotNull(bounded.column(0, 18));
|
||||
assertNull(bounded.column(0, -18));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void geometricExclusionRetainsPolicyBandsAtExactAndAdjacentThresholds() {
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
|
||||
for (double width : new double[]{Math.nextDown(1.75D), 1.75D, Math.nextUp(1.75D), 12D}) {
|
||||
HydrologyTerrainSampler terrain = (x, z) -> land(80, z >= 0 ? width : 0D)
|
||||
.withShoreWidth(z < 0 ? width : surface.shoreWidth());
|
||||
double bound = Math.max(surface.shoreWidth(), width);
|
||||
ErosionField reference = rasterWithShoreBound(surface, terrain, 128D, null);
|
||||
ErosionField complete = rasterWithShoreBound(surface, terrain, bound, null);
|
||||
assertEqualFields(reference, complete);
|
||||
for (SurfaceBounds bounds : List.of(new SurfaceBounds(-16, -16, -1, 15),
|
||||
new SurfaceBounds(0, -16, 15, 15))) {
|
||||
ErosionField bounded = rasterWithShoreBound(surface, terrain, bound, bounds);
|
||||
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
|
||||
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
|
||||
assertEquals("policy width " + width + " at " + x + "," + z,
|
||||
complete.column(x, z), bounded.column(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (width >= 1.75D) {
|
||||
assertEquals(SurfaceRole.SHORE, complete.column(0, 5).role());
|
||||
}
|
||||
if (width == 12D) {
|
||||
assertEquals(SurfaceRole.SHORE, complete.column(0, 15).role());
|
||||
assertEquals(SurfaceRole.BANK, complete.column(0, -15).role());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ErosionField rasterWithShoreBound(HydrologyPlannerSettings.Surface surface,
|
||||
HydrologyTerrainSampler terrain, double bound,
|
||||
SurfaceBounds bounds) {
|
||||
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(SEA_LEVEL, defaults.routing(), surface,
|
||||
defaults.hydraulics(), defaults.underground(), defaults.outlets(), defaults.geometry(),
|
||||
defaults.deepFluids(), defaults.surfacePools(), bound, defaults.seaCaves(), defaults.surfacePolicyBounds());
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
|
||||
new HydrologyPoint(-48, 80, 0), new HydrologyPoint(48, 80, 0)));
|
||||
double[] widths = new double[centerline.size()];
|
||||
double[] depths = new double[centerline.size()];
|
||||
double[] multipliers = new double[centerline.size()];
|
||||
int[] heads = new int[centerline.size()];
|
||||
Arrays.fill(widths, 3D);
|
||||
Arrays.fill(depths, 2D);
|
||||
Arrays.fill(multipliers, 1D);
|
||||
Arrays.fill(heads, 80);
|
||||
return new ErosionFieldCompiler(settings, terrain).compile(42L, centerline,
|
||||
new ChannelProfile(widths, depths, multipliers), ValleyProfile.fromHeads(heads, heads.length),
|
||||
SurfaceTerminal.SINKHOLE, 0, noPonds(), SurfaceRasterContext.bounded(bounds));
|
||||
}
|
||||
|
||||
private static void assertEqualFields(ErosionField expected, ErosionField actual) {
|
||||
assertEquals(expected.columns(), actual.columns());
|
||||
assertEquals(expected.rejection(), actual.rejection());
|
||||
assertEquals(expected.rejectionDetail(), actual.rejectionDetail());
|
||||
assertEquals(expected.uncontainedWetCells(), actual.uncontainedWetCells());
|
||||
assertEquals(expected.bankExcavation(), actual.bankExcavation());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mouthCutsDrySeaLevelSillBeforeReachingOceanWater() {
|
||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 100
|
||||
@@ -81,7 +166,7 @@ public class ErosionFieldCompilerTest {
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(new HydrologyPoint(-150, 0, 0), new HydrologyPoint(150, 0, 0)));
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, hillside, CONSTANT_GEOMETRY).build(centerline, "water", false);
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, hillside, SEA_LEVEL, 64).solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
|
||||
ErosionFieldCompiler compiler = new ErosionFieldCompiler(surface, hillside, SEA_LEVEL);
|
||||
ErosionFieldCompiler compiler = new ErosionFieldCompiler(rasterSettings(surface), hillside);
|
||||
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8);
|
||||
SurfaceBounds bounds = new SurfaceBounds(-16, -16, 15, 15);
|
||||
ErosionField bounded = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8, surface.banks().ponds(), SurfaceRasterContext.bounded(bounds));
|
||||
@@ -93,6 +178,44 @@ public class ErosionFieldCompilerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedRasterRaisesBanksFromHigherWaterAcrossTheChunkEdge() {
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
|
||||
HydrologyTerrainSampler lowGround = (x, z) -> HydrologyTerrainSample.openLand(77, 0D, "land");
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(
|
||||
List.of(new HydrologyPoint(-48, 80, 0), new HydrologyPoint(48, 78, 0)));
|
||||
int count = centerline.size();
|
||||
double[] widths = new double[count];
|
||||
double[] depths = new double[count];
|
||||
double[] multipliers = new double[count];
|
||||
int[] heads = new int[count];
|
||||
Arrays.fill(widths, 3D);
|
||||
Arrays.fill(depths, 2D);
|
||||
Arrays.fill(multipliers, 1D);
|
||||
for (int station = 0; station < count; station++) {
|
||||
heads[station] = centerline.x()[station] < 0 ? 80 : 78;
|
||||
}
|
||||
ChannelProfile channel = new ChannelProfile(widths, depths, multipliers);
|
||||
ValleyProfile valley = ValleyProfile.fromHeads(heads, count);
|
||||
ErosionFieldCompiler compiler = new ErosionFieldCompiler(rasterSettings(surface), lowGround);
|
||||
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 0);
|
||||
|
||||
assertNull(complete.rejection());
|
||||
assertEquals(80, complete.column(0, 4).height());
|
||||
for (SurfaceBounds bounds : List.of(new SurfaceBounds(-16, -16, -1, 15),
|
||||
new SurfaceBounds(0, -16, 15, 15), new SurfaceBounds(0, 4, 15, 19))) {
|
||||
ErosionField bounded = compiler.compile(42L, centerline, channel, valley,
|
||||
SurfaceTerminal.SINKHOLE, 0, noPonds(), SurfaceRasterContext.bounded(bounds));
|
||||
assertNull(bounded.rejection());
|
||||
assertEquals(0, bounded.uncontainedWetCells());
|
||||
for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) {
|
||||
for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) {
|
||||
assertEquals("bounded column " + x + "," + z, complete.column(x, z), bounded.column(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
|
||||
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
@@ -320,6 +443,12 @@ public class ErosionFieldCompilerTest {
|
||||
surface.minimumDepth(), surface.maximumDepth(), surface.maximumIncision(), surface.shoreWidth(), banks);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings.Surface withMaximumIncision(HydrologyPlannerSettings.Surface surface, int maximumIncision) {
|
||||
return new HydrologyPlannerSettings.Surface(
|
||||
surface.enabled(), surface.sources(), surface.minimumWidth(), surface.maximumWidth(),
|
||||
surface.minimumDepth(), surface.maximumDepth(), maximumIncision, surface.shoreWidth(), surface.banks());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bankColumnsBeyondTheBlendAreNotWritten() {
|
||||
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||
@@ -333,6 +462,14 @@ public class ErosionFieldCompilerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertBoundedBankFill(Compiled compiled, int maximumFill) {
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
int fill = column.height() - column.terrain().naturalHeight();
|
||||
assertTrue(column.x() + "," + column.z() + " fill " + fill,
|
||||
fill <= (column.role() == SurfaceRole.CHANNEL ? 0 : maximumFill));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertChannelContained(Compiled compiled) {
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
if (column.role() != SurfaceRole.CHANNEL || column.apron()) {
|
||||
@@ -376,7 +513,7 @@ public class ErosionFieldCompilerTest {
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
||||
.solve(centerline, channel, terminal, terminalHead);
|
||||
assertNull(valley.rejection());
|
||||
ErosionField field = new ErosionFieldCompiler(surface, sampler, SEA_LEVEL)
|
||||
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), sampler)
|
||||
.compile(1234L, centerline, channel, valley, terminal, 8);
|
||||
return new Compiled(field, valley);
|
||||
}
|
||||
@@ -624,7 +761,7 @@ public class ErosionFieldCompilerTest {
|
||||
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||
assertNull(valley.rejection());
|
||||
|
||||
ErosionField field = new ErosionFieldCompiler(surface, sampler, SEA_LEVEL)
|
||||
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), sampler)
|
||||
.compile(99L, centerline, channel, valley, SurfaceTerminal.OCEAN_MOUTH, 3);
|
||||
|
||||
int firstApron = Integer.MAX_VALUE;
|
||||
@@ -754,10 +891,12 @@ public class ErosionFieldCompilerTest {
|
||||
|
||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||
assertChannelContained(compiled);
|
||||
assertNoWriteAboveNatural(compiled);
|
||||
assertBoundedBankFill(compiled, 8);
|
||||
assertEquals(80, compiled.field().column(0, 0).headY());
|
||||
SurfaceColumn nearRim = compiled.field().column(0, 4);
|
||||
assertEquals(SurfaceRole.CHANNEL, nearRim.role());
|
||||
assertTrue(compiled.field().columns().values().stream()
|
||||
.anyMatch(column -> column.height() > column.terrain().naturalHeight()));
|
||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||
if (column.role() == SurfaceRole.CHANNEL) {
|
||||
assertTrue(Math.abs(column.z()) <= 7);
|
||||
@@ -805,19 +944,21 @@ public class ErosionFieldCompilerTest {
|
||||
HydrologyTerrainSampler shelf = (int x, int z) ->
|
||||
HydrologyTerrainSample.openLand(80 + Math.min(16, Math.max(0, Math.abs(z) - 4) * 4), 0D, "land");
|
||||
Compiled terraced = compile(
|
||||
zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
|
||||
withMaximumIncision(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
|
||||
IrisRiverBlendStyle.TERRACED, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D,
|
||||
new HydrologyPlannerSettings.Excavation(64, 64, 8192))),
|
||||
new HydrologyPlannerSettings.Excavation(64, 64, 8192))), 32),
|
||||
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
assertNull(terraced.field().rejection());
|
||||
Set<Integer> steps = bankHeights(terraced, 150, 9, 40);
|
||||
assertTrue(steps.toString(), steps.size() >= 2 && steps.size() <= 4);
|
||||
assertHolds(terraced);
|
||||
|
||||
Compiled cliff = compile(
|
||||
zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
|
||||
withMaximumIncision(zeroRoughnessSurface(0, new HydrologyPlannerSettings.Erosion(true, 12, 0.45D, 1D, 0.5D,
|
||||
IrisRiverBlendStyle.CLIFF, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D,
|
||||
new HydrologyPlannerSettings.Excavation(64, 64, 8192))),
|
||||
new HydrologyPlannerSettings.Excavation(64, 64, 8192))), 32),
|
||||
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||
assertNull(cliff.field().rejection());
|
||||
Set<Integer> bench = bankHeights(cliff, 150, 9, 40);
|
||||
assertEquals(bench.toString(), 2, bench.size());
|
||||
assertTrue(bench.contains(80));
|
||||
@@ -873,7 +1014,43 @@ public class ErosionFieldCompilerTest {
|
||||
assertTrue(column.height() <= hillside.sample(150, z).naturalHeight());
|
||||
previous = column.height();
|
||||
}
|
||||
assertHolds(rising);
|
||||
assertEquals(0, rising.field().uncontainedWetCells());
|
||||
assertChannelContained(rising);
|
||||
assertBoundedBankFill(rising, 8);
|
||||
assertEquals(92, rising.field().column(150, -7).height());
|
||||
assertEquals(90, rising.field().column(150, -7).terrain().naturalHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroBankDepthBudgetLeavesLowBanksUnfilledAndUncontained() {
|
||||
HydrologyPlannerSettings.Erosion erosion = new HydrologyPlannerSettings.Erosion(
|
||||
true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D,
|
||||
IrisRiverBedProfile.BOWL, 0D, 0D, new HydrologyPlannerSettings.Excavation(0, 16, 256));
|
||||
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface(0, erosion);
|
||||
HydrologyTerrainSampler lowGround = (x, z) -> HydrologyTerrainSample.openLand(77, 0D, "land");
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(16, 80, 0)));
|
||||
int count = centerline.size();
|
||||
double[] widths = new double[count];
|
||||
double[] depths = new double[count];
|
||||
double[] multipliers = new double[count];
|
||||
int[] heads = new int[count];
|
||||
Arrays.fill(widths, 3D);
|
||||
Arrays.fill(depths, 2D);
|
||||
Arrays.fill(multipliers, 1D);
|
||||
Arrays.fill(heads, 80);
|
||||
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), lowGround).compile(42L,
|
||||
centerline, new ChannelProfile(widths, depths, multipliers), ValleyProfile.fromHeads(heads, count),
|
||||
SurfaceTerminal.SINKHOLE, 0);
|
||||
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT, field.rejection());
|
||||
assertTrue(field.uncontainedWetCells() > 0);
|
||||
assertEquals(77, field.column(8, 4).height());
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
if (column.role() != SurfaceRole.CHANNEL) {
|
||||
assertEquals(column.terrain().naturalHeight(), column.height());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -987,4 +1164,11 @@ public class ErosionFieldCompilerTest {
|
||||
assertEquals(SurfaceRole.SHORE, bare.field().column(150, 4).role());
|
||||
assertHolds(bare);
|
||||
}
|
||||
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
|
||||
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
|
||||
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
|
||||
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
|
||||
defaults.surfacePools(), 12D, defaults.seaCaves(), defaults.surfacePolicyBounds());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
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 org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverFirstTerrainTest {
|
||||
@Test
|
||||
public void riverBuildsContainedBanksAroundALowNaturalPatch() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(77, 0D, "land");
|
||||
ErosionField field = compile(sampler, null);
|
||||
|
||||
assertNull(field.rejection());
|
||||
assertEquals(0, field.uncontainedWetCells());
|
||||
SurfaceColumn bed = field.column(0, 0);
|
||||
assertNotNull(bed);
|
||||
assertEquals(bed.terrain().naturalHeight(), bed.height());
|
||||
assertTrue(bed.height() < bed.headY());
|
||||
int raisedBanks = 0;
|
||||
for (SurfaceColumn column : field.columns().values()) {
|
||||
if (column.role() != SurfaceRole.CHANNEL && column.height() > column.terrain().naturalHeight()) {
|
||||
raisedBanks++;
|
||||
assertTrue(column.height() - column.terrain().naturalHeight() <= 8);
|
||||
}
|
||||
}
|
||||
assertTrue(raisedBanks > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverRejectsRequiredFillBeyondTheTerrainBudget() {
|
||||
ErosionField field = compile((x, z) -> HydrologyTerrainSample.openLand(69, 0D, "land"), null);
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT, field.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unavailableBankCannotCertifyTheWetPerimeter() {
|
||||
HydrologyTerrainSample ground = HydrologyTerrainSample.openLand(77, 0D, "land");
|
||||
ErosionField complete = compile((x, z) -> ground, null);
|
||||
assertNull(complete.rejection());
|
||||
SurfaceColumn bank = complete.columns().values().stream()
|
||||
.filter(column -> column.x() == 0 && column.role() != SurfaceRole.CHANNEL
|
||||
&& complete.column(0, column.z() - 1) != null
|
||||
&& complete.column(0, column.z() - 1).role() == SurfaceRole.CHANNEL)
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
ErosionField unavailable = compile((x, z) -> x == bank.x() && z == bank.z() ? null : ground, null);
|
||||
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT, unavailable.rejection());
|
||||
assertTrue(unavailable.uncontainedWetCells() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fineChannelIncisionLimitIsCheckedBeforePublication() {
|
||||
ErosionField field = compile((x, z) -> HydrologyTerrainSample.openLand(x == 0 && z == 1 ? 110 : 82, 0D, "land"), null);
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, field.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void channelEdgesRetainWaterWithinTheLocalIncisionLimit() {
|
||||
ErosionField field = compile((x, z) -> HydrologyTerrainSample.openLand(x == 0 && z == 1 ? 89 : 82, 0D, "land"), null);
|
||||
assertNull(field.rejection());
|
||||
SurfaceColumn column = field.column(0, 1);
|
||||
assertEquals(SurfaceRole.CHANNEL, column.role());
|
||||
assertEquals(79, column.height());
|
||||
assertEquals(80, column.headY());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adjacentChunkRastersRetainIdenticalRaisedTerrain() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(77 + Math.floorMod(x + z, 3), 0D, "land");
|
||||
ErosionField complete = compile(sampler, null);
|
||||
assertNull(complete.rejection());
|
||||
for (SurfaceBounds bounds : List.of(new SurfaceBounds(-16, -16, -1, 15),
|
||||
new SurfaceBounds(0, -16, 15, 15))) {
|
||||
ErosionField bounded = compile(sampler, bounds);
|
||||
assertNull(bounded.rejection());
|
||||
for (SurfaceColumn column : complete.columns().values()) {
|
||||
if (bounds.contains(column.x(), column.z())) {
|
||||
assertEquals(column, bounded.column(column.x(), column.z()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ErosionField compile(HydrologyTerrainSampler sampler, SurfaceBounds bounds) {
|
||||
HydrologyPlannerSettings.Surface settings = HydrologyPlannerSettings.defaults().surface();
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(
|
||||
List.of(new HydrologyPoint(-48, 80, 0), new HydrologyPoint(48, 80, 0)));
|
||||
int count = centerline.size();
|
||||
double[] widths = new double[count];
|
||||
double[] depths = new double[count];
|
||||
double[] multipliers = new double[count];
|
||||
int[] heads = new int[count];
|
||||
Arrays.fill(widths, 3D);
|
||||
Arrays.fill(depths, 2D);
|
||||
Arrays.fill(multipliers, 1D);
|
||||
Arrays.fill(heads, 80);
|
||||
return new ErosionFieldCompiler(rasterSettings(settings), sampler).compile(17L, centerline,
|
||||
new ChannelProfile(widths, depths, multipliers), ValleyProfile.fromHeads(heads, count),
|
||||
SurfaceTerminal.SINKHOLE, 0, HydrologyPlannerSettings.Ponds.none(), SurfaceRasterContext.bounded(bounds));
|
||||
}
|
||||
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
|
||||
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
|
||||
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
|
||||
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
|
||||
defaults.surfacePools(), surface.shoreWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package art.arcane.iris.generation.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
|
||||
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 org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class RiverProfileSolverTest {
|
||||
private static final HydrologyGeometrySampler GEOMETRY = request -> switch (request.field()) {
|
||||
case SURFACE_WIDTH -> 6;
|
||||
case SURFACE_DEPTH -> 2;
|
||||
default -> request.minimum();
|
||||
};
|
||||
|
||||
@Test
|
||||
public void detailedTerrainDoesNotReplacePlannedRiverElevations() {
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 80, 0));
|
||||
for (int natural : List.of(78, 83, 84)) {
|
||||
SurfaceCourseResult result = build(path, (x, z) -> HydrologyTerrainSample.openLand(natural, 0D, "land"));
|
||||
assertNull(result.rejection());
|
||||
assertEquals(80, result.lastHead());
|
||||
for (HydraulicSegment segment : result.segments()) {
|
||||
assertEquals(80, segment.upstreamHeadY());
|
||||
assertEquals(80, segment.downstreamHeadY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interpolatedSharedHeadsRemainNonRisingAcrossStations() {
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(-96, 88, -16),
|
||||
new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 72, 16));
|
||||
SurfaceCourseResult result = build(path,
|
||||
(x, z) -> HydrologyTerrainSample.openLand(82 - Math.floorDiv(x, 12), 0D, "land"));
|
||||
assertNull(result.rejection());
|
||||
int previous = Integer.MAX_VALUE;
|
||||
for (HydraulicSegment segment : result.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
assertTrue(point.y() <= previous);
|
||||
previous = point.y();
|
||||
}
|
||||
}
|
||||
assertEquals(72, result.lastHead());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannedProfileChecksTheCenterWithoutSearchingBankRings() {
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 80, 0));
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
double[] widths = new double[centerline.size()];
|
||||
double[] depths = new double[centerline.size()];
|
||||
double[] banks = new double[centerline.size()];
|
||||
Arrays.fill(widths, 3D);
|
||||
Arrays.fill(depths, 2D);
|
||||
Arrays.fill(banks, 1D);
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
HydrologyTerrainSampler sampler = (x, z) -> {
|
||||
assertEquals(0, z);
|
||||
samples.incrementAndGet();
|
||||
return HydrologyTerrainSample.openLand(82, 0D, "land");
|
||||
};
|
||||
ValleyProfile profile = new RiverProfileSolver(
|
||||
new RiverProfileSolver.Options(HydrologyPlannerSettings.defaults().surface(), 60), sampler)
|
||||
.solve(path, centerline, new ChannelProfile(widths, depths, banks), SurfaceTerminal.SINKHOLE, 40, 32);
|
||||
assertNull(profile.rejection());
|
||||
assertEquals(centerline.size(), samples.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aFineCliffKeepsTheSourceHeadAndRaisesOnlyTheRequiredApproach() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(x < 32 ? 102 : 86, 0D, "land");
|
||||
SurfaceCourseResult result = build(List.of(new HydrologyPoint(0, 100, 0),
|
||||
new HydrologyPoint(64, 84, 0)), sampler);
|
||||
|
||||
assertNull(result.rejection());
|
||||
assertEquals(100, result.segments().getFirst().upstreamHeadY());
|
||||
assertEquals(84, result.lastHead());
|
||||
int previous = 100;
|
||||
for (HydraulicSegment segment : result.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
assertTrue(point.y() <= previous);
|
||||
assertTrue(sampler.sample(point.x(), point.z()).naturalHeight() - point.y() + 2
|
||||
<= HydrologyPlannerSettings.defaults().surface().maximumIncision());
|
||||
previous = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anUncuttableFineTerrainRidgeStillRejectsTheCourse() {
|
||||
SurfaceCourseResult result = build(List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(96, 80, 0)),
|
||||
(x, z) -> HydrologyTerrainSample.openLand(x == 48 ? 110 : 82, 0D, "land"));
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, result.rejection());
|
||||
}
|
||||
|
||||
private static SurfaceCourseResult build(List<HydrologyPoint> path, HydrologyTerrainSampler sampler) {
|
||||
return new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, GEOMETRY, 60)
|
||||
.build(7L, 19L, "default", path, SurfaceTerminal.SINKHOLE, 40, 32);
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -180,7 +180,7 @@ public class SurfaceBendProfileTest {
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, FLAT, 60, 32)
|
||||
.solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
|
||||
assertTrue(valley.accepted());
|
||||
return new ErosionFieldCompiler(surface, FLAT, 60).compile(42L, centerline, channel, valley,
|
||||
return new ErosionFieldCompiler(rasterSettings(surface), FLAT).compile(42L, centerline, channel, valley,
|
||||
SurfaceTerminal.SINKHOLE, 8, HydrologyPlannerSettings.Ponds.none(), context);
|
||||
}
|
||||
|
||||
@@ -191,4 +191,11 @@ public class SurfaceBendProfileTest {
|
||||
HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
|
||||
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.none()));
|
||||
}
|
||||
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
|
||||
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
|
||||
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
|
||||
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
|
||||
defaults.surfacePools(), surface.shoreWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+64
-7
@@ -13,6 +13,7 @@ import org.junit.Test;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -34,7 +35,8 @@ public class SurfaceCourseBuilderTest {
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(140, 0, 0), new HydrologyPoint(280, 0, 0)),
|
||||
List.of(new HydrologyPoint(0, 110, 0), new HydrologyPoint(140, 87, 0),
|
||||
new HydrologyPoint(260, 67, 0), new HydrologyPoint(280, SEA_LEVEL, 0)),
|
||||
SurfaceTerminal.OCEAN_MOUTH,
|
||||
SEA_LEVEL,
|
||||
64
|
||||
@@ -72,7 +74,7 @@ public class SurfaceCourseBuilderTest {
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
List.of(new HydrologyPoint(0, 80, 0), new HydrologyPoint(200, 80, 0)),
|
||||
SurfaceTerminal.SINKHOLE,
|
||||
40,
|
||||
64
|
||||
@@ -85,6 +87,41 @@ public class SurfaceCourseBuilderTest {
|
||||
assertEquals(201, result.segments().getFirst().centerline().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void coastalDropKeepsTheUpstreamGradedReachExposed() {
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
defaults.enabled(), defaults.sources(), defaults.minimumWidth(), defaults.maximumWidth(),
|
||||
defaults.minimumDepth(), defaults.maximumDepth(), defaults.maximumIncision(), defaults.shoreWidth(),
|
||||
defaults.banks().withInlet(HydrologyPlannerSettings.Inlet.none()));
|
||||
HydrologyTerrainSampler sampler = (x, z) -> x >= 40
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(100 - x, 0D, "land");
|
||||
SurfaceCourseBuilder builder = new SurfaceCourseBuilder(surface, sampler, CONSTANT_GEOMETRY, SEA_LEVEL);
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(40, SEA_LEVEL, 0));
|
||||
SurfaceCourseResult result = builder.build(7L, COURSE_ID, "water", path,
|
||||
SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 16);
|
||||
|
||||
assertNull(result.rejection());
|
||||
assertEquals(2, result.segments().size());
|
||||
HydraulicSegment approach = result.segments().getFirst();
|
||||
HydraulicSegment drop = result.segments().getLast();
|
||||
assertEquals(HydrologyFeatureType.CASCADE, approach.type());
|
||||
assertFalse(approach.fallingFluid());
|
||||
assertEquals(100, approach.upstreamHeadY());
|
||||
assertEquals(61, approach.downstreamHeadY());
|
||||
assertEquals(new HydrologyPoint(20, 80, 0), approach.centerline().get(20));
|
||||
assertEquals(HydrologyFeatureType.RIFFLE, drop.type());
|
||||
assertTrue(drop.fallingFluid());
|
||||
assertTrue(drop.receivingPool());
|
||||
assertEquals(List.of(new HydrologyPoint(39, 61, 0), new HydrologyPoint(40, 60, 0)), drop.centerline());
|
||||
assertEquals(approach.channelProfile().widthAt(39), drop.channelProfile().widthAt(0), 0D);
|
||||
assertEquals(approach.channelProfile().depthAt(39), drop.channelProfile().depthAt(0), 0D);
|
||||
assertTrue(approach.id() != drop.id());
|
||||
assertEquals(result, builder.build(7L, COURSE_ID, "water", path,
|
||||
SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cliffProducesAWaterfallSegment() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
||||
@@ -92,7 +129,8 @@ public class SurfaceCourseBuilderTest {
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
|
||||
new HydrologyPoint(100, 90, 0), new HydrologyPoint(200, 90, 0)),
|
||||
SurfaceTerminal.SINKHOLE,
|
||||
40,
|
||||
64
|
||||
@@ -106,14 +144,32 @@ public class SurfaceCourseBuilderTest {
|
||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().get(2).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cliffIntoTheOceanUsesFallingFluidInsteadOfAnInlandSeaLevelChannel() {
|
||||
HydrologyTerrainSampler sampler = (x, z) -> x >= 100
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(100, 0D, "land");
|
||||
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water",
|
||||
List.of(new HydrologyPoint(0, 97, 0), new HydrologyPoint(99, 97, 0),
|
||||
new HydrologyPoint(100, SEA_LEVEL, 0)), SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 64);
|
||||
|
||||
assertNull(result.rejection());
|
||||
HydraulicSegment fall = result.segments().getLast();
|
||||
assertEquals(HydrologyFeatureType.WATERFALL, fall.type());
|
||||
assertTrue(fall.fallingFluid());
|
||||
assertTrue(fall.receivingPool());
|
||||
assertEquals(SEA_LEVEL, fall.downstreamHeadY());
|
||||
assertEquals(100, fall.end().x());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gentleSlopeProducesRifflesAndSteepSlopeProducesCascades() {
|
||||
HydrologyTerrainSampler gentle = (int x, int z) -> HydrologyTerrainSample.openLand(120 - x / 10, 0D, "land");
|
||||
SurfaceCourseResult gentleResult = builder(gentle).build(7L, COURSE_ID, "water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
|
||||
List.of(new HydrologyPoint(0, 120, 0), new HydrologyPoint(200, 100, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
|
||||
HydrologyTerrainSampler steep = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
|
||||
SurfaceCourseResult steepResult = builder(steep).build(7L, COURSE_ID, "water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
|
||||
List.of(new HydrologyPoint(0, 300, 0), new HydrologyPoint(200, 100, 0)), SurfaceTerminal.SINKHOLE, 40, 64);
|
||||
|
||||
assertNull(gentleResult.rejection());
|
||||
assertNull("detail=" + steepResult.rejectionDetail(), steepResult.rejection());
|
||||
@@ -138,7 +194,7 @@ public class SurfaceCourseBuilderTest {
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
List.of(new HydrologyPoint(0, 50, 0), new HydrologyPoint(200, 50, 0)),
|
||||
SurfaceTerminal.OCEAN_MOUTH,
|
||||
SEA_LEVEL,
|
||||
64
|
||||
@@ -153,7 +209,8 @@ public class SurfaceCourseBuilderTest {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(241, 0, 0));
|
||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(239, 61, 0),
|
||||
new HydrologyPoint(240, SEA_LEVEL, 0), new HydrologyPoint(241, SEA_LEVEL, 0));
|
||||
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
||||
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 64);
|
||||
|
||||
+133
-9
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydraulicChannelProfile;
|
||||
import art.arcane.iris.generation.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
@@ -131,6 +132,33 @@ public class SurfaceFootprintCompilerTest {
|
||||
assertNull(column(footprint, 160, 12));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannedCoastalWaterfallRetainsAConnectedOceanReceiverWithoutCuttingTheCliffToSeaLevel() {
|
||||
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
|
||||
HydrologyTerrainSampler sampler = (x, z) -> x >= 100
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(100, 0D, "land");
|
||||
SurfaceCourseResult result = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(),
|
||||
sampler, CONSTANT_GEOMETRY, seaLevel).build(7L, 1L, "water",
|
||||
List.of(new HydrologyPoint(0, 97, 0), new HydrologyPoint(99, 97, 0),
|
||||
new HydrologyPoint(100, seaLevel, 0)), SurfaceTerminal.OCEAN_MOUTH, seaLevel, 64);
|
||||
assertNull(result.rejection());
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
|
||||
segments.add(segment(77L, HydrologyFeatureType.MOUTH, seaLevel, seaLevel,
|
||||
List.of(result.pathEnd())));
|
||||
SurfaceFootprint footprint = compiler(sampler).compile(course(segments));
|
||||
|
||||
assertNull(footprint.rejection());
|
||||
assertEquals(0, footprint.uncontainedWetCells());
|
||||
assertTrue(footprint.columns().stream().anyMatch(column -> column.layer().channel()
|
||||
&& column.layer().fluidOwned() && column.layer().fluidHeadY() == 97));
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (!column.apron() && column.role() == SurfaceRole.CHANNEL) {
|
||||
assertTrue(column.layer().bedY() >= 90);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mouthOverTheOceanPublishesOnlyANonOwningApron() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 100
|
||||
@@ -212,13 +240,101 @@ public class SurfaceFootprintCompilerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inletAllowanceSurvivesAFallWithoutReachingOrdinaryUpstreamTerrain() {
|
||||
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Banks banks = defaults.surface().banks();
|
||||
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
|
||||
HydrologyPlannerSettings.Erosion expandedVolume = new HydrologyPlannerSettings.Erosion(
|
||||
erosion.enabled(), erosion.smoothingRadius(), erosion.thalwegFraction(), erosion.blendCurve(),
|
||||
erosion.bedNoise(), erosion.style(), erosion.terraceSteps(), erosion.cliffFraction(), erosion.bedProfile(),
|
||||
erosion.shoreRise(), erosion.blendBaseWidth(), new HydrologyPlannerSettings.Excavation(8, 16, 512));
|
||||
HydrologyPlannerSettings.Banks expandedBanks = new HydrologyPlannerSettings.Banks(
|
||||
banks.sink(), banks.blendSlope(), banks.minimumBlendWidth(), banks.maximumBlendWidth(), banks.roughness(),
|
||||
banks.roughnessWavelength(), banks.cascadeRun(), banks.waterfallMinimumDrop(), banks.mouthFlareRatio(),
|
||||
banks.inlet(), banks.springWidthRatio(), banks.springLength(), banks.exposeCutStrata(), expandedVolume,
|
||||
banks.ponds(), banks.channel(), banks.flow());
|
||||
HydrologyPlannerSettings.Surface originalSurface = defaults.surface();
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
originalSurface.enabled(), originalSurface.sources(), originalSurface.minimumWidth(), originalSurface.maximumWidth(),
|
||||
originalSurface.minimumDepth(), originalSurface.maximumDepth(), originalSurface.maximumIncision(),
|
||||
originalSurface.shoreWidth(), expandedBanks);
|
||||
HydrologyPlannerSettings settings = new HydrologyPlannerSettings(defaults.seaLevel(), defaults.routing(), surface,
|
||||
defaults.hydraulics(), defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
|
||||
defaults.surfacePools(), defaults.widestShoreBiomeWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
|
||||
int seaLevel = settings.seaLevel();
|
||||
int upstreamHead = seaLevel + 22;
|
||||
int downstreamHead = seaLevel + 12;
|
||||
HydrologyTerrainSampler sampler = (x, z) -> x >= 200
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x >= 120 && x <= 135 ? 100 : x < 151 ? 90 : 80, 0D, "land");
|
||||
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
|
||||
upstreamHead, downstreamHead, 6, 3, true, true,
|
||||
List.of(new HydrologyPoint(150, upstreamHead, 0), new HydrologyPoint(151, downstreamHead, 0)),
|
||||
HydraulicChannelProfile.uniform(6, 3));
|
||||
ArrayList<HydrologyPoint> ramp = new ArrayList<>();
|
||||
for (int x = 180; x <= 192; x++) {
|
||||
ramp.add(new HydrologyPoint(x, downstreamHead - (x - 180), 0));
|
||||
}
|
||||
RiverCourse course = course(List.of(
|
||||
segment(1L, HydrologyFeatureType.SURFACE_POOL, upstreamHead, upstreamHead, points(0, 149, upstreamHead)),
|
||||
fall,
|
||||
segment(3L, HydrologyFeatureType.SURFACE_POOL, downstreamHead, downstreamHead, points(151, 180, downstreamHead)),
|
||||
segment(4L, HydrologyFeatureType.CASCADE, downstreamHead, seaLevel, ramp),
|
||||
segment(5L, HydrologyFeatureType.SURFACE_POOL, seaLevel, seaLevel, points(192, 199, seaLevel)),
|
||||
segment(6L, HydrologyFeatureType.MOUTH, seaLevel, seaLevel, points(199, 201, seaLevel))));
|
||||
SurfaceFootprintCompiler compiler = new SurfaceFootprintCompiler(settings, sampler, CONSTANT_GEOMETRY);
|
||||
SurfaceFootprint full = compiler.compile(course);
|
||||
|
||||
assertNull("detail=" + full.rejectionDetail(), full.rejection());
|
||||
assertEquals(0, full.uncontainedWetCells());
|
||||
for (int x : new int[]{130, 195}) {
|
||||
SurfaceLayerColumn channel = column(full, x, 0);
|
||||
assertNotNull(channel);
|
||||
assertTrue(channel.layer().channel());
|
||||
assertEquals(x == 130 ? upstreamHead : seaLevel, channel.layer().fluidHeadY());
|
||||
int cut = channel.terrain().naturalHeight() - channel.layer().bedY();
|
||||
assertTrue(cut > settings.surface().maximumIncision());
|
||||
assertTrue(cut <= settings.surface().banks().inlet().maximumIncision());
|
||||
}
|
||||
for (SurfaceBounds bounds : List.of(new SurfaceBounds(112, -8, 159, 8), new SurfaceBounds(144, -8, 199, 8))) {
|
||||
SurfaceFootprint bounded = compiler.compile(course, bounds);
|
||||
assertNull(bounded.rejection());
|
||||
assertEquals(full.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(),
|
||||
bounded.columns());
|
||||
}
|
||||
HydrologyTerrainSampler upstreamRidge = (x, z) -> x == 60
|
||||
? HydrologyTerrainSample.openLand(100, 0D, "land") : sampler.sample(x, z);
|
||||
SurfaceFootprint rejected = new SurfaceFootprintCompiler(settings, upstreamRidge, CONSTANT_GEOMETRY).compile(course);
|
||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, rejected.rejection());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standaloneMouthKeepsItsInletBudgetWithinTheDryCourseFraction() {
|
||||
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
|
||||
for (int coast : new int[]{8, 100}) {
|
||||
int ridgeX = Math.max(1, coast / 10);
|
||||
HydrologyTerrainSampler sampler = (x, z) -> x >= coast
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(x == ridgeX ? 85 : 70, 0D, "land");
|
||||
RiverCourse mouth = course(List.of(segment(1L, HydrologyFeatureType.MOUTH,
|
||||
seaLevel, seaLevel, points(0, coast + 1, seaLevel))));
|
||||
|
||||
SurfaceFootprint rejected = compiler(sampler).compile(mouth);
|
||||
|
||||
assertEquals("coast=" + coast, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
rejected.rejection());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aContainedFallRetainsBothExposedRunsAndWholeCourseStationIndices() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
|
||||
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
||||
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
|
||||
new HydrologyPoint(100, 90, 0), new HydrologyPoint(200, 90, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 90, 64);
|
||||
assertNull(built.rejection());
|
||||
RiverCourse course = course(built.segments());
|
||||
HydraulicSegment fall = course.segments().stream().filter(HydraulicSegment::fallingFluid).findFirst().orElseThrow();
|
||||
@@ -246,7 +362,13 @@ public class SurfaceFootprintCompilerTest {
|
||||
assertEquals(full.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(), bounded.columns());
|
||||
for (SurfaceLayerColumn value : full.columns()) {
|
||||
if (value.layer().terrainOwned()) {
|
||||
assertTrue(value.layer().bedY() <= value.terrain().naturalHeight());
|
||||
int fill = value.layer().bedY() - value.terrain().naturalHeight();
|
||||
if (value.layer().channel()) {
|
||||
assertTrue(fill <= 0);
|
||||
} else {
|
||||
assertTrue(fill <= settings.surface().maximumIncision());
|
||||
assertTrue(fill <= settings.surface().banks().erosion().excavation().maximumDepth());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,8 +378,8 @@ public class SurfaceFootprintCompilerTest {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
|
||||
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
||||
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 300, 0), new HydrologyPoint(200, 100, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 100, 64);
|
||||
assertNull("detail=" + built.rejectionDetail(), built.rejection());
|
||||
SurfaceFootprint footprint = compiler(sampler).compile(course(built.segments()));
|
||||
assertNull("detail=" + footprint.rejectionDetail(), footprint.rejection());
|
||||
@@ -276,8 +398,9 @@ public class SurfaceFootprintCompilerTest {
|
||||
x + z < 200 ? 100 : 86, 0D, "land");
|
||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
|
||||
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 200)),
|
||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
||||
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 99),
|
||||
new HydrologyPoint(100, 86, 100), new HydrologyPoint(200, 86, 200)),
|
||||
SurfaceTerminal.TRIBUTARY, 86, 64);
|
||||
assertNull(built.rejection());
|
||||
RiverCourse course = course(built.segments());
|
||||
assertTrue(course.segments().stream().anyMatch(segment -> segment.fallingFluid() && segment.drop() == 14));
|
||||
@@ -311,8 +434,9 @@ public class SurfaceFootprintCompilerTest {
|
||||
: HydrologyTerrainSample.openLand((x < 100 ? 100 : 90) + Math.abs(z) / 3, 0D, "land");
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler,
|
||||
CONSTANT_GEOMETRY, SEA_LEVEL).build(7L, 1L, "water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(180, 0, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
||||
List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
|
||||
new HydrologyPoint(100, 90, 0), new HydrologyPoint(180, 90, 0)),
|
||||
SurfaceTerminal.TRIBUTARY, 90, 64);
|
||||
assertNull(built.rejection());
|
||||
RiverCourse falling = course(built.segments());
|
||||
RiverCourse mouth = course(List.of(segment(1L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL,
|
||||
|
||||
+10
-3
@@ -186,7 +186,7 @@ public class ValleyProfileSolverTest {
|
||||
assertNull(valley.rejection());
|
||||
assertEquals(100, valley.head()[130]);
|
||||
assertEquals(98, valley.head()[149]);
|
||||
ErosionField field = new ErosionFieldCompiler(HydrologyPlannerSettings.defaults().surface(), terrain, SEA_LEVEL)
|
||||
ErosionField field = new ErosionFieldCompiler(rasterSettings(HydrologyPlannerSettings.defaults().surface()), terrain)
|
||||
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
|
||||
HydrologyPlannerSettings.Ponds.none());
|
||||
assertEquals(0, field.uncontainedWetCells());
|
||||
@@ -214,7 +214,7 @@ public class ValleyProfileSolverTest {
|
||||
assertEquals(120, valley.head()[110]);
|
||||
assertEquals(118, valley.head()[128]);
|
||||
assertContained(valley);
|
||||
ErosionField field = new ErosionFieldCompiler(surface, terrain, SEA_LEVEL)
|
||||
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), terrain)
|
||||
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
|
||||
HydrologyPlannerSettings.Ponds.none());
|
||||
assertEquals(0, field.uncontainedWetCells());
|
||||
@@ -231,7 +231,7 @@ public class ValleyProfileSolverTest {
|
||||
assertNull(valley.rejection());
|
||||
assertTrue(valley.exposedStations() < 240);
|
||||
assertEquals(92, valley.head()[valley.exposedStations() - 1]);
|
||||
ErosionField field = new ErosionFieldCompiler(HydrologyPlannerSettings.defaults().surface(), terrain, SEA_LEVEL)
|
||||
ErosionField field = new ErosionFieldCompiler(rasterSettings(HydrologyPlannerSettings.defaults().surface()), terrain)
|
||||
.compile(77L, centerline, channel, valley, SurfaceTerminal.COASTAL_GROTTO, 0,
|
||||
HydrologyPlannerSettings.Ponds.none());
|
||||
assertEquals(0, field.uncontainedWetCells());
|
||||
@@ -492,4 +492,11 @@ public class ValleyProfileSolverTest {
|
||||
private static List<HydrologyPoint> path(int stations) {
|
||||
return List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
||||
}
|
||||
private static HydrologyPlannerSettings rasterSettings(HydrologyPlannerSettings.Surface surface) {
|
||||
HydrologyPlannerSettings defaults = HydrologyPlannerSettings.defaults();
|
||||
return new HydrologyPlannerSettings(60, defaults.routing(), surface, defaults.hydraulics(),
|
||||
defaults.underground(), defaults.outlets(), defaults.geometry(), defaults.deepFluids(),
|
||||
defaults.surfacePools(), surface.shoreWidth(), defaults.seaCaves(), defaults.surfacePolicyBounds());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -261,7 +261,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void translatedTreeSchedulesDistantOwnerChunks() throws Exception {
|
||||
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
|
||||
File objectFile = temporaryFolder.newFile("tree.iob");
|
||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
||||
output.writeInt(1);
|
||||
@@ -293,8 +293,8 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
||||
assertEquals(33, component.getRadius());
|
||||
assertEquals(33, component.getOutputRadius());
|
||||
assertEquals(0, component.getInputRadius());
|
||||
assertEquals(0, component.getOutputRadius());
|
||||
assertEquals(82, component.getInputRadius());
|
||||
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||
@@ -322,9 +322,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(1, directOrigins.get());
|
||||
assertEquals(49, directOrigins.get());
|
||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||
assertEquals(2, directOrigins.get());
|
||||
assertEquals(49, directOrigins.get());
|
||||
|
||||
List<String> generatedChunks = new ArrayList<>();
|
||||
doAnswer(invocation -> {
|
||||
@@ -342,7 +342,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
||||
));
|
||||
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
|
||||
|
||||
assertEquals(49, generatedChunks.size());
|
||||
assertEquals(List.of("0,0"), generatedChunks);
|
||||
|
||||
List<String> replayedSources = new ArrayList<>();
|
||||
MantleObjectComponent.replaySourceChunks(
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleObjectComponentPlacementDeterminismTest {
|
||||
private static final Position LEFT = new Position(15, 10, 0);
|
||||
private static final Position RIGHT = new Position(16, 10, 0);
|
||||
private static final Position SUPPORT = new Position(15, 11, 0);
|
||||
|
||||
@Test
|
||||
public void ordinaryObjectsKeepPrerequisitesAndSameSourceReadsAcrossDestinationOrders() throws Exception {
|
||||
Fixture forward = new Fixture();
|
||||
forward.generate(List.of(0, 1));
|
||||
Fixture reverse = new Fixture();
|
||||
reverse.generate(List.of(1, 0));
|
||||
Fixture parallel = new Fixture();
|
||||
parallel.generateParallel();
|
||||
Map<Position, String> expected = Map.of(
|
||||
LEFT, "minecraft:mangrove_wood",
|
||||
RIGHT, "minecraft:mangrove_wood",
|
||||
SUPPORT, "minecraft:mangrove_wood"
|
||||
);
|
||||
|
||||
assertEquals(expected, forward.snapshot());
|
||||
assertEquals(expected, reverse.snapshot());
|
||||
assertEquals(expected, parallel.snapshot());
|
||||
assertEquals(12, parallel.sourceCalls.size());
|
||||
for (Integer calls : parallel.sourceCalls.values()) {
|
||||
assertEquals(1, calls.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryObjectSourcePlansRemainReusableAfterNeighborPublicationAndHotload() {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.generate(List.of(1, 0));
|
||||
Map<Position, String> expected = fixture.snapshot();
|
||||
Map<Chunk, Integer> calls = Map.copyOf(fixture.sourceCalls);
|
||||
|
||||
fixture.generate(List.of(0, 1));
|
||||
|
||||
assertEquals(expected, fixture.snapshot());
|
||||
assertEquals(calls, fixture.sourceCalls);
|
||||
fixture.component.hotload();
|
||||
fixture.generate(List.of(0, 1));
|
||||
assertEquals(expected, fixture.snapshot());
|
||||
for (Map.Entry<Chunk, Integer> entry : fixture.sourceCalls.entrySet()) {
|
||||
assertEquals(2, entry.getValue().intValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeBlockState stone = state("minecraft:stone");
|
||||
private final NativeBlockState oak = state("minecraft:oak_wood");
|
||||
private final NativeBlockState mangrove = state("minecraft:mangrove_wood");
|
||||
private final NativeBlockState wrongRead = state("minecraft:dirt");
|
||||
private final Map<Position, NativeBlockState> published = new ConcurrentHashMap<>();
|
||||
private final Map<Chunk, Integer> sourceCalls = new ConcurrentHashMap<>();
|
||||
private final ThreadLocal<Chunk> destination = new ThreadLocal<>();
|
||||
private final MantleWriter writer;
|
||||
private final MantleObjectComponent component;
|
||||
private final ChunkContext context = mock(ChunkContext.class);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture() {
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.getAllRegions(any())).thenReturn(new KList<>());
|
||||
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>());
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.getWorldHeight()).thenReturn(64);
|
||||
writer = mock(MantleWriter.class);
|
||||
when(writer.getMantle()).thenReturn(mantle);
|
||||
when(writer.getEngine()).thenReturn(engine);
|
||||
when(writer.getPrerequisiteBlock(anyInt(), anyInt(), anyInt())).thenReturn(stone);
|
||||
when(writer.get(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> published.getOrDefault(
|
||||
new Position(invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)),
|
||||
stone));
|
||||
doAnswer(invocation -> {
|
||||
destination.set(new Chunk(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
try {
|
||||
Runnable task = invocation.getArgument(2);
|
||||
task.run();
|
||||
} finally {
|
||||
destination.remove();
|
||||
}
|
||||
return null;
|
||||
}).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class));
|
||||
doAnswer(invocation -> {
|
||||
Position position = new Position(
|
||||
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2));
|
||||
NativeBlockState value = invocation.getArgument(3);
|
||||
assertEquals(new Chunk(position.x() >> 4, position.z() >> 4), destination.get());
|
||||
published.put(position, value);
|
||||
return null;
|
||||
}).when(writer).setData(anyInt(), anyInt(), anyInt(), any(NativeBlockState.class));
|
||||
component = new OrdinaryObjects(engineMantle, this);
|
||||
}
|
||||
|
||||
private void generate(List<Integer> destinations) {
|
||||
for (Integer chunkX : destinations) {
|
||||
component.generateLayer(writer, chunkX, 0, context);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateParallel() throws Exception {
|
||||
CountDownLatch ready = new CountDownLatch(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
|
||||
Future<?> left = executor.submit(() -> generateAfterBarrier(0, ready, start));
|
||||
Future<?> right = executor.submit(() -> generateAfterBarrier(1, ready, start));
|
||||
assertTrue(ready.await(5L, TimeUnit.SECONDS));
|
||||
start.countDown();
|
||||
left.get(5L, TimeUnit.SECONDS);
|
||||
right.get(5L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateAfterBarrier(int chunkX, CountDownLatch ready, CountDownLatch start) {
|
||||
ready.countDown();
|
||||
try {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(failure);
|
||||
}
|
||||
component.generateLayer(writer, chunkX, 0, context);
|
||||
}
|
||||
|
||||
private Map<Position, String> snapshot() {
|
||||
Map<Position, String> result = new HashMap<>();
|
||||
for (Map.Entry<Position, NativeBlockState> entry : published.entrySet()) {
|
||||
result.put(entry.getKey(), entry.getValue().key());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static NativeBlockState state(String key) {
|
||||
NativeBlockState state = mock(NativeBlockState.class);
|
||||
when(state.key()).thenReturn(key);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OrdinaryObjects extends MantleObjectComponent {
|
||||
private final Fixture fixture;
|
||||
|
||||
private OrdinaryObjects(EngineMantle engineMantle, Fixture fixture) {
|
||||
super(engineMantle);
|
||||
this.fixture = fixture;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
void generateOrigin(ObjectPassPlacer placer, int chunkX, int chunkZ, ChunkContext context) {
|
||||
fixture.sourceCalls.merge(new Chunk(chunkX, chunkZ), 1, Integer::sum);
|
||||
if (chunkZ != 0 || chunkX < 0 || chunkX > 1) {
|
||||
return;
|
||||
}
|
||||
NativeBlockState prerequisite = placer.get(LEFT.x(), LEFT.y(), LEFT.z());
|
||||
NativeBlockState wood = chunkX == 0 ? fixture.oak : fixture.mangrove;
|
||||
placer.set(SUPPORT.x(), SUPPORT.y(), SUPPORT.z(), wood);
|
||||
boolean sameSourceSupport = placer.get(SUPPORT.x(), SUPPORT.y(), SUPPORT.z()) == wood;
|
||||
NativeBlockState placed = prerequisite == fixture.stone && sameSourceSupport ? wood : fixture.wrongRead;
|
||||
placer.set(LEFT.x(), LEFT.y(), LEFT.z(), placed);
|
||||
placer.set(RIGHT.x(), RIGHT.y(), RIGHT.z(), placed);
|
||||
}
|
||||
}
|
||||
|
||||
private record Position(int x, int y, int z) {
|
||||
}
|
||||
|
||||
private record Chunk(int x, int z) {
|
||||
}
|
||||
}
|
||||
+29
@@ -93,6 +93,35 @@ public class ObjectDestinationTransactionTest {
|
||||
verify(writer).setData(1, 4, 0, source);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyingSourcePlanSkipsForeignChunksBeforeOverlayingAndPreservesLocalOrder() {
|
||||
MantleWriter writer = writer();
|
||||
Marker foreign = new Marker("foreign");
|
||||
Marker prerequisite = new Marker("prerequisite");
|
||||
Marker first = new Marker("first");
|
||||
Marker second = new Marker("second");
|
||||
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, Marker.class)).thenReturn(prerequisite);
|
||||
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
|
||||
source.setData(-1, 4, 0, first);
|
||||
source.setData(0, 4, 0, foreign);
|
||||
source.setData(-1, 4, 0, second);
|
||||
source.setData(-1, 4, 16, foreign);
|
||||
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, -1, 0);
|
||||
|
||||
destination.apply(source.sourcePlanSince(0));
|
||||
|
||||
assertEquals(2, destination.mutationCheckpoint());
|
||||
assertSame(second, destination.getDataIfPresent(-1, 4, 0, Marker.class));
|
||||
assertSame(prerequisite, destination.getDataIfPresent(0, 4, 0, Marker.class));
|
||||
assertNull(destination.getDataIfPresent(-1, 4, 16, Marker.class));
|
||||
destination.commit();
|
||||
InOrder order = inOrder(writer);
|
||||
order.verify(writer).setData(-1, 4, 0, first);
|
||||
order.verify(writer).setData(-1, 4, 0, second);
|
||||
verify(writer, never()).setData(0, 4, 0, foreign);
|
||||
verify(writer, never()).setData(-1, 4, 16, foreign);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sourcePlanRejectsAnInvalidCheckpoint() {
|
||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer(), 0, 0);
|
||||
|
||||
+67
@@ -3,11 +3,15 @@ package art.arcane.iris.generation.runtime;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisInterpolator;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.volmlib.util.interpolation.InterpolationMethod;
|
||||
import art.arcane.volmlib.util.interpolation.NoiseBounds;
|
||||
import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -15,6 +19,8 @@ import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -23,8 +29,69 @@ import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.mockingDetails;
|
||||
|
||||
public class IrisComplexBoundsBiomeCacheTest {
|
||||
@Test
|
||||
public void cachedBiomeColumnsReuseFractionalSamplesWithSignedParity() throws Exception {
|
||||
IrisBiome first = new IrisBiome();
|
||||
IrisBiome second = new IrisBiome();
|
||||
ProceduralStream<IrisBiome> source = ProceduralStream.of(
|
||||
(x, z) -> x.intValue() < 0 || z.intValue() < 0 ? first : second,
|
||||
Interpolated.of(value -> 0D, value -> first));
|
||||
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||
services.when(() -> IrisServices.get(PreservationRegistry.class))
|
||||
.thenReturn(mock(PreservationRegistry.class));
|
||||
CachedStream2D<IrisBiome> stream = spy(new CachedStream2D<>("bounds", mock(Engine.class), source, 16));
|
||||
Fixture fixture = new Fixture(stream);
|
||||
IrisInterpolator interpolator = new IrisInterpolator()
|
||||
.setFunction(InterpolationMethod.STARCAST_12).setHorizontalScale(1.25D);
|
||||
fixture.setBounds(interpolator, first, -15.25D, 63.5D);
|
||||
fixture.setBounds(interpolator, second, 7.75D, 127.25D);
|
||||
AtomicInteger requestedSamples = new AtomicInteger();
|
||||
Set<Long> columns = new HashSet<>();
|
||||
for (int z = -2; z <= 2; z++) {
|
||||
for (int x = -2; x <= 2; x++) {
|
||||
NoiseBounds expected = interpolator.interpolateBounds(x, z, (xx, zz) -> {
|
||||
requestedSamples.incrementAndGet();
|
||||
columns.add(((long) (int) xx << 32) | ((int) zz & 0xffffffffL));
|
||||
return (int) xx < 0 || (int) zz < 0
|
||||
? new NoiseBounds(-15.25D, 63.5D) : new NoiseBounds(7.75D, 127.25D);
|
||||
});
|
||||
assertBits(expected, fixture.sample(interpolator, x, z));
|
||||
}
|
||||
}
|
||||
long reads = mockingDetails(stream).getInvocations().stream()
|
||||
.filter(invocation -> invocation.getMethod().getName().equals("get"))
|
||||
.count();
|
||||
assertTrue(reads <= columns.size() * 2L);
|
||||
assertTrue(reads < requestedSamples.get() / 4L);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncachedBiomeStreamsPreserveFractionalSamplesWithinOneColumn() throws Exception {
|
||||
IrisBiome first = new IrisBiome();
|
||||
IrisBiome second = new IrisBiome();
|
||||
ProceduralStream<IrisBiome> source = ProceduralStream.of(
|
||||
(x, z) -> x < -0.5D || z > 0.5D ? first : second,
|
||||
Interpolated.of(value -> 0D, value -> first));
|
||||
Fixture fixture = new Fixture(source);
|
||||
IrisInterpolator sampling = new IrisInterpolator() {
|
||||
@Override
|
||||
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||
return provider.noise(x, z);
|
||||
}
|
||||
};
|
||||
fixture.setBounds(sampling, first, -10D, 20D);
|
||||
fixture.setBounds(sampling, second, 50D, 80D);
|
||||
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(sampling, -0.75D, 0.25D));
|
||||
assertBits(new NoiseBounds(50D, 80D), fixture.sample(sampling, -0.25D, 0.25D));
|
||||
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(sampling, -0.25D, 0.75D));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void adjacentPassesReuseSamplesWithoutChangingInterpolationBits() throws Exception {
|
||||
IrisBiome first = new IrisBiome();
|
||||
|
||||
+3
-1
@@ -841,7 +841,9 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
private static IrisBiome carvingBiome(IrisEngine engine, IrisDimensionCarvingResolver.State state) {
|
||||
IrisDimensionCarvingEntry root = IrisDimensionCarvingResolver.resolveRootEntry(engine, 80, state);
|
||||
IrisDimensionCarvingEntry child = IrisDimensionCarvingResolver.resolveFromRoot(engine, root, 19, -3, state);
|
||||
return IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
|
||||
IrisBiome expected = IrisDimensionCarvingResolver.resolveEntryBiome(engine, child, state);
|
||||
assertSame(expected, IrisDimensionCarvingResolver.resolveBiome(engine, 19, 80, -3, state));
|
||||
return expected;
|
||||
}
|
||||
|
||||
private static GenerationHistoryRuntimeRouter historyRouter(IrisEngine engine) {
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.world.history.GenerationKernelRegistry;
|
||||
import art.arcane.iris.world.history.TransitionGenerationPlan;
|
||||
import org.junit.Test;
|
||||
import org.junit.Rule;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
import art.arcane.iris.pack.PackValidationCache;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.iris.world.IrisWorld;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.studio.StudioMode;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class PreparedHydrologyCacheIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
|
||||
@Rule
|
||||
public TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void frozenStudioAndProductionCopiesShareUntilPackBytesChange() throws Exception {
|
||||
Path studio = folder.newFolder("studio").toPath();
|
||||
Path production = folder.newFolder("production").toPath();
|
||||
Files.writeString(studio.resolve("terrain.json"), "{\"height\":80}");
|
||||
Files.copy(studio.resolve("terrain.json"), production.resolve("terrain.json"));
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
|
||||
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
|
||||
PreparedHydrologyCacheIdentity prepared = PreparedHydrologyCacheIdentity.capture(target(studio), kernel, null, true);
|
||||
assertEquals(prepared, PreparedHydrologyCacheIdentity.capture(target(production), kernel, null, false));
|
||||
Files.writeString(production.resolve("terrain.json"), "{\"height\":81}");
|
||||
assertNotEquals(prepared, PreparedHydrologyCacheIdentity.capture(target(production), kernel, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void biomeBuffetLayoutsAreIsolatedWhileNormalStudioSharesProduction() throws Exception {
|
||||
Path pack = folder.newFolder("layout").toPath();
|
||||
Files.writeString(pack.resolve("terrain.json"), "{}");
|
||||
EngineTarget target = target(pack);
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||
when(IrisPlatforms.get().platformName()).thenReturn("bukkit");
|
||||
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
|
||||
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
|
||||
PreparedHydrologyCacheIdentity normal = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
|
||||
assertNotNull(normal);
|
||||
assertEquals(normal, PreparedHydrologyCacheIdentity.capture(target, kernel, null, true));
|
||||
when(target.getDimension().getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_3x3);
|
||||
PreparedHydrologyCacheIdentity buffet = PreparedHydrologyCacheIdentity.capture(target, kernel, null, true);
|
||||
assertNotEquals(normal, buffet);
|
||||
assertEquals(normal, PreparedHydrologyCacheIdentity.capture(target, kernel, null, false));
|
||||
when(target.getDimension().getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_9x9);
|
||||
assertNotEquals(buffet, PreparedHydrologyCacheIdentity.capture(target, kernel, null, true));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visibleFallbackPackBytesAndNamesSeparatePlans() throws Exception {
|
||||
Path targetPack = folder.newFolder("target").toPath();
|
||||
Path packs = folder.newFolder("packs").toPath();
|
||||
Path dependency = Files.createDirectory(packs.resolve("shared"));
|
||||
Path content = dependency.resolve("terrain.json");
|
||||
Files.writeString(content, "{\"height\":80}");
|
||||
EngineTarget target = target(targetPack);
|
||||
when(target.getData().hasGenerationRegistryContract()).thenReturn(false);
|
||||
when(IrisPlatforms.get().packsFolderNoCreate()).thenReturn(packs.toFile());
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
|
||||
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
|
||||
PreparedHydrologyCacheIdentity initial = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
|
||||
assertNotNull(initial);
|
||||
Files.writeString(content, "{\"height\":81}");
|
||||
PreparedHydrologyCacheIdentity changed = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
|
||||
assertNotNull(changed);
|
||||
assertNotEquals(initial, changed);
|
||||
Files.move(dependency, packs.resolve("renamed"));
|
||||
PreparedHydrologyCacheIdentity renamed = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
|
||||
assertNotNull(renamed);
|
||||
assertNotEquals(changed, renamed);
|
||||
Path hidden = Files.createDirectory(packs.resolve(".hidden"));
|
||||
Files.writeString(hidden.resolve("terrain.json"), "{\"height\":90}");
|
||||
assertEquals(renamed, PreparedHydrologyCacheIdentity.capture(target, kernel, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void visiblePackSymlinksUseLogicalNamesAndResolvedContent() throws Exception {
|
||||
Path targetPack = folder.newFolder("symlink-target").toPath();
|
||||
Path first = folder.newFolder("first-packs").toPath();
|
||||
Path second = folder.newFolder("second-packs").toPath();
|
||||
Path dependency = Files.createDirectory(first.resolve("shared"));
|
||||
Files.writeString(dependency.resolve("terrain.json"), "{}");
|
||||
Files.createSymbolicLink(second.resolve("shared"), dependency);
|
||||
EngineTarget target = target(targetPack);
|
||||
when(target.getData().hasGenerationRegistryContract()).thenReturn(false);
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||
try (MockedStatic<PackValidationCache> validation = mockStatic(PackValidationCache.class)) {
|
||||
validation.when(PackValidationCache::contextFingerprint).thenReturn("platform");
|
||||
when(IrisPlatforms.get().packsFolderNoCreate()).thenReturn(first.toFile());
|
||||
PreparedHydrologyCacheIdentity direct = PreparedHydrologyCacheIdentity.capture(target, kernel, null, false);
|
||||
assertNotNull(direct);
|
||||
when(IrisPlatforms.get().packsFolderNoCreate()).thenReturn(second.toFile());
|
||||
assertEquals(direct, PreparedHydrologyCacheIdentity.capture(target, kernel, null, false));
|
||||
}
|
||||
}
|
||||
|
||||
private static EngineTarget target(Path path) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getDataFolder()).thenReturn(path.toFile());
|
||||
when(data.hasGenerationRegistryContract()).thenReturn(true);
|
||||
IrisWorld world = mock(IrisWorld.class);
|
||||
when(world.minHeight()).thenReturn(-64);
|
||||
when(world.maxHeight()).thenReturn(320);
|
||||
EngineTarget target = mock(EngineTarget.class);
|
||||
IrisDimension dimension = mock(IrisDimension.class);
|
||||
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
|
||||
when(target.getDimension()).thenReturn(dimension);
|
||||
when(target.getData()).thenReturn(data);
|
||||
when(target.getWorld()).thenReturn(world);
|
||||
return target;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void identicalInputsShareRegardlessOfWorldLocation() {
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||
assertEquals(identity("pack", "closed", "platform", kernel, null, -64),
|
||||
identity("pack", "closed", "platform", kernel, null, -64));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyExternalInputAndTransitionSeparatesPlans() {
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = kernel("implementation");
|
||||
PreparedHydrologyCacheIdentity base = identity("pack", "closed", "platform", kernel, null, -64);
|
||||
assertNotEquals(base, identity("changed", "closed", "platform", kernel, null, -64));
|
||||
assertNotEquals(base, identity("pack", "dependency-changed", "platform", kernel, null, -64));
|
||||
assertNotEquals(base, identity("pack", "closed", "platform-changed", kernel, null, -64));
|
||||
assertNotEquals(base, identity("pack", "closed", "platform", kernel("changed"), null, -64));
|
||||
assertNotEquals(base, identity("pack", "closed", "platform", kernel, null, 0));
|
||||
TransitionGenerationPlan.Specification transition = new TransitionGenerationPlan.Specification(
|
||||
1L, "old", "new", 1, 256, "boundary", "terrain");
|
||||
assertNotEquals(base, identity("pack", "closed", "platform", kernel, transition, -64));
|
||||
assertNotEquals(identity("pack", "closed", "platform", kernel, transition, -64),
|
||||
identity("pack", "closed", "platform", kernel,
|
||||
new TransitionGenerationPlan.Specification(1L, "old", "new", 1, 256, "other", "terrain"), -64));
|
||||
}
|
||||
|
||||
private static PreparedHydrologyCacheIdentity identity(String pack, String dependencies, String platform,
|
||||
GenerationKernelRegistry.RuntimeKernel kernel, TransitionGenerationPlan.Specification transition, int minY) {
|
||||
return PreparedHydrologyCacheIdentity.fromInputs(pack, dependencies, platform, kernel, transition, minY, 320, 0);
|
||||
}
|
||||
|
||||
private static GenerationKernelRegistry.RuntimeKernel kernel(String implementation) {
|
||||
GenerationKernelRegistry.RuntimeKernel kernel = mock(GenerationKernelRegistry.RuntimeKernel.class);
|
||||
when(kernel.version()).thenReturn(new GenerationKernelRegistry.Version(1, 1, 1));
|
||||
when(kernel.implementationFingerprint()).thenReturn(implementation);
|
||||
return kernel;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user