mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Tunabelly
This commit is contained in:
1 parent
197d2c2461
commit
30c533feaf
114 files changed
+4071
-3994
No files matched your search
+2
-2
@@ -210,7 +210,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
packDirs = validation.packDirs();
|
packDirs = validation.packDirs();
|
||||||
contentSnapshot = validation.contentSnapshot();
|
contentSnapshot = validation.contentSnapshot();
|
||||||
results = validation.results();
|
results = validation.results();
|
||||||
if (validation.stable()) {
|
if (validation.stable() && contextFingerprint.equals(PackValidationCache.contextFingerprint())) {
|
||||||
try {
|
try {
|
||||||
PackValidationCache.save(
|
PackValidationCache.save(
|
||||||
cacheFile,
|
cacheFile,
|
||||||
@@ -229,7 +229,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
String packFingerprint = packFingerprints.get(result.getPackName());
|
String packFingerprint = packFingerprints.get(result.getPackName());
|
||||||
File packDirectory = PackDirectoryResolver.resolveExisting(packsRoot, result.getPackName());
|
File packDirectory = PackDirectoryResolver.resolveExisting(packsRoot, result.getPackName());
|
||||||
if (packDirectory != null && packFingerprint != null && !packFingerprint.isBlank()) {
|
if (packDirectory != null && packFingerprint != null && !packFingerprint.isBlank()) {
|
||||||
PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint);
|
PackValidationRegistry.publish(packDirectory.toPath(), result, packFingerprint, contextFingerprint);
|
||||||
}
|
}
|
||||||
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
|
String minecraftVersion = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
|
||||||
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
|
String compatSummary = PackValidator.compatSummary(result, minecraftVersion);
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeWorld;
|
|||||||
import art.arcane.iris.pack.loading.IrisData;
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
import art.arcane.iris.pack.BrokenPackException;
|
import art.arcane.iris.pack.BrokenPackException;
|
||||||
|
import art.arcane.iris.pack.PackValidationCache;
|
||||||
import art.arcane.iris.pack.PackValidationRegistry;
|
import art.arcane.iris.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.pack.PackValidationResult;
|
import art.arcane.iris.pack.PackValidationResult;
|
||||||
import art.arcane.iris.pack.PackValidator;
|
import art.arcane.iris.pack.PackValidator;
|
||||||
@@ -184,8 +185,9 @@ final class ModdedGenerationHistoryStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void validatePack(Path packRoot, String fingerprint) {
|
private static void validatePack(Path packRoot, String fingerprint) {
|
||||||
|
String contextFingerprint = PackValidationCache.contextFingerprint();
|
||||||
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
|
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
|
||||||
PackValidationRegistry.publish(packRoot, validation, fingerprint);
|
PackValidationRegistry.publish(packRoot, validation, fingerprint, contextFingerprint);
|
||||||
if (!validation.isLoadable()) {
|
if (!validation.isLoadable()) {
|
||||||
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,6 +266,7 @@ public final class ModdedWorldEngines {
|
|||||||
TransitionGenerationPlan transitionPlan = activation.isInitial()
|
TransitionGenerationPlan transitionPlan = activation.isInitial()
|
||||||
? null
|
? null
|
||||||
: history.transitionPlan(activation.activationId());
|
: history.transitionPlan(activation.activationId());
|
||||||
|
data.bindGenerationRegistryContract(epoch.registryContract());
|
||||||
IrisEngine engine = new IrisEngine(
|
IrisEngine engine = new IrisEngine(
|
||||||
new EngineTarget(world, dimension, data),
|
new EngineTarget(world, dimension, data),
|
||||||
IrisEngine.InitializationMode.RUNTIME,
|
IrisEngine.InitializationMode.RUNTIME,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ record FootprintLayerShape(
|
|||||||
boolean archedChannel,
|
boolean archedChannel,
|
||||||
boolean roundedSurfaceBed,
|
boolean roundedSurfaceBed,
|
||||||
boolean organicBoundary,
|
boolean organicBoundary,
|
||||||
boolean fallingThroat
|
boolean fallingThroat,
|
||||||
|
boolean inlet
|
||||||
) {
|
) {
|
||||||
double totalRadius() {
|
double totalRadius() {
|
||||||
return channelRadius + shoreWidth + gradingWidth;
|
return channelRadius + shoreWidth + gradingWidth;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ final class FootprintMutableColumn {
|
|||||||
first.connectedFluid() || second.connectedFluid(),
|
first.connectedFluid() || second.connectedFluid(),
|
||||||
first.fallingFluid() || second.fallingFluid(),
|
first.fallingFluid() || second.fallingFluid(),
|
||||||
first.receivingPool() || second.receivingPool(),
|
first.receivingPool() || second.receivingPool(),
|
||||||
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY < naturalHeight),
|
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY != naturalHeight),
|
||||||
first.fluidOwned() || second.fluidOwned(),
|
first.fluidOwned() || second.fluidOwned(),
|
||||||
first.oceanApron() || second.oceanApron(),
|
first.oceanApron() || second.oceanApron(),
|
||||||
first.profileKey(),
|
first.profileKey(),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ final class HydrologyCacheBudget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
long routingBytes() {
|
long routingBytes() {
|
||||||
return regionalEnabled ? plannerBytes / 8L : plannerBytes * 3L / 16L;
|
return plannerBytes * 3L / 16L;
|
||||||
}
|
}
|
||||||
|
|
||||||
long ownerBytes() {
|
long ownerBytes() {
|
||||||
@@ -69,7 +69,4 @@ final class HydrologyCacheBudget {
|
|||||||
return regionalEnabled ? plannerBytes / 4L : 0L;
|
return regionalEnabled ? plannerBytes / 4L : 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
long regionalReachBytes() {
|
|
||||||
return regionalEnabled ? plannerBytes / 16L : 0L;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ final class HydrologyCacheWeights {
|
|||||||
static int regionalCoarse(HydrologyRegionalCoarsePlan plan) {
|
static int regionalCoarse(HydrologyRegionalCoarsePlan plan) {
|
||||||
HydrologyRegionalGraph.Tree tree = plan.tree();
|
HydrologyRegionalGraph.Tree tree = plan.tree();
|
||||||
long treeBytes = tree == null ? 0L : 96L + (long) tree.selected().length * 8L
|
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
|
return saturated(ENTRY_BYTES + grid(plan.grid()) + treeBytes + (long) plan.outlets().size() * 224L
|
||||||
+ (long) plan.sources().size() * 32L + (long) plan.envelopes().size() * 64L);
|
+ (long) plan.sources().size() * 32L + (long) plan.envelopes().size() * 64L);
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ final class HydrologyCacheWeights {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static long grid(HydrologySampledGrid grid) {
|
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) {
|
private static long routingPlan(HydrologyRoutingPlan plan) {
|
||||||
@@ -88,7 +88,8 @@ final class HydrologyCacheWeights {
|
|||||||
}
|
}
|
||||||
return 128L + (long) plan.potential().length * 8L
|
return 128L + (long) plan.potential().length * 8L
|
||||||
+ ((long) plan.parent().length + plan.outletIndex().length + plan.routeLengths().length) * 4L
|
+ ((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,
|
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);
|
HydraulicSegment segment = segments.get(index);
|
||||||
if (segment.type().isSurface() && segment.fallingFluid()) {
|
if (segment.type().isSurface() && segment.fallingFluid()) {
|
||||||
int radius = Math.max(1, segment.width() / 2) + 1;
|
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,
|
openings.add(CaveSurfaceOpening.create(segment, true, (long) radius * radius,
|
||||||
segment.downstreamHeadY() + 2, true));
|
segment.downstreamHeadY() + (coastal ? 0 : 2), true));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!segment.type().isUnderground()) {
|
if (!segment.type().isUnderground()) {
|
||||||
|
|||||||
+18
-1
@@ -1,9 +1,12 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.surface.SurfaceNoise;
|
import art.arcane.iris.generation.hydrology.surface.SurfaceNoise;
|
||||||
|
import art.arcane.iris.generation.hydrology.surface.SurfaceInletRange;
|
||||||
|
|
||||||
final class HydrologyChannelGeometry {
|
final class HydrologyChannelGeometry {
|
||||||
private final HydrologyFootprintCompiler compiler;
|
private final HydrologyFootprintCompiler compiler;
|
||||||
|
private RiverCourse inletCourse;
|
||||||
|
private SurfaceInletRange inletRange;
|
||||||
|
|
||||||
HydrologyChannelGeometry(HydrologyFootprintCompiler compiler) {
|
HydrologyChannelGeometry(HydrologyFootprintCompiler compiler) {
|
||||||
this.compiler = compiler;
|
this.compiler = compiler;
|
||||||
@@ -141,10 +144,24 @@ final class HydrologyChannelGeometry {
|
|||||||
archedChannel,
|
archedChannel,
|
||||||
roundedSurfaceBed,
|
roundedSurfaceBed,
|
||||||
organicBoundary,
|
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(
|
int organicVerticalVariation(
|
||||||
long seed,
|
long seed,
|
||||||
int x,
|
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();
|
boolean surfaceDrop = segment.type().isSurface() && segment.fallingFluid();
|
||||||
if (surfaceDrop && channel && !falling) {
|
if (surfaceDrop && channel && !falling) {
|
||||||
int maximumIncision = terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision());
|
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 allowedIncision = Math.min(maximumIncision,
|
||||||
(int) StrictMath.floor(maximumIncision * terrain.incisionMultiplier()));
|
(int) StrictMath.floor(maximumIncision * terrain.incisionMultiplier()));
|
||||||
bed = Math.max(bed, Math.min(shape.fluidHead(), terrain.naturalHeight() - allowedIncision));
|
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.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -701,7 +700,6 @@ public final class HydrologyPlanner {
|
|||||||
final HashMap<UndergroundSegmentCapKey, Integer> undergroundSegmentCaps;
|
final HashMap<UndergroundSegmentCapKey, Integer> undergroundSegmentCaps;
|
||||||
final HashMap<Long, HydrologyPoint> routeAnchors;
|
final HashMap<Long, HydrologyPoint> routeAnchors;
|
||||||
final HashMap<Long, HydrologyPoint> surfaceRouteAnchors;
|
final HashMap<Long, HydrologyPoint> surfaceRouteAnchors;
|
||||||
final IdentityHashMap<HydrologySampledGrid, HashMap<SurfaceRouteKey, List<HydrologyPoint>>> surfaceRoutes;
|
|
||||||
final HydrologyCaveCourseFilter.CandidateCache caveCandidates;
|
final HydrologyCaveCourseFilter.CandidateCache caveCandidates;
|
||||||
final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
|
final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
|
||||||
|
|
||||||
@@ -723,7 +721,6 @@ public final class HydrologyPlanner {
|
|||||||
this.undergroundSegmentCaps = new HashMap<>();
|
this.undergroundSegmentCaps = new HashMap<>();
|
||||||
this.routeAnchors = new HashMap<>();
|
this.routeAnchors = new HashMap<>();
|
||||||
this.surfaceRouteAnchors = new HashMap<>();
|
this.surfaceRouteAnchors = new HashMap<>();
|
||||||
this.surfaceRoutes = new IdentityHashMap<>();
|
|
||||||
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
|
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
|
||||||
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
|
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -18,7 +18,7 @@ public record HydrologyPlannerSettings(
|
|||||||
SeaCaves seaCaves,
|
SeaCaves seaCaves,
|
||||||
SurfacePolicyBounds surfacePolicyBounds
|
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;
|
private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4;
|
||||||
|
|
||||||
public HydrologyPlannerSettings {
|
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
|
* {@code sink} is how many blocks the water surface sits below the shaped bank top;
|
||||||
* the channel; zero keeps the water flush with the bank and the bank top always meets the water.
|
* zero keeps the water flush with the bank.
|
||||||
* {@code channel} shapes the wet outline and {@code flow} the waterfalls and plunge basins.
|
* {@code channel} shapes the wet outline and {@code flow} the waterfalls and plunge basins.
|
||||||
*/
|
*/
|
||||||
public record Banks(
|
public record Banks(
|
||||||
|
|||||||
+47
-2
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -94,7 +95,42 @@ final class HydrologyRegionalGraph {
|
|||||||
accumulation[label.downstream.state.id()] += accumulation[label.state.id()];
|
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) {
|
List<Integer> path(Tree tree, int source) {
|
||||||
@@ -160,7 +196,7 @@ final class HydrologyRegionalGraph {
|
|||||||
+ first.routingCost() + second.routingCost()) * policy);
|
+ 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() {
|
int retainedLabelCount() {
|
||||||
BitSet retained = new BitSet(accumulation.length);
|
BitSet retained = new BitSet(accumulation.length);
|
||||||
for (Label label : selected) {
|
for (Label label : selected) {
|
||||||
@@ -187,6 +223,15 @@ final class HydrologyRegionalGraph {
|
|||||||
return selected[source] == null ? 0D : selected[source].state.length();
|
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[] contributions(int source) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (Label label = selected[source]; label != null; label = label.downstream) {
|
for (Label label = selected[source]; label != null; label = label.downstream) {
|
||||||
|
|||||||
-125
@@ -1,31 +1,10 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBankSupport;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
final class HydrologyRegionalHydraulics {
|
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 HydrologyPlannerSettings settings;
|
||||||
private final SurfaceBankSupport bankSupport;
|
|
||||||
private final double minimumHalfWidth;
|
|
||||||
private final double maximumProbedHalfWidth;
|
|
||||||
private final double[] candidateHalfWidths;
|
|
||||||
|
|
||||||
HydrologyRegionalHydraulics(HydrologyPlannerSettings settings) {
|
HydrologyRegionalHydraulics(HydrologyPlannerSettings settings) {
|
||||||
this.settings = 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) {
|
int minimumHead(HydrologyTerrainSample terrain) {
|
||||||
@@ -41,90 +20,6 @@ final class HydrologyRegionalHydraulics {
|
|||||||
return Math.max(settings.seaLevel(), terrain.naturalHeight() - settings.surface().banks().sink());
|
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) {
|
private int minimumHead(HydrologyTerrainSample terrain, int incision) {
|
||||||
int permitted = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
|
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,
|
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);
|
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);
|
List<Integer> indices = graph.path(tree, source);
|
||||||
OutletCandidate destination = tree.outlets().get(tree.root(source));
|
OutletCandidate destination = tree.outlets().get(tree.root(source));
|
||||||
RiverOutlet outlet = destination.outlet();
|
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());
|
ArrayList<String> profiles = new ArrayList<>(grid.node(source).terrain().preferredProfileKeys());
|
||||||
for (int index : indices) {
|
for (int index : indices) {
|
||||||
HydrologyGridNode node = grid.node(index);
|
HydrologyGridNode node = grid.node(index);
|
||||||
guide.add(node.naturalPoint());
|
|
||||||
profiles.retainAll(node.terrain().preferredProfileKeys());
|
profiles.retainAll(node.terrain().preferredProfileKeys());
|
||||||
}
|
}
|
||||||
profiles.retainAll(sample(outlet.connectionPoint().x(), outlet.connectionPoint().z()).preferredProfileKeys());
|
profiles.retainAll(sample(outlet.connectionPoint().x(), outlet.connectionPoint().z()).preferredProfileKeys());
|
||||||
if (origin != null) {
|
if (origin != null) {
|
||||||
profiles.retainAll(sample(origin.outlet().connectionPoint().x(), origin.outlet().connectionPoint().z()).preferredProfileKeys());
|
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) {
|
if (profiles.isEmpty() || guide.size() < 2) {
|
||||||
diagnostic(diagnostics, grid.node(source).id(), grid.node(source).naturalPoint(), origin != null,
|
diagnostic(diagnostics, grid.node(source).id(), grid.node(source).naturalPoint(), origin != null,
|
||||||
HydrologyCandidateRejection.POLICY_EXCLUDED, guide.size());
|
HydrologyCandidateRejection.POLICY_EXCLUDED, guide.size());
|
||||||
return HydrologyRegionalNetwork.EMPTY;
|
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));
|
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,
|
BuildContext context = new BuildContext(grid.node(source), origin, basinSeed, outlet, profile,
|
||||||
new HydrologyRegionalFlow(guide, tree.contributions(source)),
|
new HydrologyRegionalFlow(guide, tree.contributions(source)),
|
||||||
|
|||||||
+33
-40
@@ -18,41 +18,32 @@ final class HydrologyRegionalRoute {
|
|||||||
private static final int MAXIMUM_CORRIDOR_WORKERS = 8;
|
private static final int MAXIMUM_CORRIDOR_WORKERS = 8;
|
||||||
|
|
||||||
private final HydrologyPlanner planner;
|
private final HydrologyPlanner planner;
|
||||||
private final HydrologyRegionalTerrainRefiner terrainRefiner;
|
private final HydrologyRegionalTerrain terrain;
|
||||||
private final HydrologyRegionalHydraulics hydraulics;
|
|
||||||
private final SimplexNoise bends;
|
private final SimplexNoise bends;
|
||||||
private final SimplexNoise details;
|
private final SimplexNoise details;
|
||||||
|
|
||||||
HydrologyRegionalRoute(HydrologyPlanner planner) {
|
HydrologyRegionalRoute(HydrologyPlanner planner) {
|
||||||
this.planner = planner;
|
this.planner = planner;
|
||||||
this.terrainRefiner = new HydrologyRegionalTerrainRefiner(planner);
|
this.terrain = new HydrologyRegionalTerrain(planner);
|
||||||
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
|
||||||
this.bends = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x52454742454e4453L));
|
this.bends = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x52454742454e4453L));
|
||||||
this.details = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x5245474445544149L));
|
this.details = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x5245474445544149L));
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
terrainRefiner.clear();
|
terrain.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
Attempt select(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver,
|
Attempt select(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver,
|
||||||
CandidateAdmission admission) {
|
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);
|
Refinement topology = validateCorridor(guide, profile, coastal);
|
||||||
OceanEntry resolved = topology.rejection() == null ? topology.oceanEntry() : null;
|
OceanEntry resolved = topology.rejection() == null ? topology.oceanEntry() : null;
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
guide = topology.points();
|
guide = topology.points();
|
||||||
receiver = HydrologyOceanReceiver.forConnection(planner.settings, terrainRefiner::sample,
|
receiver = HydrologyOceanReceiver.forConnection(planner.settings, this.terrain::sample,
|
||||||
resolved.landward(), resolved.receiving());
|
resolved.landward(), resolved.receiving());
|
||||||
}
|
}
|
||||||
CandidateSelection selection = new CandidateSelection(admission, resolved);
|
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);
|
Attempt accepted = selectCurve(guide, profile, coastal, receiver, selection, morphology);
|
||||||
return accepted == null ? selection.failure : accepted;
|
return accepted == null ? selection.failure : accepted;
|
||||||
}
|
}
|
||||||
@@ -396,7 +387,7 @@ final class HydrologyRegionalRoute {
|
|||||||
terrain.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth(), null);
|
terrain.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth(), null);
|
||||||
}
|
}
|
||||||
previousTerrain = terrain;
|
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));
|
length += StrictMath.sqrt(first.distanceSquared2D(second));
|
||||||
}
|
}
|
||||||
@@ -419,7 +410,7 @@ final class HydrologyRegionalRoute {
|
|||||||
if (++samples > MAXIMUM_RECEIVER_SAMPLES) {
|
if (++samples > MAXIMUM_RECEIVER_SAMPLES) {
|
||||||
return new Refinement(List.of(), previousPoint, HydrologyCandidateRejection.ROUTE_LIMIT, samples, null);
|
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);
|
HydrologyPoint point = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
|
||||||
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()) {
|
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()) {
|
||||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, 1, null);
|
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,
|
private boolean wetBridge(int x, int z, HydrologyTerrainSample previous, HydrologyTerrainSample next,
|
||||||
String profile, boolean coastal) {
|
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()
|
return bridge != null && bridge.naturalHeight() < planner.settings.seaLevel()
|
||||||
&& bridge.preferredProfileKeys().contains(profile) && previous.drainsInto(bridge) && bridge.drainsInto(next)
|
&& bridge.preferredProfileKeys().contains(profile) && previous.drainsInto(bridge) && bridge.drainsInto(next)
|
||||||
&& (!coastal || next.drainsInto(bridge) && bridge.drainsInto(previous));
|
&& (!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());
|
HydrologyPoint landward = new HydrologyPoint(shore.point().x(), seaLevel, shore.point().z());
|
||||||
receiving = new HydrologyPoint(receiving.x(), seaLevel, receiving.z());
|
receiving = new HydrologyPoint(receiving.x(), seaLevel, receiving.z());
|
||||||
HydrologyTerrainSampler proof = HydrologyOceanReceiver.forConnection(planner.settings,
|
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)
|
if (!shore.terrain().outletAllowed() || !proof.receivingWater(firstWet.x(), firstWet.z(), seaLevel)
|
||||||
|| !receivingChordAllowed(shore, receiving, profile, coastal)) {
|
|| !receivingChordAllowed(shore, receiving, profile, coastal)) {
|
||||||
return new Refinement(List.of(), firstWet, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
return new Refinement(List.of(), firstWet, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||||
@@ -489,7 +480,7 @@ final class HydrologyRegionalRoute {
|
|||||||
double progress = step / (double) steps;
|
double progress = step / (double) steps;
|
||||||
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||||
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
int 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()
|
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()
|
||||||
|| !terrain.preferredProfileKeys().contains(profile) || !previous.drainsInto(terrain)
|
|| !terrain.preferredProfileKeys().contains(profile) || !previous.drainsInto(terrain)
|
||||||
|| coastal && !terrain.drainsInto(previous)) {
|
|| coastal && !terrain.drainsInto(previous)) {
|
||||||
@@ -517,30 +508,32 @@ final class HydrologyRegionalRoute {
|
|||||||
Math.abs(points.get(index).z() - points.get(index - 1).z()));
|
Math.abs(points.get(index).z() - points.get(index - 1).z()));
|
||||||
}
|
}
|
||||||
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
|
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
|
||||||
boolean oceanMouth = corridor.oceanEntry() != null || terrainRefiner.receivingTerminal(points.getLast(), profile, false, receiver);
|
boolean ocean = corridor.oceanEntry() != null || terrain.receivingTerminal(points.getLast(), profile, false, receiver);
|
||||||
int reach = oceanMouth ? Math.min(inlet.length(), (int) StrictMath.floor(count * inlet.courseFraction())) : 0;
|
int reach = ocean ? Math.min(inlet.length(), (int) StrictMath.floor(count * inlet.courseFraction())) : 0;
|
||||||
int rampStart = count - reach - reach / 2;
|
int rampStart = count - reach - reach / 2;
|
||||||
int availableHead = Integer.MAX_VALUE;
|
|
||||||
int station = 0;
|
int station = 0;
|
||||||
HydrologyTerrainSampler bankSampler = terrainRefiner::sample;
|
|
||||||
for (int index = 0; index + 1 < points.size(); index++) {
|
for (int index = 0; index + 1 < points.size(); index++) {
|
||||||
HydrologyPoint first = points.get(index);
|
HydrologyPoint first = points.get(index);
|
||||||
HydrologyPoint second = points.get(index + 1);
|
HydrologyPoint second = points.get(index + 1);
|
||||||
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
|
if (first.y() < second.y()) {
|
||||||
double distance = StrictMath.sqrt(first.distanceSquared2D(second));
|
return new Refinement(List.of(), second, HydrologyCandidateRejection.SURFACE_HEAD_RANGE,
|
||||||
double tangentX = distance == 0D ? 1D : (second.x() - first.x()) / distance;
|
second.y() - first.y(), null);
|
||||||
double tangentZ = distance == 0D ? 0D : (second.z() - first.z()) / distance;
|
}
|
||||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
List<HydrologyPoint> raster = planner.segments.rasterLine(first, second);
|
||||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
for (int step = index == 0 ? 0 : 1; step < raster.size(); step++) {
|
||||||
int x = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
HydrologyPoint sampled = raster.get(step);
|
||||||
int z = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
double progress = raster.size() < 2 ? 0D : step / (double) (raster.size() - 1);
|
||||||
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
|
HydrologyPoint point = new HydrologyPoint(sampled.x(),
|
||||||
availableHead = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
|
(int) StrictMath.round(first.y() + (second.y() - first.y()) * progress), sampled.z());
|
||||||
x, z, tangentX, tangentZ, terrain, availableHead), bankSampler);
|
HydrologyTerrainSample terrain = this.terrain.sample(point.x(), point.z());
|
||||||
int requiredHead = station >= rampStart ? hydraulics.inletMinimumHead(terrain) : hydraulics.minimumHead(terrain);
|
int incision = terrain.surfacePolicy().maximumIncision(planner.settings.surface().maximumIncision());
|
||||||
if (requiredHead > availableHead) {
|
if (station >= rampStart) {
|
||||||
return new Refinement(List.of(), new HydrologyPoint(x, terrain.naturalHeight(), z),
|
incision = Math.max(incision, inlet.maximumIncision());
|
||||||
HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, requiredHead - availableHead, null);
|
}
|
||||||
|
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++;
|
station++;
|
||||||
}
|
}
|
||||||
@@ -596,7 +589,7 @@ final class HydrologyRegionalRoute {
|
|||||||
|
|
||||||
private HydrologyTerrainSample next(int segment, int step, int x, int z) {
|
private HydrologyTerrainSample next(int segment, int step, int x, int z) {
|
||||||
if (workers == 1) {
|
if (workers == 1) {
|
||||||
return terrainRefiner.sample(x, z);
|
return HydrologyRegionalRoute.this.terrain.sample(x, z);
|
||||||
}
|
}
|
||||||
if (cursor == count) {
|
if (cursor == count) {
|
||||||
fill(segment, step);
|
fill(segment, step);
|
||||||
@@ -643,7 +636,7 @@ final class HydrologyRegionalRoute {
|
|||||||
private void sampleRange(int start, int end) {
|
private void sampleRange(int start, int end) {
|
||||||
for (int index = start; index < end; index++) {
|
for (int index = start; index < end; index++) {
|
||||||
try {
|
try {
|
||||||
terrain[index] = terrainRefiner.sample(xs[index], zs[index]);
|
terrain[index] = HydrologyRegionalRoute.this.terrain.sample(xs[index], zs[index]);
|
||||||
} catch (RuntimeException failure) {
|
} catch (RuntimeException failure) {
|
||||||
failures[index] = 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;
|
this.planner = planner;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final double CROSS_DROP_WEIGHT = 3D;
|
|
||||||
static final long ROUTE_ANCHOR_X_SALT = 0x52414e434858L;
|
static final long ROUTE_ANCHOR_X_SALT = 0x52414e434858L;
|
||||||
static final long ROUTE_ANCHOR_Z_SALT = 0x52414e43485aL;
|
static final long ROUTE_ANCHOR_Z_SALT = 0x52414e43485aL;
|
||||||
static final long ROUTE_WORM_PRIMARY_SALT = 0x52575052494dL;
|
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 double ROUTE_TANGENT_SCALE = 0.68D;
|
||||||
static final int ROUTE_TRANSVERSE_CANDIDATES = 2;
|
static final int ROUTE_TRANSVERSE_CANDIDATES = 2;
|
||||||
static final int ROUTE_FALLBACK_TRANSVERSE_CANDIDATES = 4;
|
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;
|
static final int MAXIMUM_SURFACE_BORE_STEP = 4;
|
||||||
|
|
||||||
HydrologyPoint edgeContinuation(
|
HydrologyPoint edgeContinuation(
|
||||||
HydrologySampledGrid grid,
|
HydrologySampledGrid grid,
|
||||||
HydrologyRoutingPlan routing,
|
HydrologyRoutingPlan routing,
|
||||||
HydrologyGridNode downstream,
|
HydrologyGridNode downstream,
|
||||||
RiverOutlet outlet,
|
RiverOutlet outlet
|
||||||
boolean surface
|
|
||||||
) {
|
) {
|
||||||
int continuationIndex = routing.parent()[downstream.index()];
|
int continuationIndex = routing.parent()[downstream.index()];
|
||||||
if (continuationIndex >= 0) {
|
if (continuationIndex >= 0) {
|
||||||
return routeAnchor(grid.node(continuationIndex), surface);
|
return routeAnchor(grid.node(continuationIndex), false);
|
||||||
}
|
}
|
||||||
HydrologyPoint landward = outlet.landwardPoint();
|
HydrologyPoint landward = outlet.landwardPoint();
|
||||||
if (landward.x() != downstream.x() || landward.z() != downstream.z()) {
|
if (landward.x() != downstream.x() || landward.z() != downstream.z()) {
|
||||||
@@ -177,17 +171,15 @@ final class HydrologyRouteGeometry {
|
|||||||
return score + HydrologyHash.unit(tie) * 1.0E-6D;
|
return score + HydrologyHash.unit(tie) * 1.0E-6D;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<HydrologyPoint> refineEdge(
|
List<HydrologyPoint> refineUndergroundEdge(
|
||||||
long upstreamId,
|
long upstreamId,
|
||||||
long downstreamId,
|
long downstreamId,
|
||||||
HydrologyPoint upstream,
|
HydrologyPoint upstream,
|
||||||
HydrologyPoint downstream,
|
HydrologyPoint downstream,
|
||||||
HydrologyPoint continuation,
|
HydrologyPoint continuation,
|
||||||
int transverseCandidates,
|
int transverseCandidates
|
||||||
boolean surface
|
|
||||||
) {
|
) {
|
||||||
int refinement = planner.settings.routing().refinementSpacing();
|
int refinement = planner.settings.routing().refinementSpacing();
|
||||||
int effectiveTransverseCandidates = transverseCandidates;
|
|
||||||
double deltaX = downstream.x() - upstream.x();
|
double deltaX = downstream.x() - upstream.x();
|
||||||
double deltaZ = downstream.z() - upstream.z();
|
double deltaZ = downstream.z() - upstream.z();
|
||||||
double distance = StrictMath.hypot(deltaX, deltaZ);
|
double distance = StrictMath.hypot(deltaX, deltaZ);
|
||||||
@@ -240,7 +232,7 @@ final class HydrologyRouteGeometry {
|
|||||||
nominal,
|
nominal,
|
||||||
progress,
|
progress,
|
||||||
refinement,
|
refinement,
|
||||||
elevationTransition && !surface ? 0 : effectiveTransverseCandidates,
|
elevationTransition ? 0 : transverseCandidates,
|
||||||
upstreamTerrain == null ? null : upstreamTerrain.confinesKey()
|
upstreamTerrain == null ? null : upstreamTerrain.confinesKey()
|
||||||
);
|
);
|
||||||
if (candidates.isEmpty()) {
|
if (candidates.isEmpty()) {
|
||||||
@@ -531,144 +523,6 @@ final class HydrologyRouteGeometry {
|
|||||||
return List.copyOf(candidates.values());
|
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) {
|
double routeTerrainScore(HydrologyTerrainSample terrain) {
|
||||||
return terrain.naturalHeight() * planner.settings.routing().valleyPreference()
|
return terrain.naturalHeight() * planner.settings.routing().valleyPreference()
|
||||||
+ terrain.slope() * planner.settings.routing().slopePenalty()
|
+ terrain.slope() * planner.settings.routing().slopePenalty()
|
||||||
@@ -791,414 +645,6 @@ final class HydrologyRouteGeometry {
|
|||||||
return null;
|
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) {
|
record TerrainTransitionKey(HydrologyPoint start, HydrologyPoint end) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,19 +23,6 @@ final class HydrologyRoutePath {
|
|||||||
return false;
|
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 removeTerrainPits(List<List<RouteCandidate>> layers, RouteCandidate[] route) {
|
||||||
int removed = 0;
|
int removed = 0;
|
||||||
for (int routeIndex = 1; routeIndex < route.length - 1; routeIndex++) {
|
for (int routeIndex = 1; routeIndex < route.length - 1; routeIndex++) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ record HydrologyRoutingPlan(
|
|||||||
int[] outletIndex,
|
int[] outletIndex,
|
||||||
int[] routeLengths,
|
int[] routeLengths,
|
||||||
List<OutletCandidate> outlets,
|
List<OutletCandidate> outlets,
|
||||||
boolean organicSurfaceRequired
|
boolean organicSurfaceRequired,
|
||||||
|
HydrologyDrainageGeometry surfaceDrainage
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,77 @@ package art.arcane.iris.generation.hydrology;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
record HydrologySampledGrid(
|
final class HydrologySampledGrid {
|
||||||
int minimumX,
|
private final int minimumX;
|
||||||
int minimumZ,
|
private final int minimumZ;
|
||||||
int ownerMinimumX,
|
private final int ownerMinimumX;
|
||||||
int ownerMinimumZ,
|
private final int ownerMinimumZ;
|
||||||
int ownerSize,
|
private final int ownerSize;
|
||||||
int width,
|
private final int width;
|
||||||
int spacing,
|
private final int spacing;
|
||||||
List<HydrologyGridNode> nodes
|
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) {
|
HydrologyGridNode node(int index) {
|
||||||
return nodes.get(index);
|
return nodes.get(index);
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-93
@@ -21,6 +21,10 @@ final class HydrologySourcePlanner {
|
|||||||
this.planner = planner;
|
this.planner = planner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HydrologyPlannerSettings settings() {
|
||||||
|
return planner.settings;
|
||||||
|
}
|
||||||
|
|
||||||
static final long EDGE_SALT = 0x45444745L;
|
static final long EDGE_SALT = 0x45444745L;
|
||||||
static final long SURFACE_SOURCE_SALT = 0x53555246414345L;
|
static final long SURFACE_SOURCE_SALT = 0x53555246414345L;
|
||||||
static final long UNDERGROUND_SOURCE_SALT = 0x554e444552L;
|
static final long UNDERGROUND_SOURCE_SALT = 0x554e444552L;
|
||||||
@@ -70,10 +74,7 @@ final class HydrologySourcePlanner {
|
|||||||
double[] potential = new double[nodeCount];
|
double[] potential = new double[nodeCount];
|
||||||
int[] parent = new int[nodeCount];
|
int[] parent = new int[nodeCount];
|
||||||
int[] outletIndex = new int[nodeCount];
|
int[] outletIndex = new int[nodeCount];
|
||||||
int[] minimumNeighborHeights = new int[nodeCount];
|
HydrologyDrainageGraph drainage = grid.drainage(this);
|
||||||
for (HydrologyGridNode node : grid.nodes()) {
|
|
||||||
minimumNeighborHeights[node.index()] = minimumNeighborHeight(grid, node);
|
|
||||||
}
|
|
||||||
Arrays.fill(potential, Double.POSITIVE_INFINITY);
|
Arrays.fill(potential, Double.POSITIVE_INFINITY);
|
||||||
Arrays.fill(parent, -1);
|
Arrays.fill(parent, -1);
|
||||||
Arrays.fill(outletIndex, -1);
|
Arrays.fill(outletIndex, -1);
|
||||||
@@ -91,25 +92,13 @@ final class HydrologySourcePlanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
HydrologyGridNode downstream = grid.node(currentEntry.nodeIndex());
|
HydrologyGridNode downstream = grid.node(currentEntry.nodeIndex());
|
||||||
for (HydrologyGridOffset offset : ROUTING_OFFSETS) {
|
for (int direction = 0; direction < ROUTING_OFFSETS.size(); direction++) {
|
||||||
HydrologyGridNode upstream = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
|
int upstreamIndex = drainage.upstream(downstream.index(), direction, surface);
|
||||||
if (upstream == null || upstream.terrain().ocean() || !upstream.terrain().transitAllowed()
|
if (upstreamIndex < 0) {
|
||||||
|| !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.
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (surface && downstream.terrain().naturalHeight() - upstream.terrain().naturalHeight()
|
HydrologyGridNode upstream = grid.node(upstreamIndex);
|
||||||
> maximumSurfaceEdgeRise(downstream.terrain())) {
|
double edgeCost = drainage.cost(downstream.index(), direction);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
double edgeCost = routeCost(
|
|
||||||
upstream,
|
|
||||||
downstream,
|
|
||||||
offset,
|
|
||||||
minimumNeighborHeights[downstream.index()]
|
|
||||||
);
|
|
||||||
double candidatePotential = currentEntry.potential() + edgeCost;
|
double candidatePotential = currentEntry.potential() + edgeCost;
|
||||||
int existingParent = parent[upstream.index()];
|
int existingParent = parent[upstream.index()];
|
||||||
boolean replace = candidatePotential < potential[upstream.index()] - 1.0E-9D;
|
boolean replace = candidatePotential < potential[upstream.index()] - 1.0E-9D;
|
||||||
@@ -135,7 +124,9 @@ final class HydrologySourcePlanner {
|
|||||||
outletIndex,
|
outletIndex,
|
||||||
routeLengths(parent, grid.width(), planner.settings.routing()),
|
routeLengths(parent, grid.width(), planner.settings.routing()),
|
||||||
List.copyOf(outlets),
|
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.outletIndex(),
|
||||||
routing.routeLengths(),
|
routing.routeLengths(),
|
||||||
routing.outlets(),
|
routing.outlets(),
|
||||||
true
|
true,
|
||||||
|
routing.surfaceDrainage()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,75 +977,75 @@ final class HydrologySourcePlanner {
|
|||||||
HydrologyGridNode downstream = grid.node(downstreamIndex);
|
HydrologyGridNode downstream = grid.node(downstreamIndex);
|
||||||
RiverOutlet outlet = routing.outlets().get(routing.outletIndex()[upstream.index()]).outlet();
|
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 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;
|
||||||
List<HydrologyPoint> centerline = refinedEdges.get(refinementId);
|
if (surface) {
|
||||||
int transverseCandidates = HydrologyRouteGeometry.ROUTE_TRANSVERSE_CANDIDATES;
|
centerline = routing.surfaceDrainage().edge(upstream.index());
|
||||||
if (centerline == null) {
|
} else {
|
||||||
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, surface);
|
centerline = refinedEdges.get(edgeId);
|
||||||
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, surface);
|
int transverseCandidates = HydrologyRouteGeometry.ROUTE_TRANSVERSE_CANDIDATES;
|
||||||
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet, surface);
|
if (centerline == null) {
|
||||||
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
|
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, false);
|
||||||
upstream.id(),
|
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, false);
|
||||||
downstream.id(),
|
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet);
|
||||||
upstreamAnchor.x(),
|
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
|
||||||
upstreamAnchor.z(),
|
upstream.id(),
|
||||||
downstreamAnchor.x(),
|
downstream.id(),
|
||||||
downstreamAnchor.z(),
|
upstreamAnchor.x(),
|
||||||
continuation.x(),
|
upstreamAnchor.z(),
|
||||||
continuation.z(),
|
downstreamAnchor.x(),
|
||||||
transverseCandidates,
|
downstreamAnchor.z(),
|
||||||
surface
|
continuation.x(),
|
||||||
);
|
continuation.z(),
|
||||||
centerline = planner.refinedEdgeCache.get(
|
transverseCandidates
|
||||||
refinedEdgeKey,
|
);
|
||||||
ignored -> planner.routeGeometry.refineEdge(
|
centerline = planner.refinedEdgeCache.get(
|
||||||
upstream.id(),
|
refinedEdgeKey,
|
||||||
downstream.id(),
|
ignored -> planner.routeGeometry.refineUndergroundEdge(
|
||||||
upstreamAnchor,
|
upstream.id(),
|
||||||
downstreamAnchor,
|
downstream.id(),
|
||||||
continuation,
|
upstreamAnchor,
|
||||||
transverseCandidates,
|
downstreamAnchor,
|
||||||
surface
|
continuation,
|
||||||
)
|
transverseCandidates
|
||||||
);
|
)
|
||||||
refinedEdges.put(refinementId, centerline);
|
);
|
||||||
}
|
refinedEdges.put(edgeId, centerline);
|
||||||
if (centerline.isEmpty()) {
|
}
|
||||||
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, surface);
|
if (centerline.isEmpty()) {
|
||||||
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, surface);
|
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream, false);
|
||||||
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet, surface);
|
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream, false);
|
||||||
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
|
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet);
|
||||||
upstream.id(),
|
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
|
||||||
downstream.id(),
|
upstream.id(),
|
||||||
upstreamAnchor.x(),
|
downstream.id(),
|
||||||
upstreamAnchor.z(),
|
upstreamAnchor.x(),
|
||||||
downstreamAnchor.x(),
|
upstreamAnchor.z(),
|
||||||
downstreamAnchor.z(),
|
downstreamAnchor.x(),
|
||||||
continuation.x(),
|
downstreamAnchor.z(),
|
||||||
continuation.z(),
|
continuation.x(),
|
||||||
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES,
|
continuation.z(),
|
||||||
surface
|
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES
|
||||||
);
|
);
|
||||||
centerline = planner.refinedEdgeCache.get(
|
centerline = planner.refinedEdgeCache.get(
|
||||||
refinedEdgeKey,
|
refinedEdgeKey,
|
||||||
ignored -> planner.routeGeometry.refineEdge(
|
ignored -> planner.routeGeometry.refineUndergroundEdge(
|
||||||
upstream.id(),
|
upstream.id(),
|
||||||
downstream.id(),
|
downstream.id(),
|
||||||
upstreamAnchor,
|
upstreamAnchor,
|
||||||
downstreamAnchor,
|
downstreamAnchor,
|
||||||
continuation,
|
continuation,
|
||||||
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES,
|
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES
|
||||||
surface
|
)
|
||||||
)
|
);
|
||||||
);
|
refinedEdges.put(edgeId, centerline);
|
||||||
refinedEdges.put(refinementId, centerline);
|
}
|
||||||
}
|
if (centerline.isEmpty()) {
|
||||||
if (centerline.isEmpty()) {
|
centerline = planner.routeGeometry.constrainedTerrainFallbackEdge(
|
||||||
centerline = planner.routeGeometry.constrainedTerrainFallbackEdge(
|
planner.routeGeometry.routeAnchor(upstream, false),
|
||||||
planner.routeGeometry.routeAnchor(upstream, surface),
|
planner.routeGeometry.routeAnchor(downstream, false)
|
||||||
planner.routeGeometry.routeAnchor(downstream, surface)
|
);
|
||||||
);
|
refinedEdges.put(edgeId, centerline);
|
||||||
refinedEdges.put(refinementId, centerline);
|
}
|
||||||
}
|
}
|
||||||
if (centerline.isEmpty()) {
|
if (centerline.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1081,7 +1073,8 @@ final class HydrologySourcePlanner {
|
|||||||
}
|
}
|
||||||
int outletIndex = routing.outletIndex()[node.index()];
|
int outletIndex = routing.outletIndex()[node.index()];
|
||||||
RiverOutlet outlet = routing.outlets().get(outletIndex).outlet();
|
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(
|
HydrologyTerrainSample terrain = Objects.requireNonNull(
|
||||||
planner.sampleLandBasis(anchor.x(), anchor.z()),
|
planner.sampleLandBasis(anchor.x(), anchor.z()),
|
||||||
"Hydrology route anchor left natural land"
|
"Hydrology route anchor left natural land"
|
||||||
|
|||||||
+3
-503
@@ -9,8 +9,8 @@ import java.util.Comparator;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
final class HydrologySurfaceCoursePlanner {
|
final class HydrologySurfaceCoursePlanner {
|
||||||
private final HydrologyPlanner planner;
|
private final HydrologyPlanner planner;
|
||||||
@@ -28,7 +28,6 @@ final class HydrologySurfaceCoursePlanner {
|
|||||||
static final double SURFACE_GRID_LOCK_TOLERANCE_DEGREES = 5D;
|
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_FRACTION = 0.5D;
|
||||||
static final double SURFACE_MAXIMUM_GRID_LOCKED_RUN = 64D;
|
static final double SURFACE_MAXIMUM_GRID_LOCKED_RUN = 64D;
|
||||||
static final double SURFACE_MAXIMUM_WIDTH_SCALE = 1.18D;
|
|
||||||
|
|
||||||
void compileSurfaceCourses(
|
void compileSurfaceCourses(
|
||||||
HydrologyTileKey key,
|
HydrologyTileKey key,
|
||||||
@@ -407,224 +406,8 @@ final class HydrologySurfaceCoursePlanner {
|
|||||||
if (guide.size() < 2 || guideEdges.size() != guide.size() - 1) {
|
if (guide.size() < 2 || guideEdges.size() != guide.size() - 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ArrayList<HydrologyPoint> routedGuide = new ArrayList<>(guide);
|
return new HydrologyCoursePath(List.copyOf(guide), List.copyOf(guideEdges), List.copyOf(edges),
|
||||||
ArrayList<DrainageEdge> routedGuideEdges = new ArrayList<>(guideEdges);
|
outlet, true, routing.organicSurfaceRequired(), null);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double routeTurnScore(List<HydrologyPoint> route) {
|
double routeTurnScore(List<HydrologyPoint> route) {
|
||||||
@@ -652,289 +435,6 @@ final class HydrologySurfaceCoursePlanner {
|
|||||||
return percentile + maximum * 0.05D;
|
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(
|
record SurfaceCourseBuild(
|
||||||
RiverCourse course,
|
RiverCourse course,
|
||||||
HydrologyCandidateRejection rejection,
|
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.SurfaceBounds;
|
||||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -14,16 +14,15 @@ public final class HydrologySurfaceDropRaster {
|
|||||||
private static final HydrologySurfaceDropRaster EMPTY = new HydrologySurfaceDropRaster(List.of(), Map.of());
|
private static final HydrologySurfaceDropRaster EMPTY = new HydrologySurfaceDropRaster(List.of(), Map.of());
|
||||||
|
|
||||||
private final List<HydrologyColumnSample> columns;
|
private final List<HydrologyColumnSample> columns;
|
||||||
private final Map<Long, HydrologyColumnSample> indexed;
|
private final Long2ObjectOpenHashMap<HydrologyColumnSample> indexed;
|
||||||
private final Map<Long, OceanReceiver> oceanReceivers;
|
private final Map<Long, OceanReceiver> oceanReceivers;
|
||||||
|
|
||||||
private HydrologySurfaceDropRaster(List<HydrologyColumnSample> columns, Map<Long, OceanReceiver> oceanReceivers) {
|
private HydrologySurfaceDropRaster(List<HydrologyColumnSample> columns, Map<Long, OceanReceiver> oceanReceivers) {
|
||||||
this.columns = List.copyOf(columns);
|
this.columns = List.copyOf(columns);
|
||||||
Map<Long, HydrologyColumnSample> indexed = new HashMap<>(columns.size());
|
this.indexed = new Long2ObjectOpenHashMap<>(columns.size());
|
||||||
for (HydrologyColumnSample sample : columns) {
|
for (HydrologyColumnSample sample : columns) {
|
||||||
indexed.put(RiverFootprint.pack(sample.x(), sample.z()), sample);
|
indexed.put(RiverFootprint.pack(sample.x(), sample.z()), sample);
|
||||||
}
|
}
|
||||||
this.indexed = Map.copyOf(indexed);
|
|
||||||
this.oceanReceivers = Map.copyOf(oceanReceivers);
|
this.oceanReceivers = Map.copyOf(oceanReceivers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,10 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
private final Object publicationLock = new Object();
|
private final Object publicationLock = new Object();
|
||||||
private final BooleanSupplier waitingForbidden;
|
private final BooleanSupplier waitingForbidden;
|
||||||
private volatile SharedCacheScope sharedCacheScope;
|
private volatile SharedCacheScope sharedCacheScope;
|
||||||
private volatile StudioHydrologyTileStore persistentStore;
|
private volatile PreparedHydrologyTileStore persistentStore;
|
||||||
private volatile boolean neighbourPrefetchEnabled;
|
private volatile boolean neighbourPrefetchEnabled;
|
||||||
private volatile Runnable terrainPreparation;
|
private volatile Runnable terrainPreparation;
|
||||||
|
private PregenerationScope pregenerationScope;
|
||||||
private int demandBatches;
|
private int demandBatches;
|
||||||
|
|
||||||
public HydrologyTileCache(HydrologyPlanner planner) {
|
public HydrologyTileCache(HydrologyPlanner planner) {
|
||||||
@@ -338,27 +339,31 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
SharedTileKey sharedKey = sharedCacheScope == null ? null : new SharedTileKey(sharedCacheScope, key);
|
SharedTileKey sharedKey = sharedCacheScope == null ? null : new SharedTileKey(sharedCacheScope, key);
|
||||||
HydrologyTile shared = sharedKey == null ? null : SHARED_TILES.getIfPresent(sharedKey);
|
HydrologyTile shared = sharedKey == null ? null : SHARED_TILES.getIfPresent(sharedKey);
|
||||||
if (shared != null && validSharedTile(shared, sharedKey)) {
|
if (shared != null && validSharedTile(shared, sharedKey)) {
|
||||||
|
PreparedHydrologyTileStore store = persistentStore;
|
||||||
|
if (store != null && store.load(key).isEmpty()) {
|
||||||
|
persistTile(store, shared);
|
||||||
|
}
|
||||||
planner.reuseResolvedTile(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;
|
return shared;
|
||||||
}
|
}
|
||||||
if (shared != null) {
|
if (shared != null) {
|
||||||
SHARED_TILES.invalidate(sharedKey);
|
SHARED_TILES.invalidate(sharedKey);
|
||||||
}
|
}
|
||||||
StudioHydrologyTileStore store = persistentStore;
|
PreparedHydrologyTileStore store = persistentStore;
|
||||||
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
|
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
|
||||||
return planAdmitted(key, planningEpoch, sharedKey, store);
|
return planAdmitted(key, planningEpoch, sharedKey, store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
|
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
|
||||||
SharedTileKey sharedKey, StudioHydrologyTileStore store) {
|
SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
|
||||||
if (store != null && persistentStudioKey(key)) {
|
if (store != null) {
|
||||||
HydrologyTile persisted = store.load(key).orElse(null);
|
HydrologyTile persisted = store.load(key).orElse(null);
|
||||||
if (persisted != null) {
|
if (persisted != null) {
|
||||||
SHARED_TILES.put(sharedKey, persisted);
|
SHARED_TILES.put(sharedKey, persisted);
|
||||||
planner.reuseResolvedTile(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;
|
return persisted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,18 +374,12 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
|
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
|
||||||
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
|
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
|
||||||
if (existing == null) {
|
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()) {
|
if (store != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
|
||||||
try {
|
persistTile(store, planned);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return planned;
|
return planned;
|
||||||
} catch (RuntimeException failure) {
|
} 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) {
|
private static boolean interrupted(Throwable failure) {
|
||||||
if (Thread.currentThread().isInterrupted()) {
|
if (Thread.currentThread().isInterrupted()) {
|
||||||
return true;
|
return true;
|
||||||
@@ -510,6 +518,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
public void clear() {
|
public void clear() {
|
||||||
ArrayList<CompletableFuture<ChunkColumns>> cancelledQueries = new ArrayList<>();
|
ArrayList<CompletableFuture<ChunkColumns>> cancelledQueries = new ArrayList<>();
|
||||||
synchronized (prefetchQueue) {
|
synchronized (prefetchQueue) {
|
||||||
|
if (pregenerationScope != null) {
|
||||||
|
pregenerationScope.close();
|
||||||
|
}
|
||||||
synchronized (publicationLock) {
|
synchronized (publicationLock) {
|
||||||
cacheEpoch.incrementAndGet();
|
cacheEpoch.incrementAndGet();
|
||||||
planner.clearOwnerDrafts();
|
planner.clearOwnerDrafts();
|
||||||
@@ -547,20 +558,29 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
neighbourPrefetchEnabled = enabled;
|
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 tileSize = planner.settings().routing().tileSize();
|
||||||
int minimumBlockX = Math.subtractExact(centerBlockX, tileSize);
|
int publicationRadius = planner.settings().publicationRadius();
|
||||||
int minimumBlockZ = Math.subtractExact(centerBlockZ, tileSize);
|
TileBounds bounds = new TileBounds(
|
||||||
int maximumBlockX = Math.addExact(centerBlockX, tileSize);
|
tileCoordinate(Math.subtractExact(area.minimumBlockX(), publicationRadius), tileSize),
|
||||||
int maximumBlockZ = Math.addExact(centerBlockZ, tileSize);
|
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), publicationRadius), tileSize),
|
||||||
enqueuePrefetchArea(prefetchAreaKeys(
|
tileCoordinate(Math.addExact(area.maximumBlockX(), publicationRadius), tileSize),
|
||||||
minimumBlockX,
|
tileCoordinate(Math.addExact(area.maximumBlockZ(), publicationRadius), tileSize));
|
||||||
minimumBlockZ,
|
List<HydrologyTileKey> keys = prefetchAreaKeys(
|
||||||
maximumBlockX,
|
Math.max(area.minimumBlockX(), (long) area.centerBlockX() - tileSize),
|
||||||
maximumBlockZ,
|
Math.max(area.minimumBlockZ(), (long) area.centerBlockZ() - tileSize),
|
||||||
centerBlockX,
|
Math.min(area.maximumBlockX(), (long) area.centerBlockX() + tileSize),
|
||||||
centerBlockZ
|
Math.min(area.maximumBlockZ(), (long) area.centerBlockZ() + tileSize),
|
||||||
), true);
|
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) {
|
public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) {
|
||||||
@@ -570,16 +590,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
sharedCacheScope = Objects.requireNonNull(scope, "scope");
|
sharedCacheScope = Objects.requireNonNull(scope, "scope");
|
||||||
persistentStore = persistentRoot == null
|
persistentStore = persistentRoot == null
|
||||||
? null
|
? null
|
||||||
: new StudioHydrologyTileStore(persistentRoot, scope, planner.settings().routing().tileSize());
|
: new PreparedHydrologyTileStore(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -622,6 +633,11 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
.join();
|
.join();
|
||||||
planning.clear();
|
planning.clear();
|
||||||
diagnosing.clear();
|
diagnosing.clear();
|
||||||
|
synchronized (prefetchQueue) {
|
||||||
|
if (pregenerationScope != null) {
|
||||||
|
pregenerationScope.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,11 +849,11 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
|
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
|
||||||
centreBlockX, centreBlockZ), false);
|
centreBlockX, centreBlockZ), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HydrologyTileKey> prefetchAreaKeys(int minimumBlockX, int minimumBlockZ, int maximumBlockX,
|
private List<HydrologyTileKey> prefetchAreaKeys(long minimumBlockX, long minimumBlockZ, long maximumBlockX,
|
||||||
int maximumBlockZ, int centreBlockX, int centreBlockZ) {
|
long maximumBlockZ, int centreBlockX, int centreBlockZ) {
|
||||||
int tileSize = planner.settings().routing().tileSize();
|
int tileSize = planner.settings().routing().tileSize();
|
||||||
int publicationRadius = planner.settings().publicationRadius();
|
int publicationRadius = planner.settings().publicationRadius();
|
||||||
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
|
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
|
||||||
@@ -858,11 +874,20 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, boolean discardOutsideArea) {
|
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, PregenerationScope scope) {
|
||||||
Set<HydrologyTileKey> area = discardOutsideArea ? new HashSet<>(keys) : Set.of();
|
Set<HydrologyTileKey> area = scope != null ? new HashSet<>(keys) : Set.of();
|
||||||
tiles.cleanUp();
|
tiles.cleanUp();
|
||||||
synchronized (prefetchQueue) {
|
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 -> {
|
prefetchQueue.removeIf(key -> {
|
||||||
if (!area.contains(key)) {
|
if (!area.contains(key)) {
|
||||||
queuedPrefetches.remove(key);
|
queuedPrefetches.remove(key);
|
||||||
@@ -903,6 +928,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
|
|
||||||
private boolean enqueuePrefetch(HydrologyTileKey key) {
|
private boolean enqueuePrefetch(HydrologyTileKey key) {
|
||||||
if (closed.get()
|
if (closed.get()
|
||||||
|
|| pregenerationScope != null && !pregenerationScope.bounds.contains(key)
|
||||||
|| tiles.getIfPresent(key) != null
|
|| tiles.getIfPresent(key) != null
|
||||||
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|
||||||
|| queuedPrefetches.contains(key)
|
|| queuedPrefetches.contains(key)
|
||||||
@@ -1056,7 +1082,8 @@ public final class HydrologyTileCache implements AutoCloseable {
|
|||||||
if (!queuedPrefetches.remove(key)) {
|
if (!queuedPrefetches.remove(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (tiles.getIfPresent(key) == null) {
|
if ((pregenerationScope == null || pregenerationScope.bounds.contains(key))
|
||||||
|
&& tiles.getIfPresent(key) == null) {
|
||||||
return key;
|
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 record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
|
||||||
private long estimatedBytes() {
|
private long estimatedBytes() {
|
||||||
long bytes = 64L + 8L * columns.length;
|
long bytes = 64L + 8L * columns.length;
|
||||||
|
|||||||
@@ -492,14 +492,6 @@ final class HydrologyTributaryPlanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
|
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) {
|
if (segments.getLast().downstreamHeadY() < stemHead) {
|
||||||
int shortfall = stemHead - segments.getLast().downstreamHeadY();
|
int shortfall = stemHead - segments.getLast().downstreamHeadY();
|
||||||
if (shortfall > TRIBUTARY_BACKWATER_LIMIT) {
|
if (shortfall > TRIBUTARY_BACKWATER_LIMIT) {
|
||||||
|
|||||||
+3
-3
@@ -10,16 +10,16 @@ import lombok.experimental.Accessors;
|
|||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@Description("Bounds terrain removal outside the wet river channel.")
|
@Description("Bounds terrain shaping outside the wet river channel.")
|
||||||
public class IrisRiverExcavationConfig {
|
public class IrisRiverExcavationConfig {
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
@MaxNumber(64)
|
@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;
|
private int maximumDepth = 8;
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
@MaxNumber(64)
|
@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;
|
private int maximumWidth = 16;
|
||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ public class IrisSurfaceRiverBankConfig {
|
|||||||
|
|
||||||
@MinNumber(0.5)
|
@MinNumber(0.5)
|
||||||
@MaxNumber(12)
|
@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;
|
private double blendSlope = 3D;
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
@@ -33,7 +33,7 @@ public class IrisSurfaceRiverBankConfig {
|
|||||||
@Description("Widest eroded band outside the shore, in blocks.")
|
@Description("Widest eroded band outside the shore, in blocks.")
|
||||||
private int maximumBlendWidth = 32;
|
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();
|
private IrisRiverExcavationConfig excavation = new IrisRiverExcavationConfig();
|
||||||
|
|
||||||
@Description("Show the biome's deeper layers on eroded banks instead of the surface layer.")
|
@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)
|
@MinNumber(0)
|
||||||
@MaxNumber(3)
|
@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;
|
private int sink = 0;
|
||||||
|
|
||||||
@MinNumber(1)
|
@MinNumber(1)
|
||||||
@MaxNumber(32)
|
@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;
|
private int maximumIncision = 10;
|
||||||
|
|
||||||
@MinNumber(0)
|
@MinNumber(0)
|
||||||
|
|||||||
+14
-9
@@ -41,8 +41,8 @@ import java.util.Set;
|
|||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
import java.util.zip.GZIPOutputStream;
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
final class StudioHydrologyTileStore {
|
final class PreparedHydrologyTileStore {
|
||||||
private static final int SCHEMA_VERSION = 5;
|
private static final int SCHEMA_VERSION = 6;
|
||||||
private static final long MAXIMUM_COMPRESSED_BYTES = 128L * 1024L * 1024L;
|
private static final long MAXIMUM_COMPRESSED_BYTES = 128L * 1024L * 1024L;
|
||||||
private static final long MAXIMUM_DECOMPRESSED_BYTES = 512L * 1024L * 1024L;
|
private static final long MAXIMUM_DECOMPRESSED_BYTES = 512L * 1024L * 1024L;
|
||||||
private static final Gson GSON = new GsonBuilder()
|
private static final Gson GSON = new GsonBuilder()
|
||||||
@@ -51,19 +51,21 @@ final class StudioHydrologyTileStore {
|
|||||||
.create();
|
.create();
|
||||||
|
|
||||||
private final Path directory;
|
private final Path directory;
|
||||||
|
private final String scopeIdentity;
|
||||||
private final HydrologyTileCache.SharedCacheScope scope;
|
private final HydrologyTileCache.SharedCacheScope scope;
|
||||||
private final int expectedTileSize;
|
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");
|
this.scope = Objects.requireNonNull(scope, "scope");
|
||||||
if (expectedTileSize < 1) {
|
if (expectedTileSize < 1) {
|
||||||
throw new IllegalArgumentException("Hydrology tile size must be positive.");
|
throw new IllegalArgumentException("Hydrology tile size must be positive.");
|
||||||
}
|
}
|
||||||
this.expectedTileSize = expectedTileSize;
|
this.expectedTileSize = expectedTileSize;
|
||||||
|
this.scopeIdentity = scopeFingerprint(scope);
|
||||||
this.directory = Objects.requireNonNull(root, "root")
|
this.directory = Objects.requireNonNull(root, "root")
|
||||||
.toAbsolutePath()
|
.toAbsolutePath()
|
||||||
.normalize()
|
.normalize()
|
||||||
.resolve(scopeFingerprint(scope));
|
.resolve(scopeIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<HydrologyTile> load(HydrologyTileKey key) {
|
Optional<HydrologyTile> load(HydrologyTileKey key) {
|
||||||
@@ -85,6 +87,7 @@ final class StudioHydrologyTileStore {
|
|||||||
PersistedTile persisted = GSON.fromJson(reader, PersistedTile.class);
|
PersistedTile persisted = GSON.fromJson(reader, PersistedTile.class);
|
||||||
if (persisted == null
|
if (persisted == null
|
||||||
|| persisted.schemaVersion() != SCHEMA_VERSION
|
|| persisted.schemaVersion() != SCHEMA_VERSION
|
||||||
|
|| !scopeIdentity.equals(persisted.scopeIdentity())
|
||||||
|| !key.equals(persisted.key())
|
|| !key.equals(persisted.key())
|
||||||
|| persisted.worldSeed() != scope.worldSeed()
|
|| persisted.worldSeed() != scope.worldSeed()
|
||||||
|| persisted.settingsFingerprint() != scope.settingsFingerprint()) {
|
|| persisted.settingsFingerprint() != scope.settingsFingerprint()) {
|
||||||
@@ -99,7 +102,7 @@ final class StudioHydrologyTileStore {
|
|||||||
|
|
||||||
void save(HydrologyTile tile) throws IOException {
|
void save(HydrologyTile tile) throws IOException {
|
||||||
if (!valid(tile, tile.key())) {
|
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);
|
Files.createDirectories(directory);
|
||||||
Path target = file(tile.key());
|
Path target = file(tile.key());
|
||||||
@@ -109,10 +112,10 @@ final class StudioHydrologyTileStore {
|
|||||||
OutputStream buffered = new BufferedOutputStream(raw);
|
OutputStream buffered = new BufferedOutputStream(raw);
|
||||||
OutputStream compressed = new GZIPOutputStream(buffered);
|
OutputStream compressed = new GZIPOutputStream(buffered);
|
||||||
Writer writer = new OutputStreamWriter(compressed, StandardCharsets.UTF_8)) {
|
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) {
|
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 {
|
try {
|
||||||
Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
Files.move(staged, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
@@ -161,6 +164,7 @@ final class StudioHydrologyTileStore {
|
|||||||
|
|
||||||
private record PersistedTile(
|
private record PersistedTile(
|
||||||
int schemaVersion,
|
int schemaVersion,
|
||||||
|
String scopeIdentity,
|
||||||
HydrologyTileKey key,
|
HydrologyTileKey key,
|
||||||
long worldSeed,
|
long worldSeed,
|
||||||
long settingsFingerprint,
|
long settingsFingerprint,
|
||||||
@@ -174,7 +178,7 @@ final class StudioHydrologyTileStore {
|
|||||||
List<HydrologyDiagnosticCandidate> localDiagnosticCandidates,
|
List<HydrologyDiagnosticCandidate> localDiagnosticCandidates,
|
||||||
List<HydrologyColumnSample> columns
|
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());
|
ArrayList<PersistedCourse> courses = new ArrayList<>(tile.courses().size());
|
||||||
for (RiverCourse course : tile.courses()) {
|
for (RiverCourse course : tile.courses()) {
|
||||||
courses.add(PersistedCourse.from(course));
|
courses.add(PersistedCourse.from(course));
|
||||||
@@ -187,6 +191,7 @@ final class StudioHydrologyTileStore {
|
|||||||
}
|
}
|
||||||
return new PersistedTile(
|
return new PersistedTile(
|
||||||
SCHEMA_VERSION,
|
SCHEMA_VERSION,
|
||||||
|
scopeIdentity,
|
||||||
tile.key(),
|
tile.key(),
|
||||||
tile.worldSeed(),
|
tile.worldSeed(),
|
||||||
tile.settingsFingerprint(),
|
tile.settingsFingerprint(),
|
||||||
@@ -375,7 +380,7 @@ final class StudioHydrologyTileStore {
|
|||||||
private void advance(long amount) throws IOException {
|
private void advance(long amount) throws IOException {
|
||||||
count += amount;
|
count += amount;
|
||||||
if (count > limit) {
|
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 downstreamZ,
|
||||||
int continuationX,
|
int continuationX,
|
||||||
int continuationZ,
|
int continuationZ,
|
||||||
int transverseCandidates,
|
int transverseCandidates
|
||||||
boolean surface
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -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);
|
cache.prefetchArea(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ, centreBlockX, centreBlockZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
|
public HydrologyTileCache.PregenerationScope preparePregeneration(HydrologyTileCache.PregenerationArea area) {
|
||||||
cache.preparePregeneration(centerBlockX, centerBlockZ);
|
return cache.preparePregeneration(area);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setNeighbourPrefetchEnabled(boolean enabled) {
|
public void setNeighbourPrefetchEnabled(boolean enabled) {
|
||||||
|
|||||||
+96
-42
@@ -31,15 +31,17 @@ public final class ErosionFieldCompiler {
|
|||||||
private final HydrologyPlannerSettings.Surface surface;
|
private final HydrologyPlannerSettings.Surface surface;
|
||||||
private final HydrologyTerrainSampler sampler;
|
private final HydrologyTerrainSampler sampler;
|
||||||
private final int seaLevel;
|
private final int seaLevel;
|
||||||
|
private final double maximumShoreWidth;
|
||||||
|
|
||||||
public ErosionFieldCompiler(
|
public ErosionFieldCompiler(
|
||||||
HydrologyPlannerSettings.Surface surface,
|
HydrologyPlannerSettings settings,
|
||||||
HydrologyTerrainSampler sampler,
|
HydrologyTerrainSampler sampler
|
||||||
int seaLevel
|
|
||||||
) {
|
) {
|
||||||
this.surface = Objects.requireNonNull(surface, "surface");
|
Objects.requireNonNull(settings, "settings");
|
||||||
|
this.surface = settings.surface();
|
||||||
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||||
this.seaLevel = seaLevel;
|
this.seaLevel = settings.seaLevel();
|
||||||
|
this.maximumShoreWidth = Math.max(surface.shoreWidth(), settings.widestShoreBiomeWidth());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ErosionField compile(
|
public ErosionField compile(
|
||||||
@@ -80,11 +82,12 @@ public final class ErosionFieldCompiler {
|
|||||||
SurfaceRasterContext context
|
SurfaceRasterContext context
|
||||||
) {
|
) {
|
||||||
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit, ponds, 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,
|
PreparedField prepare(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley,
|
||||||
SurfaceTerminal terminal) {
|
SurfaceTerminal terminal, int inletStart) {
|
||||||
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
|
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
|
||||||
boolean[] basin = basins(valley, channel, count);
|
boolean[] basin = basins(valley, channel, count);
|
||||||
double[] bendOffsets = SurfaceBendProfile.offsets(centerline, channel);
|
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())
|
radii[station] = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + surface.banks().roughness())
|
||||||
+ Math.max(stationShore + widest, stationBand)) + 1;
|
+ 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,
|
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 oceanStart = oceanStart(centerline, valley, terminal);
|
||||||
|
int inletStart = prepared.inletStart();
|
||||||
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(nearest.size());
|
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(nearest.size());
|
||||||
LongArrayList wetKeys = new LongArrayList();
|
LongArrayList wetKeys = new LongArrayList();
|
||||||
for (long key : nearest.keySet()) {
|
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)) {
|
if (!context.boundary().allows(cellX, cellZ) || context.drops().connects(cellX, cellZ, head)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
|
||||||
double halfWidth = channel.halfWidth()[station];
|
double halfWidth = channel.halfWidth()[station];
|
||||||
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
|
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
|
||||||
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
|
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
|
||||||
@@ -176,6 +179,13 @@ public final class ErosionFieldCompiler {
|
|||||||
Math.max(0.5D, channelSettings.outlineMinimumRatio() * halfWidth),
|
Math.max(0.5D, channelSettings.outlineMinimumRatio() * halfWidth),
|
||||||
Math.min(channelSettings.outlineMaximumRatio() * halfWidth, outline));
|
Math.min(channelSettings.outlineMaximumRatio() * halfWidth, outline));
|
||||||
boolean wet = cellDistance <= outline + 0.25D;
|
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
|
boolean mouthLand = terminal == SurfaceTerminal.OCEAN_MOUTH && head == seaLevel
|
||||||
&& SurfaceCellAdmission.mouthLand(terrain, seaLevel);
|
&& SurfaceCellAdmission.mouthLand(terrain, seaLevel);
|
||||||
if (!SurfaceCellAdmission.writable(terrain, seaLevel) && !mouthLand) {
|
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())
|
+ erosion.bedNoise() * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
|
||||||
+ (basin[station] ? flow.plungeBasinDepth() : 0D);
|
+ (basin[station] ? flow.plungeBasinDepth() : 0D);
|
||||||
int bed = Math.min(natural, head - Math.max(1, (int) StrictMath.round(local)));
|
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));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
|
||||||
wetKeys.add(key);
|
wetKeys.add(key);
|
||||||
continue;
|
continue;
|
||||||
@@ -212,7 +226,6 @@ public final class ErosionFieldCompiler {
|
|||||||
// geometric shore with the bank biome.
|
// geometric shore with the bank biome.
|
||||||
boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D;
|
boolean shoreBiome = cellDistance <= outline + terrain.shoreBiomeWidth(shoreWidth) + 0.25D;
|
||||||
SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK;
|
SurfaceRole dryRole = shoreBiome ? SurfaceRole.SHORE : SurfaceRole.BANK;
|
||||||
double bankDistance = cellDistance - outline;
|
|
||||||
if (bankDistance > erosion.excavation().maximumWidth()) {
|
if (bankDistance > erosion.excavation().maximumWidth()) {
|
||||||
if (shoreBiome) {
|
if (shoreBiome) {
|
||||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
||||||
@@ -220,14 +233,14 @@ public final class ErosionFieldCompiler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cellDistance <= outline + shore + 0.25D) {
|
if (cellDistance <= outline + shore + 0.25D) {
|
||||||
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(cellDistance, outline, shore)));
|
int height = bankTop + (int) StrictMath.round(shoreRise * benchProgress(cellDistance, outline, shore));
|
||||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
height = boundedBankHeight(terrain, height, erosion.excavation());
|
||||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
|
int shoreTop = bankTop + (int) StrictMath.round(shoreRise);
|
||||||
int cut = natural - shoreTop;
|
int cut = natural - shoreTop;
|
||||||
if (cut <= 0 || !erosion.enabled() || !terrain.erosion()) {
|
if (cut == 0 || !erosion.enabled() || !terrain.erosion()) {
|
||||||
if (shoreBiome) {
|
if (shoreBiome) {
|
||||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, natural, natural, false));
|
||||||
}
|
}
|
||||||
@@ -243,8 +256,8 @@ public final class ErosionFieldCompiler {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
|
int height = (int) StrictMath.round(shoreTop + cut * blend(progress, erosion));
|
||||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
height = boundedBankHeight(terrain, height, erosion.excavation());
|
||||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, dryRole, height, height, false));
|
||||||
}
|
}
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
@@ -255,7 +268,7 @@ public final class ErosionFieldCompiler {
|
|||||||
}
|
}
|
||||||
int uncontained = contain(columns, wetKeys, sink, bounds, context.drops());
|
int uncontained = contain(columns, wetKeys, sink, bounds, context.drops());
|
||||||
connectSteps(columns, wetKeys);
|
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) {
|
if (wasWet) {
|
||||||
bed = Math.min(bed, existing.height());
|
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));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
|
||||||
if (!wasWet) {
|
if (!wasWet) {
|
||||||
wetKeys.add(key);
|
wetKeys.add(key);
|
||||||
@@ -353,24 +370,24 @@ public final class ErosionFieldCompiler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (distance <= outline + shore + 0.25D) {
|
if (distance <= outline + shore + 0.25D) {
|
||||||
int height = Math.min(natural, bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore)));
|
int height = bankTop + (int) StrictMath.round(shoreRise * benchProgress(distance, outline, shore));
|
||||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
height = boundedBankHeight(terrain, height, erosion.excavation());
|
||||||
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
|
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int cut = natural - shoreTop;
|
int cut = natural - shoreTop;
|
||||||
if (cut <= 0 || !erosion.enabled() || !erode) {
|
if (cut == 0 || !erosion.enabled() || !erode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
double width = Math.max(banks.minimumBlendWidth(),
|
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));
|
width = Math.min(width, Math.max(0.25D, erosion.excavation().maximumWidth() - shore));
|
||||||
double progress = (distance - outline - shore) / width;
|
double progress = (distance - outline - shore) / width;
|
||||||
if (progress >= 1D) {
|
if (progress >= 1D) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int height = Math.min(natural, (int) StrictMath.round(shoreTop + cut * blend(progress, erosion)));
|
int height = (int) StrictMath.round(shoreTop + cut * blend(progress, erosion));
|
||||||
height = boundedBankHeight(natural, height, erosion.excavation());
|
height = boundedBankHeight(terrain, height, erosion.excavation());
|
||||||
if (existing != null && existing.height() <= height) {
|
if (existing != null && existing.height() <= height) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -401,7 +418,8 @@ public final class ErosionFieldCompiler {
|
|||||||
int cellX = (int) StrictMath.round(centreX + Math.cos(angle) * ring);
|
int cellX = (int) StrictMath.round(centreX + Math.cos(angle) * ring);
|
||||||
int cellZ = (int) StrictMath.round(centreZ + Math.sin(angle) * ring);
|
int cellZ = (int) StrictMath.round(centreZ + Math.sin(angle) * ring);
|
||||||
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,20 +530,17 @@ public final class ErosionFieldCompiler {
|
|||||||
int uncontained = 0;
|
int uncontained = 0;
|
||||||
for (int index = 0; index < wetKeys.size(); index++) {
|
for (int index = 0; index < wetKeys.size(); index++) {
|
||||||
SurfaceColumn wet = columns.get(wetKeys.getLong(index));
|
SurfaceColumn wet = columns.get(wetKeys.getLong(index));
|
||||||
if (bounds != null && !bounds.contains(wet.x(), wet.z())) {
|
boolean owned = bounds == null || bounds.contains(wet.x(), wet.z());
|
||||||
continue;
|
uncontained += lip(columns, wet, CARDINALS, sink, owned, drops);
|
||||||
}
|
|
||||||
uncontained += lip(columns, wet, CARDINALS, sink, true, drops);
|
|
||||||
lip(columns, wet, DIAGONALS, sink, false, drops);
|
lip(columns, wet, DIAGONALS, sink, false, drops);
|
||||||
}
|
}
|
||||||
return uncontained;
|
return uncontained;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The bank beside water must reach the natural ground the head was taken from: the water's own
|
* The bank beside water reaches the planned water level plus the sink. Low neighbours may be
|
||||||
* block level plus the sink. A neighbour lower than that is raised to it. The solver takes the head
|
* filled within the local channel and bank terrain limits. Any remaining shortfall below the
|
||||||
* from the banks it samples at each station. A missed low cell can retain its natural ground,
|
* water head stays uncontained and is counted.
|
||||||
* but cannot be raised above it. A shortfall below the water head stays uncontained and is counted.
|
|
||||||
*/
|
*/
|
||||||
private int lip(
|
private int lip(
|
||||||
Long2ObjectOpenHashMap<SurfaceColumn> columns,
|
Long2ObjectOpenHashMap<SurfaceColumn> columns,
|
||||||
@@ -558,8 +573,7 @@ public final class ErosionFieldCompiler {
|
|||||||
}
|
}
|
||||||
int required = wet.headY() + sink;
|
int required = wet.headY() + sink;
|
||||||
if (neighbour.height() < required) {
|
if (neighbour.height() < required) {
|
||||||
int ceiling = neighbour.terrain().naturalHeight();
|
int raised = boundedBankHeight(neighbour.terrain(), required, surface.banks().erosion().excavation());
|
||||||
int raised = Math.min(ceiling, required);
|
|
||||||
neighbour = neighbour.withHeight(raised);
|
neighbour = neighbour.withHeight(raised);
|
||||||
columns.put(key, neighbour);
|
columns.put(key, neighbour);
|
||||||
}
|
}
|
||||||
@@ -584,18 +598,51 @@ public final class ErosionFieldCompiler {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int boundedBankHeight(int natural, int target, HydrologyPlannerSettings.Excavation limits) {
|
private int boundedBankHeight(HydrologyTerrainSample terrain, int target, HydrologyPlannerSettings.Excavation limits) {
|
||||||
return Math.max(natural - limits.maximumDepth(), Math.min(natural, target));
|
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,
|
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;
|
long excavation = 0L;
|
||||||
int[] stationVolumes = new int[centerline.size()];
|
int[] stationVolumes = new int[centerline.size()];
|
||||||
int deepest = 0;
|
int deepest = 0;
|
||||||
for (SurfaceColumn column : columns.values()) {
|
for (SurfaceColumn column : columns.values()) {
|
||||||
if (column.apron() || column.role() == SurfaceRole.CHANNEL
|
if (column.apron() || bounds != null && !bounds.contains(column.x(), column.z())) {
|
||||||
|| 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;
|
continue;
|
||||||
}
|
}
|
||||||
int cut = Math.max(0, column.terrain().naturalHeight() - column.height());
|
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,
|
return new ErosionField(columns, uncontained, HydrologyCandidateRejection.SURFACE_WATER_CONTAINMENT,
|
||||||
uncontained, excavation);
|
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, 0, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, excavation);
|
||||||
}
|
}
|
||||||
return new ErosionField(columns, uncontained, null, 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()) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
long firstKey = wetKeys.getLong(0);
|
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 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);
|
int probeZ = (int) StrictMath.round(centerline.z()[station] + centerline.normalZ(station) * probe * direction);
|
||||||
HydrologyTerrainSample terrain = sampler.sample(probeX, probeZ);
|
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();
|
double width = cut * banks.blendSlope() * channel.bankMultiplier()[station] + erosion.blendBaseWidth();
|
||||||
widths[station][side] = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), width));
|
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);
|
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.HydrologyCandidateRejection;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
|
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.HydrologyPlannerSettings;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyPoint;
|
import art.arcane.iris.generation.hydrology.HydrologyPoint;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||||
@@ -15,6 +16,8 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class SurfaceCourseBuilder {
|
public final class SurfaceCourseBuilder {
|
||||||
|
private static final long COASTAL_DROP_SALT = 0x434f41535446414cL;
|
||||||
|
|
||||||
private final HydrologyPlannerSettings.Surface surface;
|
private final HydrologyPlannerSettings.Surface surface;
|
||||||
private final HydrologyTerrainSampler sampler;
|
private final HydrologyTerrainSampler sampler;
|
||||||
private final HydrologyGeometrySampler geometry;
|
private final HydrologyGeometrySampler geometry;
|
||||||
@@ -45,14 +48,14 @@ public final class SurfaceCourseBuilder {
|
|||||||
boolean directOcean = terminal == SurfaceTerminal.OCEAN_MOUTH;
|
boolean directOcean = terminal == SurfaceTerminal.OCEAN_MOUTH;
|
||||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, geometry)
|
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, geometry)
|
||||||
.build(centerline, profileKey, directOcean);
|
.build(centerline, profileKey, directOcean);
|
||||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, seaLevel, minimumCourseLength)
|
ValleyProfile valley = new RiverProfileSolver(new RiverProfileSolver.Options(surface, seaLevel), sampler)
|
||||||
.solve(centerline, channel, terminal, terminalHead);
|
.solve(path, centerline, channel, terminal, terminalHead, minimumCourseLength);
|
||||||
if (!valley.accepted()) {
|
if (!valley.accepted()) {
|
||||||
return SurfaceCourseResult.rejected(valley.rejection(), valley.rejectionDetail());
|
return SurfaceCourseResult.rejected(valley.rejection(), valley.rejectionDetail());
|
||||||
}
|
}
|
||||||
int exposed = valley.exposedStations();
|
int exposed = valley.exposedStations();
|
||||||
boolean coastalDrop = directOcean && valley.head()[exposed - 1] > seaLevel;
|
boolean coastalDrop = directOcean && valley.head()[exposed - 1] > seaLevel;
|
||||||
int stations = coastalDrop ? exposed + 1 : exposed;
|
int stations = exposed;
|
||||||
int[] x = new int[stations];
|
int[] x = new int[stations];
|
||||||
int[] z = new int[stations];
|
int[] z = new int[stations];
|
||||||
int[] head = new int[stations];
|
int[] head = new int[stations];
|
||||||
@@ -65,40 +68,52 @@ public final class SurfaceCourseBuilder {
|
|||||||
width[station] = channel.halfWidth()[station] * 2D;
|
width[station] = channel.halfWidth()[station] * 2D;
|
||||||
depth[station] = channel.depth()[station];
|
depth[station] = channel.depth()[station];
|
||||||
}
|
}
|
||||||
if (coastalDrop) {
|
ArrayList<HydraulicSegment> segments = fallingTransitions(SurfaceSegmentLabeler.label(
|
||||||
int last = exposed - 1;
|
worldSeed, courseId, x, z, head, width, depth, surface.banks()), directOcean);
|
||||||
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()));
|
|
||||||
if (segments.isEmpty()) {
|
if (segments.isEmpty()) {
|
||||||
return SurfaceCourseResult.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, stations);
|
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();
|
HydraulicSegment last = segments.getLast();
|
||||||
int lastStation = stations - 1;
|
|
||||||
return new SurfaceCourseResult(
|
return new SurfaceCourseResult(
|
||||||
segments,
|
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().widthAt(last.centerline().size() - 1))),
|
||||||
Math.max(1, (int) StrictMath.ceil(last.channelProfile().depthAt(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,
|
null,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
private List<HydraulicSegment> fallingTransitions(List<HydraulicSegment> labelled) {
|
|
||||||
|
private ArrayList<HydraulicSegment> fallingTransitions(List<HydraulicSegment> labelled, boolean directOcean) {
|
||||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(labelled);
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(labelled);
|
||||||
for (int index = 0; index + 1 < segments.size(); index++) {
|
for (int index = 0; index + 1 < segments.size(); index++) {
|
||||||
HydraulicSegment fall = segments.get(index);
|
HydraulicSegment fall = segments.get(index);
|
||||||
@@ -112,6 +127,9 @@ public final class SurfaceCourseBuilder {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
int incision = terrain.surfacePolicy().maximumIncision(surface.maximumIncision());
|
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()));
|
incision = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
|
||||||
if (terrain.naturalHeight() - fall.downstreamHeadY() + fall.depth() > incision) {
|
if (terrain.naturalHeight() - fall.downstreamHeadY() + fall.depth() > incision) {
|
||||||
continue;
|
continue;
|
||||||
@@ -142,7 +160,7 @@ public final class SurfaceCourseBuilder {
|
|||||||
receiver.fallingFluid(), receiver.receivingPool(),
|
receiver.fallingFluid(), receiver.receivingPool(),
|
||||||
receiver.centerline().subList(1, receiver.centerline().size()), receiverProfile));
|
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) {
|
if (prepared.receivingSampler == null) {
|
||||||
prepared.receivingSampler = HydrologyOceanReceiver.forCourse(settings, sampler, course);
|
prepared.receivingSampler = HydrologyOceanReceiver.forCourse(settings, sampler, course);
|
||||||
prepared.offsets = stationOffsets(course.segments());
|
prepared.inlet = SurfaceInletRange.forCourse(settings, course, prepared.receivingSampler);
|
||||||
}
|
}
|
||||||
HydrologyTerrainSampler receivingSampler = prepared.receivingSampler;
|
HydrologyTerrainSampler receivingSampler = prepared.receivingSampler;
|
||||||
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, receivingSampler, geometry, course,
|
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, receivingSampler, geometry, course,
|
||||||
bounds == null ? null : bounds.expand(2));
|
bounds == null ? null : bounds.expand(2));
|
||||||
int[] offsets = prepared.offsets;
|
|
||||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>();
|
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>();
|
||||||
int uncontained = 0;
|
int uncontained = 0;
|
||||||
long excavation = 0L;
|
long excavation = 0L;
|
||||||
@@ -90,7 +89,7 @@ public final class SurfaceFootprintCompiler {
|
|||||||
while (after < course.segments().size() && exposedSegment(course.segments().get(after))) {
|
while (after < course.segments().size() && exposedSegment(course.segments().get(after))) {
|
||||||
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());
|
columns.addAll(run.columns());
|
||||||
uncontained += run.uncontainedWetCells();
|
uncontained += run.uncontainedWetCells();
|
||||||
excavation += run.bankExcavation();
|
excavation += run.bankExcavation();
|
||||||
@@ -123,7 +122,7 @@ public final class SurfaceFootprintCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PreparedRun prepareRun(RiverCourse course, int first, int after, boolean pool,
|
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);
|
List<HydraulicSegment> exposed = course.segments().subList(first, after);
|
||||||
Stations stations = stations(exposed);
|
Stations stations = stations(exposed);
|
||||||
boolean fallingEnd = after < course.segments().size() && course.segments().get(after).fallingFluid();
|
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(),
|
: new HydrologyPlannerSettings.Ponds(first == 0 ? settings.surface().banks().ponds().source() : disabled.source(),
|
||||||
fallingEnd ? disabled.terminal() : settings.surface().banks().ponds().terminal());
|
fallingEnd ? disabled.terminal() : settings.surface().banks().ponds().terminal());
|
||||||
SurfaceRunBoundary boundary = runBoundary(course, first, after, centerline, channel);
|
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,
|
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,
|
private SurfaceFootprint compileRun(PreparedCourse prepared, int first, int after, int stationOffset,
|
||||||
@@ -152,7 +151,7 @@ public final class SurfaceFootprintCompiler {
|
|||||||
RiverCourse course = prepared.course;
|
RiverCourse course = prepared.course;
|
||||||
PreparedRun run = prepared.runs[first];
|
PreparedRun run = prepared.runs[first];
|
||||||
if (run == null) {
|
if (run == null) {
|
||||||
run = prepareRun(course, first, after, pool, receivingSampler);
|
run = prepareRun(course, first, after, pool, receivingSampler, prepared.inlet);
|
||||||
prepared.runs[first] = run;
|
prepared.runs[first] = run;
|
||||||
}
|
}
|
||||||
List<HydraulicSegment> exposed = run.exposed();
|
List<HydraulicSegment> exposed = run.exposed();
|
||||||
@@ -210,26 +209,6 @@ public final class SurfaceFootprintCompiler {
|
|||||||
+ settings.surface().banks().maximumBlendWidth() + settings.surface().shoreWidth() + 2D;
|
+ 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) {
|
private ChannelProfile acceptedProfile(Stations stations, SurfaceCenterline centerline) {
|
||||||
int count = centerline.size();
|
int count = centerline.size();
|
||||||
double[] halfWidth = new double[count];
|
double[] halfWidth = new double[count];
|
||||||
@@ -406,7 +385,7 @@ public final class SurfaceFootprintCompiler {
|
|||||||
private final RiverCourse course;
|
private final RiverCourse course;
|
||||||
private final PreparedRun[] runs;
|
private final PreparedRun[] runs;
|
||||||
private HydrologyTerrainSampler receivingSampler;
|
private HydrologyTerrainSampler receivingSampler;
|
||||||
private int[] offsets;
|
private SurfaceInletRange inlet;
|
||||||
|
|
||||||
private PreparedCourse(SurfaceFootprintCompiler compiler, RiverCourse course) {
|
private PreparedCourse(SurfaceFootprintCompiler compiler, RiverCourse course) {
|
||||||
this.compiler = compiler;
|
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
|
@Override
|
||||||
public int getOutputRadius() {
|
public int getOutputRadius() {
|
||||||
return hasCollisionRules() ? 0 : getRadius();
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getInputRadius() {
|
public int getInputRadius() {
|
||||||
return hasCollisionRules() ? calculateInputRadius(getRadius(), true) : 0;
|
return calculateInputRadius(getRadius(), hasCollisionRules());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -140,10 +140,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
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);
|
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
||||||
replaySourceChunks(
|
replaySourceChunks(
|
||||||
x,
|
x,
|
||||||
@@ -169,12 +165,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
sourceChunkX,
|
sourceChunkX,
|
||||||
sourceChunkZ
|
sourceChunkZ
|
||||||
);
|
);
|
||||||
replaySourcePredecessors(
|
if (hasCollisionRules()) {
|
||||||
sourceChunkX,
|
replaySourcePredecessors(
|
||||||
sourceChunkZ,
|
sourceChunkX,
|
||||||
getRadius(),
|
sourceChunkZ,
|
||||||
(predecessorX, predecessorZ) -> generateOrigin(scratch, predecessorX, predecessorZ, context)
|
getRadius(),
|
||||||
);
|
(predecessorX, predecessorZ) -> generateOrigin(scratch, predecessorX, predecessorZ, context)
|
||||||
|
);
|
||||||
|
}
|
||||||
int checkpoint = scratch.mutationCheckpoint();
|
int checkpoint = scratch.mutationCheckpoint();
|
||||||
generateOrigin(scratch, sourceChunkX, sourceChunkZ, context);
|
generateOrigin(scratch, sourceChunkX, sourceChunkZ, context);
|
||||||
return scratch.sourcePlanSince(checkpoint);
|
return scratch.sourcePlanSince(checkpoint);
|
||||||
|
|||||||
+3
-1
@@ -54,7 +54,9 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
|
|
||||||
void apply(ObjectSourcePlan plan) {
|
void apply(ObjectSourcePlan plan) {
|
||||||
for (Mutation mutation : plan.mutations()) {
|
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.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.pack.value.IrisColor;
|
import art.arcane.iris.pack.value.IrisColor;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
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.generation.terrain.IrisDimensionCarvingResolver;
|
||||||
import art.arcane.iris.structure.object.IrisObject;
|
import art.arcane.iris.structure.object.IrisObject;
|
||||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||||
@@ -424,15 +423,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
IrisDimensionCarvingResolver.State state
|
IrisDimensionCarvingResolver.State state
|
||||||
) {
|
) {
|
||||||
int worldY = y + getWorld().minHeight();
|
int worldY = y + getWorld().minHeight();
|
||||||
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
|
return IrisDimensionCarvingResolver.resolveBiome(this, x, worldY, z, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome resolveDepthCaveBiome(
|
private IrisBiome resolveDepthCaveBiome(
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ final class EngineHotloader {
|
|||||||
previousGeneration.mantleStorageDirectory(),
|
previousGeneration.mantleStorageDirectory(),
|
||||||
previousGeneration.runtimeKernel(),
|
previousGeneration.runtimeKernel(),
|
||||||
previousGeneration.transitionPlan());
|
previousGeneration.transitionPlan());
|
||||||
|
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
|
||||||
|
assembly.target, assembly.runtimeKernel, assembly.transitionPlan, engine.isStudio());
|
||||||
engine.runtimeAssembly.set(assembly);
|
engine.runtimeAssembly.set(assembly);
|
||||||
EngineRuntime next;
|
EngineRuntime next;
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
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.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
|
||||||
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
|
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
|
||||||
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
||||||
|
EngineRuntimeBuilder.enablePreparedCache(assembly, cacheIdentity, engine.isStudio());
|
||||||
GenerationRuntime nextGeneration = previousGeneration.withComplex(
|
GenerationRuntime nextGeneration = previousGeneration.withComplex(
|
||||||
assembly.cacheId,
|
assembly.cacheId,
|
||||||
assembly.complex,
|
assembly.complex,
|
||||||
|
|||||||
@@ -57,13 +57,14 @@ final class EngineRuntimeBuilder {
|
|||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineRuntime buildRuntime() {
|
EngineRuntime buildRuntime(PreparedHydrologyCacheIdentity cacheIdentity) {
|
||||||
return buildRuntime(
|
return buildRuntime(
|
||||||
engine.getTarget(),
|
engine.getTarget(),
|
||||||
engine.getInitialMantleStorageDirectory(),
|
engine.getInitialMantleStorageDirectory(),
|
||||||
engine.getInitialKernelVersion(),
|
engine.getInitialKernelVersion(),
|
||||||
engine.getInitialTransitionPlan(),
|
engine.getInitialTransitionPlan(),
|
||||||
null);
|
null,
|
||||||
|
cacheIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
|
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
|
||||||
@@ -73,7 +74,9 @@ final class EngineRuntimeBuilder {
|
|||||||
active.mantleStorageDirectory(),
|
active.mantleStorageDirectory(),
|
||||||
active.kernelVersion(),
|
active.kernelVersion(),
|
||||||
active.transitionPlan(),
|
active.transitionPlan(),
|
||||||
active.mantle());
|
active.mantle(),
|
||||||
|
PreparedHydrologyCacheIdentity.capture(runtimeTarget, selectRuntimeKernel(active.kernelVersion()),
|
||||||
|
active.transitionPlan(), engine.isStudio()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private EngineRuntime buildRuntime(
|
private EngineRuntime buildRuntime(
|
||||||
@@ -81,7 +84,8 @@ final class EngineRuntimeBuilder {
|
|||||||
Path mantleStorageDirectory,
|
Path mantleStorageDirectory,
|
||||||
GenerationKernelRegistry.Version kernelVersion,
|
GenerationKernelRegistry.Version kernelVersion,
|
||||||
TransitionGenerationPlan transitionPlan,
|
TransitionGenerationPlan transitionPlan,
|
||||||
EngineMantle transferredMantle
|
EngineMantle transferredMantle,
|
||||||
|
PreparedHydrologyCacheIdentity cacheIdentity
|
||||||
) {
|
) {
|
||||||
RuntimeAssembly assembly = new RuntimeAssembly(
|
RuntimeAssembly assembly = new RuntimeAssembly(
|
||||||
RuntimeAssembly.nextRuntimeId(),
|
RuntimeAssembly.nextRuntimeId(),
|
||||||
@@ -135,6 +139,7 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
|
||||||
BiomeMaxes biomeMaxes = computeBiomeMaxes();
|
BiomeMaxes biomeMaxes = computeBiomeMaxes();
|
||||||
|
enablePreparedCache(assembly, cacheIdentity, engine.isStudio());
|
||||||
return assembly.freezeRuntime(biomeMaxes);
|
return assembly.freezeRuntime(biomeMaxes);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
||||||
@@ -167,6 +172,8 @@ final class EngineRuntimeBuilder {
|
|||||||
mantleStorageDirectory,
|
mantleStorageDirectory,
|
||||||
selectRuntimeKernel(kernelVersion),
|
selectRuntimeKernel(kernelVersion),
|
||||||
transitionPlan);
|
transitionPlan);
|
||||||
|
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
|
||||||
|
runtimeTarget, assembly.runtimeKernel, transitionPlan, engine.isStudio());
|
||||||
engine.runtimeAssembly.set(assembly);
|
engine.runtimeAssembly.set(assembly);
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||||
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
|
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
|
||||||
@@ -184,7 +191,9 @@ final class EngineRuntimeBuilder {
|
|||||||
assembly.mode,
|
assembly.mode,
|
||||||
assembly.dimensionStackContext
|
assembly.dimensionStackContext
|
||||||
);
|
);
|
||||||
return assembly.freezeGeneration(computeBiomeMaxes());
|
BiomeMaxes biomeMaxes = computeBiomeMaxes();
|
||||||
|
enablePreparedCache(assembly, cacheIdentity, engine.isStudio());
|
||||||
|
return assembly.freezeGeneration(biomeMaxes);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
||||||
if (cleanupFailure != 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) {
|
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
|
||||||
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(
|
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(
|
||||||
previous,
|
previous,
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
hydrologyNoiseCaches = List.of();
|
hydrologyNoiseCaches = List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
void enableStudioHydrologyCache(String runtimeIdentity, Path persistentRoot) {
|
void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) {
|
||||||
if (hydrologyRuntime != null) {
|
if (hydrologyRuntime != null) {
|
||||||
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
|
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
|
||||||
}
|
}
|
||||||
@@ -1934,6 +1934,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
private Engine engine;
|
private Engine engine;
|
||||||
private IrisGenerator[] generators;
|
private IrisGenerator[] generators;
|
||||||
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
|
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
|
||||||
|
private boolean integerCoordinates;
|
||||||
private boolean inUse;
|
private boolean inUse;
|
||||||
|
|
||||||
private boolean isInUse() {
|
private boolean isInUse() {
|
||||||
@@ -1945,6 +1946,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
this.generators = generators;
|
this.generators = generators;
|
||||||
this.cachedBounds = cachedBounds;
|
this.cachedBounds = cachedBounds;
|
||||||
|
this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D<?>;
|
||||||
this.inUse = true;
|
this.inUse = true;
|
||||||
if (sampleSource.get() != complex.baseBiomeStream) {
|
if (sampleSource.get() != complex.baseBiomeStream) {
|
||||||
sampleCache.clear();
|
sampleCache.clear();
|
||||||
@@ -1967,10 +1969,12 @@ public class IrisComplex implements DataProvider {
|
|||||||
@Override
|
@Override
|
||||||
public NoiseBounds noise(double xx, double zz) {
|
public NoiseBounds noise(double xx, double zz) {
|
||||||
try {
|
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) {
|
if (bx == null) {
|
||||||
bx = complex.baseBiomeStream.get(xx, zz);
|
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);
|
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.IrisDimension;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
|
||||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
import art.arcane.iris.pack.PackValidationCache;
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import com.google.common.util.concurrent.AtomicDouble;
|
import com.google.common.util.concurrent.AtomicDouble;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
@@ -303,7 +302,8 @@ public class IrisEngine implements Engine {
|
|||||||
getData().registerEngine(this);
|
getData().registerEngine(this);
|
||||||
_t0 = M.ms();
|
_t0 = M.ms();
|
||||||
long phaseStartedAt = System.nanoTime();
|
long phaseStartedAt = System.nanoTime();
|
||||||
String studioCacheIdentity = currentStudioCacheIdentity();
|
PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture(
|
||||||
|
getTarget(), EngineRuntimeBuilder.selectRuntimeKernel(initialKernelVersion), initialTransitionPlan, studio);
|
||||||
getData().loadPrefetch(this);
|
getData().loadPrefetch(this);
|
||||||
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
|
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
|
||||||
diagnostics.logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
|
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());
|
IrisLogging.notice("Engine init: " + requiredTarget.getWorld().name() + "/" + requiredTarget.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed());
|
||||||
_t0 = M.ms();
|
_t0 = M.ms();
|
||||||
phaseStartedAt = System.nanoTime();
|
phaseStartedAt = System.nanoTime();
|
||||||
EngineRuntime initialRuntime = runtimeBuilder.buildRuntime();
|
EngineRuntime initialRuntime = runtimeBuilder.buildRuntime(cacheIdentity);
|
||||||
enableStableStudioCache(initialRuntime, studioCacheIdentity);
|
|
||||||
runtimeBuilder.publishRuntime(initialRuntime, null);
|
runtimeBuilder.publishRuntime(initialRuntime, null);
|
||||||
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
||||||
diagnostics.logPackCompatSummary();
|
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) {
|
public void startEntryHydrology(int blockX, int blockZ) {
|
||||||
GenerationRuntimeBinding binding = getActiveGenerationRuntimeBinding();
|
GenerationRuntimeBinding binding = getActiveGenerationRuntimeBinding();
|
||||||
if (!backgroundTasks.scheduleTrackedTask(() -> prepareEntryHydrology(binding, blockX, blockZ))) {
|
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);
|
return new BankColumn(flat, flat);
|
||||||
}
|
}
|
||||||
if (base >= natural.topY()) {
|
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);
|
double rise = Math.min(amplitude, natural.topY() - base);
|
||||||
IntArrayList boundaries = new IntArrayList(natural.spanCount() * 2 + 8);
|
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) {
|
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY) {
|
||||||
return resolveRootEntry(engine, worldY, threadState());
|
return resolveRootEntry(engine, worldY, threadState());
|
||||||
}
|
}
|
||||||
@@ -35,6 +46,36 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
|
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
|
||||||
State resolvedState = state == null ? threadState() : state;
|
State resolvedState = state == null ? threadState() : state;
|
||||||
IrisData data = resolvedState.bind(engine);
|
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)) {
|
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
|
||||||
return resolvedState.rootEntriesByWorldY.get(worldY);
|
return resolvedState.rootEntriesByWorldY.get(worldY);
|
||||||
}
|
}
|
||||||
@@ -59,13 +100,7 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ) {
|
private static IrisDimensionCarvingEntry resolveBoundFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, IrisData data, State resolvedState) {
|
||||||
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);
|
|
||||||
if (rootEntry == null) {
|
if (rootEntry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -100,22 +135,6 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return current;
|
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) {
|
private static IrisBiome resolveBoundEntryBiome(IrisData data, IrisDimensionCarvingEntry entry, State state) {
|
||||||
if (state.biomeCache.containsKey(entry)) {
|
if (state.biomeCache.containsKey(entry)) {
|
||||||
return state.biomeCache.get(entry);
|
return state.biomeCache.get(entry);
|
||||||
|
|||||||
@@ -92,9 +92,10 @@ public final class Terrain3DRuntime {
|
|||||||
int gridX = Math.floorDiv(x, STEP) * STEP;
|
int gridX = Math.floorDiv(x, STEP) * STEP;
|
||||||
int gridZ = Math.floorDiv(z, STEP) * STEP;
|
int gridZ = Math.floorDiv(z, STEP) * STEP;
|
||||||
Anchor northWest = anchor(gridX, gridZ);
|
Anchor northWest = anchor(gridX, gridZ);
|
||||||
Anchor northEast = anchor(gridX + STEP, gridZ);
|
Anchor northEast = x == gridX ? northWest : anchor(gridX + STEP, gridZ);
|
||||||
Anchor southWest = anchor(gridX, gridZ + STEP);
|
Anchor southWest = z == gridZ ? northWest : anchor(gridX, gridZ + STEP);
|
||||||
Anchor southEast = anchor(gridX + STEP, gridZ + STEP);
|
Anchor southEast = x == gridX ? southWest
|
||||||
|
: z == gridZ ? northEast : anchor(gridX + STEP, gridZ + STEP);
|
||||||
double dx = (x - gridX) / (double) STEP;
|
double dx = (x - gridX) / (double) STEP;
|
||||||
double dz = (z - gridZ) / (double) STEP;
|
double dz = (z - gridZ) / (double) STEP;
|
||||||
double amplitude = interpolate(northWest.amplitude(), northEast.amplitude(),
|
double amplitude = interpolate(northWest.amplitude(), northEast.amplitude(),
|
||||||
|
|||||||
@@ -49,14 +49,25 @@ public final class PackValidationRegistry {
|
|||||||
if (packRoot == null || result == null) {
|
if (packRoot == null || result == null) {
|
||||||
return;
|
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()) {
|
if (packRoot == null || result == null || contentFingerprint == null || contentFingerprint.isBlank()) {
|
||||||
return;
|
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(
|
public static PackValidationResult publishMatchingCopy(
|
||||||
@@ -118,7 +129,7 @@ public final class PackValidationRegistry {
|
|||||||
return new RootState(
|
return new RootState(
|
||||||
current.generation(),
|
current.generation(),
|
||||||
false,
|
false,
|
||||||
new RootValidation(result, ""));
|
new RootValidation(result, "", ""));
|
||||||
});
|
});
|
||||||
return published.get();
|
return published.get();
|
||||||
}
|
}
|
||||||
@@ -140,6 +151,14 @@ public final class PackValidationRegistry {
|
|||||||
: state.validation().result();
|
: 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) {
|
public static PackValidationResult requireLoadable(String packName) {
|
||||||
if (packName == null || packName.isBlank()) {
|
if (packName == null || packName.isBlank()) {
|
||||||
throw new IllegalArgumentException("Pack name is required for validation");
|
throw new IllegalArgumentException("Pack name is required for validation");
|
||||||
@@ -250,10 +269,11 @@ public final class PackValidationRegistry {
|
|||||||
if (sourceState == null
|
if (sourceState == null
|
||||||
|| sourceState.mutating()
|
|| sourceState.mutating()
|
||||||
|| sourceState.validation() == null
|
|| sourceState.validation() == null
|
||||||
|| !copiedContentFingerprint.equals(sourceState.validation().contentFingerprint())) {
|
|| !copiedContentFingerprint.equals(sourceState.validation().contentFingerprint())
|
||||||
|
|| !sourceState.validation().contextFingerprint().equals(PackValidationCache.contextFingerprint())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return sourceState.validation();
|
return ROOT_STATES.get(normalizedSource) == sourceState ? sourceState.validation() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long nextGeneration(RootState current) {
|
private static long nextGeneration(RootState current) {
|
||||||
@@ -297,6 +317,7 @@ public final class PackValidationRegistry {
|
|||||||
requireOpen();
|
requireOpen();
|
||||||
pendingValidation = new RootValidation(
|
pendingValidation = new RootValidation(
|
||||||
Objects.requireNonNull(result, "Pack validation result"),
|
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) {
|
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) {
|
public synchronized void bindGenerationRegistryContract(GenerationRegistryContract contract) {
|
||||||
GenerationRegistryContract required = Objects.requireNonNull(contract, "contract");
|
GenerationRegistryContract required = Objects.requireNonNull(contract, "contract");
|
||||||
if (generationRegistryContract != null && !generationRegistryContract.equals(required)) {
|
if (generationRegistryContract != null && !generationRegistryContract.equals(required)) {
|
||||||
|
|||||||
@@ -716,6 +716,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
} catch (IOException failure) {
|
} catch (IOException failure) {
|
||||||
throw new IllegalStateException("Unable to load the active Iris generation transition.", failure);
|
throw new IllegalStateException("Unable to load the active Iris generation transition.", failure);
|
||||||
}
|
}
|
||||||
|
engineTarget.getData().bindGenerationRegistryContract(epoch.registryContract());
|
||||||
IrisEngine createdEngine = new IrisEngine(
|
IrisEngine createdEngine = new IrisEngine(
|
||||||
engineTarget,
|
engineTarget,
|
||||||
mode,
|
mode,
|
||||||
@@ -1319,7 +1320,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
|
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
|
||||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
|
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
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();
|
blocks.apply();
|
||||||
if (historyScope != null) {
|
if (historyScope != null) {
|
||||||
historyScope.claimGeneratedSemantics();
|
historyScope.claimGeneratedSemantics();
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ public class StudioSVC implements IrisService {
|
|||||||
packRoot,
|
packRoot,
|
||||||
GenerationPackFingerprint.CURRENT_VERSION
|
GenerationPackFingerprint.CURRENT_VERSION
|
||||||
);
|
);
|
||||||
validatePublishedPack(packRoot);
|
validateGenerationCandidate(packRoot, fingerprint);
|
||||||
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
||||||
try {
|
try {
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
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(
|
static PackValidationResult validatePublishedPack(
|
||||||
Path packRoot,
|
Path packRoot,
|
||||||
Path validatedSource,
|
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.INMS;
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
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 MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
|
||||||
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
|
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
|
||||||
private static final long ADMISSION_WAIT_BOUND_MS = 500L;
|
private static final long ADMISSION_WAIT_BOUND_MS = 500L;
|
||||||
|
private static final int HYDROLOGY_PREFETCH_HALO_BLOCKS = 512;
|
||||||
private final World world;
|
private final World world;
|
||||||
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
|
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
|
||||||
private final IrisPaperLikeBackendMode paperLikeBackendMode;
|
private final IrisPaperLikeBackendMode paperLikeBackendMode;
|
||||||
@@ -117,6 +119,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
private final AtomicBoolean holdsWorkerBoost = new AtomicBoolean();
|
||||||
private volatile Engine metricsEngine;
|
private volatile Engine metricsEngine;
|
||||||
private volatile Mantle cachedMantle;
|
private volatile Mantle cachedMantle;
|
||||||
|
private volatile HydrologyTileCache.PregenerationScope hydrologyPrefetchScope;
|
||||||
private final PregenMantleBackpressure backpressure;
|
private final PregenMantleBackpressure backpressure;
|
||||||
|
|
||||||
public AsyncPregenMethod(World world, int unusedThreads) {
|
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) {
|
if (engine == null || engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
|
||||||
return;
|
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) {
|
private boolean inBounds(int rx, int rz) {
|
||||||
@@ -827,6 +841,11 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
|||||||
releaseWorkerThreadBoost();
|
releaseWorkerThreadBoost();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
HydrologyTileCache.PregenerationScope scope = hydrologyPrefetchScope;
|
||||||
|
if (scope != null) {
|
||||||
|
scope.close();
|
||||||
|
hydrologyPrefetchScope = null;
|
||||||
|
}
|
||||||
if (interrupted) {
|
if (interrupted) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,9 +196,15 @@ public class PregenTask {
|
|||||||
|
|
||||||
public void iterateRegions(Spiraled s) {
|
public void iterateRegions(Spiraled s) {
|
||||||
Bound bound = bounds.region();
|
Bound bound = bounds.region();
|
||||||
new Spiraler(bound.sizeX, bound.sizeZ, ((x, z) -> {
|
int centerRegionX = PowerOfTwoCoordinates.blockToRegionFloor(center.getX());
|
||||||
if (bound.check(x, z)) s.on(x, z);
|
int centerRegionZ = PowerOfTwoCoordinates.blockToRegionFloor(center.getZ());
|
||||||
})).setOffset(PowerOfTwoCoordinates.blockToRegionFloor(center.getX()), PowerOfTwoCoordinates.blockToRegionFloor(center.getZ())).drain();
|
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) {
|
public void iterateChunks(int rX, int rZ, Spiraled s) {
|
||||||
@@ -291,10 +297,10 @@ public class PregenTask {
|
|||||||
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.CHUNK_BITS)
|
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.CHUNK_BITS)
|
||||||
);
|
);
|
||||||
region = new Bound(
|
region = new Bound(
|
||||||
PowerOfTwoCoordinates.blockToRegionFloor(minX),
|
PowerOfTwoCoordinates.chunkToRegion(chunk.minX()),
|
||||||
PowerOfTwoCoordinates.blockToRegionFloor(minZ),
|
PowerOfTwoCoordinates.chunkToRegion(chunk.minZ()),
|
||||||
PowerOfTwoCoordinates.ceilDivPow2(maxX, PowerOfTwoCoordinates.REGION_BITS),
|
PowerOfTwoCoordinates.chunkToRegion(chunk.maxX()),
|
||||||
PowerOfTwoCoordinates.ceilDivPow2(maxZ, PowerOfTwoCoordinates.REGION_BITS)
|
PowerOfTwoCoordinates.chunkToRegion(chunk.maxZ())
|
||||||
);
|
);
|
||||||
requireSaneSpan(region);
|
requireSaneSpan(region);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -24,7 +24,7 @@ public class HydrologyCacheBudgetTest {
|
|||||||
HydrologyCacheBudget budget = HydrologyCacheBudget.forHeap(heap, regionalEnabled);
|
HydrologyCacheBudget budget = HydrologyCacheBudget.forHeap(heap, regionalEnabled);
|
||||||
long total = budget.tileBytes() + budget.columnBytes() + budget.diagnosticBytes()
|
long total = budget.tileBytes() + budget.columnBytes() + budget.diagnosticBytes()
|
||||||
+ budget.routingBytes() + budget.ownerBytes() + budget.edgeBytes() + budget.regionalDraftBytes()
|
+ 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 <= Math.clamp(heap / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE));
|
||||||
assertTrue(total >= 8L * MEBIBYTE);
|
assertTrue(total >= 8L * MEBIBYTE);
|
||||||
assertEquals(budget.tileBytes() / 6L, budget.columnBytes());
|
assertEquals(budget.tileBytes() / 6L, budget.columnBytes());
|
||||||
@@ -40,7 +40,7 @@ public class HydrologyCacheBudgetTest {
|
|||||||
assertEquals(24L * MEBIBYTE, localOnly.routingBytes());
|
assertEquals(24L * MEBIBYTE, localOnly.routingBytes());
|
||||||
assertEquals(8L * MEBIBYTE, localOnly.edgeBytes());
|
assertEquals(8L * MEBIBYTE, localOnly.edgeBytes());
|
||||||
assertEquals(0L, localOnly.regionalDraftBytes() + localOnly.regionalCoarseBytes()
|
assertEquals(0L, localOnly.regionalDraftBytes() + localOnly.regionalCoarseBytes()
|
||||||
+ localOnly.regionalTerrainBytes() + localOnly.regionalReachBytes());
|
+ localOnly.regionalTerrainBytes());
|
||||||
assertThrows(IllegalArgumentException.class, () -> HydrologyCacheBudget.forHeap(0L, false));
|
assertThrows(IllegalArgumentException.class, () -> HydrologyCacheBudget.forHeap(0L, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ public class HydrologyCacheBudgetTest {
|
|||||||
HydrologyRegionalGraph.Label second = new HydrologyRegionalGraph.Label(
|
HydrologyRegionalGraph.Label second = new HydrologyRegionalGraph.Label(
|
||||||
new HydrologyRegionalGraph.LabelState(3, 2, 63, 1D, 1D, 0), downstream);
|
new HydrologyRegionalGraph.LabelState(3, 2, 63, 1D, 1D, 0), downstream);
|
||||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(
|
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());
|
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) {
|
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) {
|
private static OutletCandidate outlet(int index) {
|
||||||
|
|||||||
+1
-1
@@ -247,7 +247,7 @@ public class HydrologyFallbackParallelismTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyRoutingPlan routing(List<OutletCandidate> outlets) {
|
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) {
|
private static OutletCandidate outlet(int index) {
|
||||||
|
|||||||
+13
-12
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
@@ -1508,7 +1509,7 @@ public class HydrologyFootprintCompilerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void lowBanksKeepTheirNaturalHeightAndReportUncontainedWater() {
|
public void lowBanksFillWithinTheirPolicyAndReportRemainingUncontainedWater() {
|
||||||
HydraulicSegment channel = new HydraulicSegment(
|
HydraulicSegment channel = new HydraulicSegment(
|
||||||
529L,
|
529L,
|
||||||
528L,
|
528L,
|
||||||
@@ -1539,25 +1540,25 @@ public class HydrologyFootprintCompilerTest {
|
|||||||
HydrologyFootprintCompiler lowBankCompiler = compiler(lowBankTerrain);
|
HydrologyFootprintCompiler lowBankCompiler = compiler(lowBankTerrain);
|
||||||
RiverFootprint lowBankFootprint = lowBankCompiler.compile(List.of(course));
|
RiverFootprint lowBankFootprint = lowBankCompiler.compile(List.of(course));
|
||||||
HydrologyColumnSample lowBank = lowBankFootprint.sample(12, lowBankZ).orElseThrow();
|
HydrologyColumnSample lowBank = lowBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||||
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> HydrologyTerrainSample.openLand(
|
SurfaceRiverPolicy oneBlockFill = new SurfaceRiverPolicy("restricted-bank",
|
||||||
x == 12 && z == lowBankZ ? 68 : 82,
|
null, null, null, null, null, null, 1);
|
||||||
0D,
|
HydrologyTerrainSampler deepBankTerrain = (int x, int z) -> x == 12 && z == lowBankZ
|
||||||
"parent"
|
? HydrologyTerrainSample.openLand(68, 0D, "parent").withSurfacePolicy(oneBlockFill)
|
||||||
);
|
: HydrologyTerrainSample.openLand(82, 0D, "parent");
|
||||||
RiverFootprint deepBankFootprint = compiler(deepBankTerrain).compile(List.of(course));
|
RiverFootprint deepBankFootprint = compiler(deepBankTerrain).compile(List.of(course));
|
||||||
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
|
HydrologyColumnSample deepBank = deepBankFootprint.sample(12, lowBankZ).orElseThrow();
|
||||||
|
|
||||||
assertEquals(0, unsupportedBankCells(highFootprint, 528L));
|
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.naturalHeight());
|
||||||
assertEquals(69, lowBank.terrainHeight());
|
assertEquals(70, lowBank.terrainHeight());
|
||||||
assertTrue(unsupportedBankCells(lowBankFootprint, 528L) > 0);
|
assertEquals(0, unsupportedBankCells(lowBankFootprint, 528L));
|
||||||
for (HydrologyColumnSample sample : lowBankFootprint.columns().values()) {
|
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.naturalHeight());
|
||||||
assertEquals(68, deepBank.terrainHeight());
|
assertEquals(69, deepBank.terrainHeight());
|
||||||
assertTrue(unsupportedBankCells(deepBankFootprint, 528L) > 0);
|
assertTrue(unsupportedBankCells(deepBankFootprint, 528L) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,30 @@ public class HydrologyModelTest {
|
|||||||
assertEquals(72, sample.terrainHeight());
|
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
|
@Test
|
||||||
public void uncutShoreRetainsItsBiomeWithoutOverridingAnOwnedBank() {
|
public void uncutShoreRetainsItsBiomeWithoutOverridingAnOwnedBank() {
|
||||||
HydrologyColumnLayer shore = surfaceLayer(feature(HydrologyFeatureType.SURFACE_POOL, 30L, 75),
|
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;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class HydrologyPlanHashTest {
|
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(
|
private static final List<HydrologyTileKey> TILES = List.of(
|
||||||
new HydrologyTileKey(0, 0),
|
new HydrologyTileKey(0, 0),
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import java.lang.reflect.InvocationTargetException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -35,7 +34,6 @@ import java.util.concurrent.ExecutorService;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -669,25 +667,18 @@ public class HydrologyPlannerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void surfaceRouteMemoPreservesCompleteOwnerOutputAcrossPublicationPasses() throws Exception {
|
public void sharedDrainagePreservesCompleteOwnerOutputAcrossPublicationPasses() {
|
||||||
for (boolean organic : new boolean[]{false, true}) {
|
for (boolean organic : new boolean[]{false, true}) {
|
||||||
HydrologyPlannerSettings settings = organic ? organicShapeSettings()
|
HydrologyPlannerSettings settings = organic ? organicShapeSettings()
|
||||||
: standardSettings(4D, 2D, true, false, List.of());
|
: standardSettings(4D, 2D, true, false, List.of());
|
||||||
HydrologyTerrainSampler terrain = organic ? organicShapeTerrain() : rollingCoast(112);
|
HydrologyTerrainSampler terrain = organic ? organicShapeTerrain() : rollingCoast(112);
|
||||||
SurfaceRouteMemos uncached = new SurfaceRouteMemos(false);
|
HydrologyPlanner planner = new HydrologyPlanner(642L, settings, terrain);
|
||||||
SurfaceRouteMemos cached = new SurfaceRouteMemos(true);
|
HydrologyTile expected = planner.plan(TILE);
|
||||||
HydrologyTile expected = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), uncached);
|
planner.clearOwnerDrafts();
|
||||||
HydrologyTile actual = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), cached);
|
HydrologyTile actual = planner.plan(TILE);
|
||||||
|
|
||||||
assertEquals(expected, actual);
|
assertEquals(expected, actual);
|
||||||
assertEquals(expected.courses(), actual.courses());
|
assertEquals(expected.courses(), actual.courses());
|
||||||
assertEquals(expected.localDiagnosticCandidates(), actual.localDiagnosticCandidates());
|
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()) {
|
for (HydraulicSegment segment : course.segments()) {
|
||||||
if (segment.drop() > 0) {
|
if (segment.drop() > 0) {
|
||||||
assertTrue(segment.type().isDrop());
|
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)) {
|
for (HydraulicSegment segment : segments(cliff, HydrologyFeatureType.WATERFALL)) {
|
||||||
assertTrue(segment.drop() > 0);
|
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.centerline().size() >= 2);
|
||||||
assertTrue(segment.width() >= 1);
|
assertTrue(segment.width() >= 1);
|
||||||
}
|
}
|
||||||
@@ -1139,7 +1135,8 @@ public class HydrologyPlannerTest {
|
|||||||
HydraulicSegment receiver = course.segments().get(index + 1);
|
HydraulicSegment receiver = course.segments().get(index + 1);
|
||||||
assertTrue(approach.type().isSurface());
|
assertTrue(approach.type().isSurface());
|
||||||
assertTrue(receiver.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());
|
assertEquals(waterfall.end(), receiver.start());
|
||||||
sawBlendedWaterfall = true;
|
sawBlendedWaterfall = true;
|
||||||
}
|
}
|
||||||
@@ -1206,6 +1203,13 @@ public class HydrologyPlannerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assertTrue("courses=" + surfaceCourses(tile), aggregateTransition);
|
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
|
@Test
|
||||||
@@ -1879,7 +1883,10 @@ public class HydrologyPlannerTest {
|
|||||||
}
|
}
|
||||||
assertTrue(bed);
|
assertTrue(bed);
|
||||||
assertEquals(drop.fallingFluid(), falling);
|
assertEquals(drop.fallingFluid(), falling);
|
||||||
assertFalse(drop.fallingFluid());
|
if (drop.fallingFluid()) {
|
||||||
|
assertEquals(HydrologyFeatureType.WATERFALL, drop.type());
|
||||||
|
assertTrue(tile.cavePlan(drop.courseId()).orElseThrow().accepted());
|
||||||
|
}
|
||||||
if (!receiving) {
|
if (!receiving) {
|
||||||
HydrologyColumnSample endColumn = tile.columnAt(drop.end().x(), drop.end().z()).orElseThrow();
|
HydrologyColumnSample endColumn = tile.columnAt(drop.end().x(), drop.end().z()).orElseThrow();
|
||||||
receiving = endColumn.layers().stream().anyMatch((HydrologyColumnLayer layer) ->
|
receiving = endColumn.layers().stream().anyMatch((HydrologyColumnLayer layer) ->
|
||||||
@@ -2210,6 +2217,15 @@ public class HydrologyPlannerTest {
|
|||||||
).orElseThrow();
|
).orElseThrow();
|
||||||
assertEquals(course.id(), located.courseId());
|
assertEquals(course.id(), located.courseId());
|
||||||
assertEquals(sinkhole.id(), located.segmentId());
|
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
|
@Test
|
||||||
@@ -3446,57 +3462,6 @@ public class HydrologyPlannerTest {
|
|||||||
}
|
}
|
||||||
return false;
|
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) {
|
private HydrologyPlanner countedPlanner(CountingNaturalSampler sampler) {
|
||||||
return new HydrologyPlanner(91L, standardSettings(4D, 0D, true, false, List.of()),
|
return new HydrologyPlanner(91L, standardSettings(4D, 0D, true, false, List.of()),
|
||||||
sampler.delegate, sampler, HydrologyGeometrySampler.deterministic(sampler.delegate),
|
sampler.delegate, sampler, HydrologyGeometrySampler.deterministic(sampler.delegate),
|
||||||
|
|||||||
+9
-62
@@ -31,9 +31,7 @@ public class HydrologyRegionalBankSupportTest {
|
|||||||
HydrologyTerrainSampler terrain = depression(false);
|
HydrologyTerrainSampler terrain = depression(false);
|
||||||
List<HydrologyPoint> path = path();
|
List<HydrologyPoint> path = path();
|
||||||
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
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());
|
assertNull(validate(planner, path, terrain).rejection());
|
||||||
SurfaceCourseResult built = build(path, terrain);
|
SurfaceCourseResult built = build(path, terrain);
|
||||||
assertTrue(built.accepted());
|
assertTrue(built.accepted());
|
||||||
@@ -44,7 +42,7 @@ public class HydrologyRegionalBankSupportTest {
|
|||||||
assertEquals(0, footprint.uncontainedWetCells());
|
assertEquals(0, footprint.uncontainedWetCells());
|
||||||
assertTrue(course.hydraulicallyNonRising());
|
assertTrue(course.hydraulicallyNonRising());
|
||||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
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()
|
SurfaceLayerColumn center = footprint.columns().stream()
|
||||||
.filter(column -> column.x() == 132 && column.z() == 0).findFirst().orElseThrow();
|
.filter(column -> column.x() == 132 && column.z() == 0).findFirst().orElseThrow();
|
||||||
@@ -58,67 +56,16 @@ public class HydrologyRegionalBankSupportTest {
|
|||||||
HydrologyTerrainSampler terrain = depression(true);
|
HydrologyTerrainSampler terrain = depression(true);
|
||||||
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
||||||
|
|
||||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
|
assertNull(validate(planner, path(), terrain).rejection());
|
||||||
validate(planner, path(), terrain).rejection());
|
|
||||||
List<HydrologyPoint> refined = new HydrologyRegionalTerrainRefiner(planner).refine(path(), "default", false, terrain);
|
|
||||||
if (!refined.isEmpty()) {
|
|
||||||
assertFalse(build(refined, terrain).accepted());
|
|
||||||
}
|
|
||||||
SurfaceCourseResult built = build(path(), terrain);
|
SurfaceCourseResult built = build(path(), terrain);
|
||||||
assertFalse(built.accepted());
|
if (built.accepted()) {
|
||||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
|
RiverCourse course = new RiverCourse(91L, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(2L),
|
||||||
assertEquals(17, built.rejectionDetail());
|
"default", 1, List.of(), built.segments());
|
||||||
}
|
SurfaceFootprint footprint = new SurfaceFootprintCompiler(SETTINGS, terrain, GEOMETRY).compile(course);
|
||||||
|
assertFalse(footprint.accepted());
|
||||||
@Test
|
} else {
|
||||||
public void levelCentersNeedNoBankSamplesAndSupportedDipsCannotRaiseIncomingHead() {
|
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, built.rejection());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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) {
|
private static HydrologyTerrainSampler depression(boolean acrossBanks) {
|
||||||
|
|||||||
+5
-54
@@ -14,34 +14,6 @@ import static org.junit.Assert.assertNull;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class HydrologyRegionalCacheTest {
|
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
|
@Test
|
||||||
public void clearingTilesRefreshesChangedRegionalTerrain() throws Exception {
|
public void clearingTilesRefreshesChangedRegionalTerrain() throws Exception {
|
||||||
AtomicInteger height = new AtomicInteger(70);
|
AtomicInteger height = new AtomicInteger(70);
|
||||||
@@ -51,7 +23,7 @@ public class HydrologyRegionalCacheTest {
|
|||||||
return HydrologyTerrainSample.openLand(height.get(), 0D, "land");
|
return HydrologyTerrainSample.openLand(height.get(), 0D, "land");
|
||||||
};
|
};
|
||||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||||
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
|
HydrologyRegionalTerrain refiner = refiner(planner);
|
||||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
||||||
assertEquals(70, refiner.sample(0, 0).naturalHeight());
|
assertEquals(70, refiner.sample(0, 0).naturalHeight());
|
||||||
height.set(85);
|
height.set(85);
|
||||||
@@ -71,7 +43,7 @@ public class HydrologyRegionalCacheTest {
|
|||||||
return available.get() ? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
|
return available.get() ? HydrologyTerrainSample.openLand(70, 0D, "land") : null;
|
||||||
};
|
};
|
||||||
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
HydrologyPlanner planner = new HydrologyPlanner(15L, HydrologyRegionalPlannerTest.settings(false, 1), terrain);
|
||||||
HydrologyRegionalTerrainRefiner refiner = refiner(planner);
|
HydrologyRegionalTerrain refiner = refiner(planner);
|
||||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
try (HydrologyTileCache cache = new HydrologyTileCache(planner)) {
|
||||||
assertNull(refiner.sample(0, 0));
|
assertNull(refiner.sample(0, 0));
|
||||||
available.set(true);
|
available.set(true);
|
||||||
@@ -82,32 +54,11 @@ public class HydrologyRegionalCacheTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private static HydrologyRegionalTerrain refiner(HydrologyPlanner planner) throws Exception {
|
||||||
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 {
|
|
||||||
Field routes = HydrologyRegionalPlanner.class.getDeclaredField("routes");
|
Field routes = HydrologyRegionalPlanner.class.getDeclaredField("routes");
|
||||||
routes.setAccessible(true);
|
routes.setAccessible(true);
|
||||||
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrainRefiner");
|
Field refiner = HydrologyRegionalRoute.class.getDeclaredField("terrain");
|
||||||
refiner.setAccessible(true);
|
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 {
|
public class HydrologyRegionalCandidateSelectionTest {
|
||||||
@Test
|
@Test
|
||||||
public void lowerMeanderCandidatePassesTheFullPlannerAfterTheFirstBankCutFails() throws Exception {
|
public void plannedHeadCrossesAChannelDepressionWithoutLoweringTheWholeCourse() throws Exception {
|
||||||
HydrologyTerrainSampler terrain = coast(true);
|
HydrologyTerrainSampler terrain = coast(true);
|
||||||
HydrologyPlanner planner = planner(terrain, 16, 2);
|
HydrologyPlanner planner = planner(terrain, 16, 2);
|
||||||
List<HydrologyPoint> guide = guide(terrain);
|
List<HydrologyPoint> guide = guide(terrain);
|
||||||
@@ -33,9 +33,8 @@ public class HydrologyRegionalCandidateSelectionTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertTrue(selected.toString(), selected.accepted());
|
assertTrue(selected.toString(), selected.accepted());
|
||||||
assertEquals(2, attempted.size());
|
assertEquals(1, attempted.size());
|
||||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, attempted.getFirst().rejection().rejection());
|
assertNull(attempted.getFirst().rejection());
|
||||||
assertEquals(18, attempted.getFirst().rejection().detail());
|
|
||||||
assertEquals(1, selected.network().courses().size());
|
assertEquals(1, selected.network().courses().size());
|
||||||
assertEquals(selected, attempted.getLast());
|
assertEquals(selected, attempted.getLast());
|
||||||
RiverCourse course = selected.network().courses().getFirst();
|
RiverCourse course = selected.network().courses().getFirst();
|
||||||
@@ -124,8 +123,10 @@ public class HydrologyRegionalCandidateSelectionTest {
|
|||||||
}
|
}
|
||||||
int[] contributions = new int[9];
|
int[] contributions = new int[9];
|
||||||
Arrays.fill(contributions, 1);
|
Arrays.fill(contributions, 1);
|
||||||
|
int[] heads = new int[9];
|
||||||
|
Arrays.fill(heads, 80);
|
||||||
HydrologyRegionalGraph.Tree tree = new HydrologyRegionalGraph.Tree(selected, contributions,
|
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<>();
|
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||||
Method build = HydrologyRegionalPlanner.class.getDeclaredMethod("build", HydrologySampledGrid.class,
|
Method build = HydrologyRegionalPlanner.class.getDeclaredMethod("build", HydrologySampledGrid.class,
|
||||||
HydrologyRegionalGraph.Tree.class, int.class, OutletCandidate.class, long.class, List.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(),
|
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 1L, SETTINGS.fingerprint(),
|
||||||
SETTINGS.routing().tileSize(), network.nodes(), network.edges(), network.outlets(),
|
SETTINGS.routing().tileSize(), network.nodes(), network.edges(), network.outlets(),
|
||||||
List.of(regional, local), Set.of(regional.id()), clipped.cavePlans(), List.of(), materialized.footprint());
|
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()),
|
new HydrologyTileCache.SharedCacheScope("regional-ownership", 1L, 128, "overworld", SETTINGS.fingerprint()),
|
||||||
SETTINGS.routing().tileSize());
|
SETTINGS.routing().tileSize());
|
||||||
store.save(original);
|
store.save(original);
|
||||||
|
|||||||
+5
-5
@@ -28,8 +28,8 @@ public class HydrologyRegionalMouthRefinementTest {
|
|||||||
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forOutlet(settings, terrain, outlet);
|
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forOutlet(settings, terrain, outlet);
|
||||||
HydrologyPlanner planner = new HydrologyPlanner(1L, settings, terrain);
|
HydrologyPlanner planner = new HydrologyPlanner(1L, settings, terrain);
|
||||||
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
|
HydrologyRegionalRoute route = new HydrologyRegionalRoute(planner);
|
||||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
|
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(767, 63, 0),
|
||||||
new HydrologyPoint(768, 80, 0), landward);
|
new HydrologyPoint(768, 63, 0), landward);
|
||||||
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class,
|
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validate", List.class, String.class,
|
||||||
boolean.class, HydrologyTerrainSampler.class);
|
boolean.class, HydrologyTerrainSampler.class);
|
||||||
validate.setAccessible(true);
|
validate.setAccessible(true);
|
||||||
@@ -40,7 +40,7 @@ public class HydrologyRegionalMouthRefinementTest {
|
|||||||
|
|
||||||
assertNull(accepted.toString(), accepted.rejection());
|
assertNull(accepted.toString(), accepted.rejection());
|
||||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, unproved.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());
|
assertFalse(terrain.sample(800, 0).ocean());
|
||||||
assertTrue(receiver.receivingWater(800, 0, 63));
|
assertTrue(receiver.receivingWater(800, 0, 63));
|
||||||
}
|
}
|
||||||
@@ -171,8 +171,8 @@ public class HydrologyRegionalMouthRefinementTest {
|
|||||||
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
|
HydrologyPlannerSettings settings = HydrologyRegionalPlannerTest.settings(false, 1);
|
||||||
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 ? HydrologyTerrainSample.ocean(60, "ocean")
|
HydrologyTerrainSampler terrain = (x, z) -> x >= 800 ? HydrologyTerrainSample.ocean(60, "ocean")
|
||||||
: HydrologyTerrainSample.openLand(x >= 768 ? 80 : 65, 0D, "land");
|
: HydrologyTerrainSample.openLand(x >= 768 ? 80 : 65, 0D, "land");
|
||||||
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 65, 0), new HydrologyPoint(767, 65, 0),
|
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 63, 0), new HydrologyPoint(767, 63, 0),
|
||||||
new HydrologyPoint(768, 80, 0), new HydrologyPoint(799, 80, 0), new HydrologyPoint(800, 63, 0));
|
new HydrologyPoint(768, 63, 0), new HydrologyPoint(799, 63, 0), new HydrologyPoint(800, 63, 0));
|
||||||
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
|
HydrologyGeometrySampler geometry = request -> switch (request.field()) {
|
||||||
case SURFACE_WIDTH -> 6;
|
case SURFACE_WIDTH -> 6;
|
||||||
case SURFACE_DEPTH -> 2;
|
case SURFACE_DEPTH -> 2;
|
||||||
|
|||||||
+4
-175
@@ -38,7 +38,7 @@ public class HydrologyRegionalSamplingTest {
|
|||||||
RecordingSampler serial = new RecordingSampler(terrain);
|
RecordingSampler serial = new RecordingSampler(terrain);
|
||||||
SurfaceBankSupport.CrossSection cross = support.crossSection(serial, station, true);
|
SurfaceBankSupport.CrossSection cross = support.crossSection(serial, station, true);
|
||||||
SurfaceBankSupport.Perimeter perimeter = support.perimeter(serial, station, 80);
|
SurfaceBankSupport.Perimeter perimeter = support.perimeter(serial, station, 80);
|
||||||
RecordingSampler parallel = new RecordingSampler(refiner(terrain).new Samples());
|
RecordingSampler parallel = new RecordingSampler(refiner(terrain));
|
||||||
pool.submit(() -> {
|
pool.submit(() -> {
|
||||||
assertEquals(cross, support.crossSection(parallel, station, true));
|
assertEquals(cross, support.crossSection(parallel, station, true));
|
||||||
assertEquals(perimeter, support.perimeter(parallel, station, 80));
|
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
|
@Test
|
||||||
public void unavailableBankStopsBeforeALaterProviderFailure() throws Exception {
|
public void unavailableBankStopsBeforeALaterProviderFailure() throws Exception {
|
||||||
IllegalStateException failure = new IllegalStateException("Later bank sample");
|
IllegalStateException failure = new IllegalStateException("Later bank sample");
|
||||||
@@ -103,7 +61,7 @@ public class HydrologyRegionalSamplingTest {
|
|||||||
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
|
SurfaceBankSupport support = new SurfaceBankSupport(SETTINGS.surface(), SETTINGS.seaLevel());
|
||||||
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(0, 0, 1D, 0D, 4D);
|
SurfaceBankSupport.Station station = new SurfaceBankSupport.Station(0, 0, 1D, 0D, 4D);
|
||||||
SurfaceBankSupport.CrossSection expected = support.crossSection(terrain, station, true);
|
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)) {
|
try (ForkJoinPool pool = new ForkJoinPool(4)) {
|
||||||
assertEquals(expected, pool.submit(() -> support.crossSection(samples, station, true)).get(10, TimeUnit.SECONDS));
|
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)));
|
assertSame(failure, assertThrows(IllegalStateException.class, () -> samples.sample(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private static HydrologyRegionalTerrain refiner(HydrologyTerrainSampler terrain) {
|
||||||
public void hydraulicBudgetAndLazyWidthsPreserveConsumptionOrder() throws Exception {
|
return new HydrologyRegionalTerrain(new HydrologyPlanner(71L, SETTINGS, terrain));
|
||||||
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 void await(CountDownLatch latch) {
|
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++);
|
sources.put(RiverFootprint.pack(node.x(), node.z()), ordinal++);
|
||||||
}
|
}
|
||||||
assertEquals(secondCatchmentSize + 4, 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));
|
when(graph.path(any(), anyInt())).thenAnswer(invocation -> List.of(invocation.<Integer>getArgument(1), 0));
|
||||||
replace(planner.regional, "graph", graph);
|
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)));
|
new RiverOutlet(100L + root, HydrologyFeatureType.MOUTH, root, point, point, 63, true)));
|
||||||
}
|
}
|
||||||
HydrologySampledGrid grid = new HydrologySampledGrid(0, 0, 0, 0, 16384, sources.size(), 1, nodes);
|
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);
|
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,
|
candidates.add(new OutletCandidate(node.index(), -1, new RiverOutlet(node.index() + 1000L,
|
||||||
HydrologyFeatureType.MOUTH, node.id(), point, point, 63, true)));
|
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) {
|
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 {
|
public void aWiderChannelContainsTheCenterDepressionWithoutRaisingLand() throws Exception {
|
||||||
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
HydrologyTerrainSampler terrain = (x, z) -> HydrologyTerrainSample.openLand(
|
||||||
x >= 128 && x <= 136 && Math.abs(z) <= 1 ? 65 : x >= 256 && x <= 288 ? 86 : 80, 0D, "land");
|
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);
|
HydrologyPlanner planner = new HydrologyPlanner(71L, SETTINGS, terrain);
|
||||||
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateHydraulics",
|
Method validate = HydrologyRegionalRoute.class.getDeclaredMethod("validateHydraulics",
|
||||||
HydrologyRegionalRoute.Refinement.class, String.class, boolean.class, HydrologyTerrainSampler.class);
|
HydrologyRegionalRoute.Refinement.class, String.class, boolean.class, HydrologyTerrainSampler.class);
|
||||||
@@ -48,7 +46,7 @@ public class HydrologyRegionalWidthSupportTest {
|
|||||||
assertTrue(course.hydraulicallyNonRising());
|
assertTrue(course.hydraulicallyNonRising());
|
||||||
SurfaceLayerColumn center = null;
|
SurfaceLayerColumn center = null;
|
||||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
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());
|
assertTrue(column.terrain().naturalHeight() - column.layer().bedY() <= SETTINGS.surface().maximumIncision());
|
||||||
if (column.x() == 132 && column.z() == 0) {
|
if (column.x() == 132 && column.z() == 0) {
|
||||||
center = column;
|
center = column;
|
||||||
@@ -59,112 +57,8 @@ public class HydrologyRegionalWidthSupportTest {
|
|||||||
assertEquals(80, center.layer().fluidHeadY());
|
assertEquals(80, center.layer().fluidHeadY());
|
||||||
|
|
||||||
SurfaceCourseResult narrow = build(terrain, geometry(4));
|
SurfaceCourseResult narrow = build(terrain, geometry(4));
|
||||||
assertFalse(narrow.accepted());
|
assertTrue(narrow.accepted());
|
||||||
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, narrow.rejection());
|
assertTrue(narrow.segments().stream().allMatch(segment -> segment.upstreamHeadY() >= segment.downstreamHeadY()));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HydrologyGeometrySampler geometry(int width) {
|
private static HydrologyGeometrySampler geometry(int width) {
|
||||||
@@ -180,22 +74,4 @@ public class HydrologyRegionalWidthSupportTest {
|
|||||||
.build(71L, 91L, "default", PATH, SurfaceTerminal.SINKHOLE, 40, 256);
|
.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;
|
import static org.mockito.Mockito.spy;
|
||||||
|
|
||||||
public class HydrologyRouteHeightProbeTest {
|
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
|
@Test
|
||||||
public void pitChecksRetainMissingAndIntegerDifferenceSemantics() {
|
public void pitChecksRetainMissingAndIntegerDifferenceSemantics() {
|
||||||
int threshold = HydrologyPlannerSettings.defaults().hydraulics().waterfallMinimumDrop();
|
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,
|
candidates.add(new OutletCandidate(node.index(), -1, new RiverOutlet(node.index() + 1000L,
|
||||||
HydrologyFeatureType.MOUTH, node.id(), point, point, 63, true)));
|
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) {
|
private static HydrologyTerrainSample land(int naturalHeight, double weight) {
|
||||||
|
|||||||
+1
@@ -64,6 +64,7 @@ public class HydrologyStyledGeometryTest {
|
|||||||
Set.of(
|
Set.of(
|
||||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
|
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
|
||||||
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
|
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
|
||||||
|
HydrologyGeometrySampler.Field.SURFACE_BLEND_WIDTH,
|
||||||
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
||||||
HydrologyGeometrySampler.Field.UNDERGROUND_WIDTH,
|
HydrologyGeometrySampler.Field.UNDERGROUND_WIDTH,
|
||||||
HydrologyGeometrySampler.Field.UNDERGROUND_DEPTH,
|
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());
|
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
|
@Test
|
||||||
public void surfaceDropsDoNotOwnSubmergedGroundOrRaiseWaterOnSeaLevelLand() {
|
public void surfaceDropsDoNotOwnSubmergedGroundOrRaiseWaterOnSeaLevelLand() {
|
||||||
RiverCourse course = course(true);
|
RiverCourse course = course(true);
|
||||||
@@ -97,7 +166,7 @@ public final class HydrologySurfaceDropRasterTest {
|
|||||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
|
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(SETTINGS, TERRAIN, GEOMETRY);
|
||||||
HydrologyPlannerSettings.Excavation limits = SETTINGS.surface().banks().erosion().excavation();
|
HydrologyPlannerSettings.Excavation limits = SETTINGS.surface().banks().erosion().excavation();
|
||||||
FootprintLayerShape shape = new FootprintLayerShape(2, 2D, 40D, 79, 90, 90,
|
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(),
|
HydrologyFeatureRef feature = new HydrologyFeatureRef(5L, segment.type(), course.id(), segment.id(),
|
||||||
0, 90, 0, 1, 0, false);
|
0, 90, 0, 1, 0, false);
|
||||||
HydrologyTerrainSample highTerrain = HydrologyTerrainSample.openLand(160, 0D, "parent");
|
HydrologyTerrainSample highTerrain = HydrologyTerrainSample.openLand(160, 0D, "parent");
|
||||||
|
|||||||
+2
-87
@@ -5,54 +5,9 @@ import org.junit.Test;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
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 {
|
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
|
@Test
|
||||||
public void surfaceAnchorKeepsTheCoarseValleyWhenSmoothGroundIsMuchHigher() {
|
public void surfaceAnchorKeepsTheCoarseValleyWhenSmoothGroundIsMuchHigher() {
|
||||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
||||||
@@ -85,7 +40,7 @@ public class HydrologySurfaceRouteGeometryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void steepSurfaceEdgesCanDetourAroundAContainedTerrainPit() {
|
public void undergroundEdgesRejectAContainedTerrainPit() {
|
||||||
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
HydrologyTerrainSampler sampler = (x, z) -> HydrologyTerrainSample.openLand(
|
||||||
128 - Math.floorDiv(x, 2) - (x >= 30 && x <= 34 && Math.abs(z) <= 1 ? 14 : 0),
|
128 - Math.floorDiv(x, 2) - (x >= 30 && x <= 34 && Math.abs(z) <= 1 ? 14 : 0),
|
||||||
0D, "land");
|
0D, "land");
|
||||||
@@ -94,47 +49,7 @@ public class HydrologySurfaceRouteGeometryTest {
|
|||||||
HydrologyPoint end = new HydrologyPoint(64, 96, 0);
|
HydrologyPoint end = new HydrologyPoint(64, 96, 0);
|
||||||
HydrologyPoint continuation = new HydrologyPoint(96, 80, 0);
|
HydrologyPoint continuation = new HydrologyPoint(96, 80, 0);
|
||||||
|
|
||||||
List<HydrologyPoint> cave = planner.routeGeometry.refineEdge(1L, 2L, start, end, continuation, 4, false);
|
List<HydrologyPoint> cave = planner.routeGeometry.refineUndergroundEdge(1L, 2L, start, end, continuation, 4);
|
||||||
List<HydrologyPoint> surface = planner.routeGeometry.refineEdge(1L, 2L, start, end, continuation, 4, true);
|
|
||||||
|
|
||||||
assertTrue(cave.isEmpty());
|
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.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.volmlib.util.cache.CacheKey;
|
import art.arcane.volmlib.util.cache.CacheKey;
|
||||||
import org.junit.Test;
|
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.lang.reflect.Field;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -50,6 +53,55 @@ import static org.mockito.Mockito.never;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class HydrologyTileCacheTest {
|
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
|
@Test
|
||||||
public void weightedTileCapacityStopsSpeculationWithoutBlockingDemand() {
|
public void weightedTileCapacityStopsSpeculationWithoutBlockingDemand() {
|
||||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||||
@@ -400,7 +452,7 @@ public class HydrologyTileCacheTest {
|
|||||||
monitor.set(queue.get(cache));
|
monitor.set(queue.get(cache));
|
||||||
|
|
||||||
if (pregeneration) {
|
if (pregeneration) {
|
||||||
cache.preparePregeneration(262144, 262144);
|
cache.preparePregeneration(pregenArea(262144, 262144));
|
||||||
} else {
|
} else {
|
||||||
cache.prefetchArea(261120, 261120, 263168, 263168, 262144, 262144);
|
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(prefetchRectangle(254, 257, 254, 257), new HashSet<>(planned.subList(0, 16)));
|
||||||
assertEquals(List.of(new HydrologyTileKey(255, 255), new HydrologyTileKey(256, 255),
|
assertEquals(List.of(new HydrologyTileKey(255, 255), new HydrologyTileKey(256, 255),
|
||||||
new HydrologyTileKey(257, 255)), planned.subList(1, 4));
|
new HydrologyTileKey(257, 255)), planned.subList(1, 4));
|
||||||
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned.subList(16, planned.size())));
|
if (pregeneration) {
|
||||||
assertEquals(22, planned.size());
|
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);
|
Runnable demand = queued.poll(5, TimeUnit.SECONDS);
|
||||||
assertNotNull(demand);
|
assertNotNull(demand);
|
||||||
|
|
||||||
cache.preparePregeneration(262144, 262144);
|
cache.preparePregeneration(pregenArea(262144, 262144));
|
||||||
|
|
||||||
assertFalse(active.isDone());
|
assertFalse(active.isDone());
|
||||||
assertFalse(demanded.isDone());
|
assertFalse(demanded.isDone());
|
||||||
@@ -485,9 +541,9 @@ public class HydrologyTileCacheTest {
|
|||||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
|
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
|
||||||
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
|
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
|
||||||
cache.preparePregeneration(262144, 262144);
|
cache.preparePregeneration(pregenArea(262144, 262144));
|
||||||
cache.preparePregeneration(263168, 262144);
|
cache.preparePregeneration(pregenArea(263168, 262144));
|
||||||
cache.preparePregeneration(263168, 262144);
|
cache.preparePregeneration(pregenArea(263168, 262144));
|
||||||
drainPrefetchTasks(queued);
|
drainPrefetchTasks(queued);
|
||||||
|
|
||||||
Set<HydrologyTileKey> expected = new HashSet<>(prefetchRectangle(255, 258, 254, 257));
|
Set<HydrologyTileKey> expected = new HashSet<>(prefetchRectangle(255, 258, 254, 257));
|
||||||
@@ -496,7 +552,7 @@ public class HydrologyTileCacheTest {
|
|||||||
assertEquals(17, planned.size());
|
assertEquals(17, planned.size());
|
||||||
cache.clear();
|
cache.clear();
|
||||||
planned.clear();
|
planned.clear();
|
||||||
cache.preparePregeneration(263168, 262144);
|
cache.preparePregeneration(pregenArea(263168, 262144));
|
||||||
drainPrefetchTasks(queued);
|
drainPrefetchTasks(queued);
|
||||||
assertEquals(prefetchRectangle(255, 258, 254, 257), new HashSet<>(planned));
|
assertEquals(prefetchRectangle(255, 258, 254, 257), new HashSet<>(planned));
|
||||||
assertEquals(16, planned.size());
|
assertEquals(16, planned.size());
|
||||||
@@ -516,13 +572,113 @@ public class HydrologyTileCacheTest {
|
|||||||
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
|
||||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
|
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
|
||||||
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
|
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
|
||||||
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(Integer.MAX_VALUE, 0));
|
assertThrows(IllegalArgumentException.class, () -> cache.preparePregeneration(
|
||||||
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(0, Integer.MIN_VALUE));
|
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);
|
drainPrefetchTasks(queued);
|
||||||
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned));
|
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned));
|
||||||
assertEquals(6, planned.size());
|
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() {
|
private static HydrologyPlannerSettings stalePrefetchSettings() {
|
||||||
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
|
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
|
||||||
when(settings.routing()).thenReturn(new HydrologyPlannerSettings.Routing(
|
when(settings.routing()).thenReturn(new HydrologyPlannerSettings.Routing(
|
||||||
@@ -1100,7 +1256,7 @@ public class HydrologyTileCacheTest {
|
|||||||
order.clear();
|
order.clear();
|
||||||
cache.setNeighbourPrefetchEnabled(false);
|
cache.setNeighbourPrefetchEnabled(false);
|
||||||
|
|
||||||
cache.preparePregeneration(0, 0);
|
cache.preparePregeneration(pregenArea(0, 0));
|
||||||
|
|
||||||
assertEquals(new HydrologyTileKey(1, -1), order.getFirst());
|
assertEquals(new HydrologyTileKey(1, -1), order.getFirst());
|
||||||
assertTrue(order.contains(new HydrologyTileKey(1, 0)));
|
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.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class StudioHydrologyTileStoreTest {
|
public class PreparedHydrologyTileStoreTest {
|
||||||
@Rule
|
@Rule
|
||||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void roundTripsValidatedEntryTile() throws Exception {
|
public void roundTripsValidatedEntryTile() throws Exception {
|
||||||
HydrologyTile original = tile();
|
HydrologyTile original = tile();
|
||||||
StudioHydrologyTileStore store = store();
|
PreparedHydrologyTileStore store = store();
|
||||||
|
|
||||||
store.save(original);
|
store.save(original);
|
||||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
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,
|
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(),
|
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)));
|
new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), column)));
|
||||||
StudioHydrologyTileStore store = store();
|
PreparedHydrologyTileStore store = store();
|
||||||
store.save(original);
|
store.save(original);
|
||||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||||
assertEquals(original, restored);
|
assertEquals(original, restored);
|
||||||
@@ -118,7 +118,7 @@ public class StudioHydrologyTileStoreTest {
|
|||||||
@Test
|
@Test
|
||||||
public void entryRequiresRegionalOwnershipMetadata() throws Exception {
|
public void entryRequiresRegionalOwnershipMetadata() throws Exception {
|
||||||
HydrologyTile original = tile();
|
HydrologyTile original = tile();
|
||||||
StudioHydrologyTileStore store = store();
|
PreparedHydrologyTileStore store = store();
|
||||||
store.save(original);
|
store.save(original);
|
||||||
Path file = store.file(original.key());
|
Path file = store.file(original.key());
|
||||||
JsonObject persisted;
|
JsonObject persisted;
|
||||||
@@ -136,7 +136,7 @@ public class StudioHydrologyTileStoreTest {
|
|||||||
@Test
|
@Test
|
||||||
public void corruptEntryFallsBackToCacheMiss() throws Exception {
|
public void corruptEntryFallsBackToCacheMiss() throws Exception {
|
||||||
HydrologyTile original = tile();
|
HydrologyTile original = tile();
|
||||||
StudioHydrologyTileStore store = store();
|
PreparedHydrologyTileStore store = store();
|
||||||
store.save(original);
|
store.save(original);
|
||||||
Path file = store.file(original.key());
|
Path file = store.file(original.key());
|
||||||
Files.writeString(file, "invalid", StandardCharsets.UTF_8);
|
Files.writeString(file, "invalid", StandardCharsets.UTF_8);
|
||||||
@@ -149,15 +149,28 @@ public class StudioHydrologyTileStoreTest {
|
|||||||
Path root = temporaryFolder.newFolder().toPath();
|
Path root = temporaryFolder.newFolder().toPath();
|
||||||
HydrologyTileCache.SharedCacheScope scope = scope();
|
HydrologyTileCache.SharedCacheScope scope = scope();
|
||||||
HydrologyTile original = tile();
|
HydrologyTile original = tile();
|
||||||
StudioHydrologyTileStore matching = new StudioHydrologyTileStore(root, scope, 64);
|
PreparedHydrologyTileStore matching = new PreparedHydrologyTileStore(root, scope, 64);
|
||||||
matching.save(original);
|
matching.save(original);
|
||||||
StudioHydrologyTileStore mismatched = new StudioHydrologyTileStore(root, scope, 128);
|
PreparedHydrologyTileStore mismatched = new PreparedHydrologyTileStore(root, scope, 128);
|
||||||
|
|
||||||
assertTrue(mismatched.load(original.key()).isEmpty());
|
assertTrue(mismatched.load(original.key()).isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
private StudioHydrologyTileStore store() throws Exception {
|
@Test
|
||||||
return new StudioHydrologyTileStore(
|
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(),
|
temporaryFolder.newFolder().toPath(),
|
||||||
scope(),
|
scope(),
|
||||||
64
|
64
|
||||||
+193
-9
@@ -13,9 +13,11 @@ import art.arcane.iris.generation.hydrology.IrisRiverBedProfile;
|
|||||||
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
|
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -31,6 +33,89 @@ public class ErosionFieldCompilerTest {
|
|||||||
default -> request.minimum();
|
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
|
@Test
|
||||||
public void mouthCutsDrySeaLevelSillBeforeReachingOceanWater() {
|
public void mouthCutsDrySeaLevelSillBeforeReachingOceanWater() {
|
||||||
HydrologyTerrainSampler coast = (int x, int z) -> x >= 100
|
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)));
|
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);
|
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);
|
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);
|
ErosionField complete = compiler.compile(42L, centerline, channel, valley, SurfaceTerminal.SINKHOLE, 8);
|
||||||
SurfaceBounds bounds = new SurfaceBounds(-16, -16, 15, 15);
|
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));
|
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
|
@Test
|
||||||
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
|
public void flatTerrainHoldsTheWaterFlushWithTheBankAndContainedByDefault() {
|
||||||
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
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);
|
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
|
@Test
|
||||||
public void bankColumnsBeyondTheBlendAreNotWritten() {
|
public void bankColumnsBeyondTheBlendAreNotWritten() {
|
||||||
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
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) {
|
private static void assertChannelContained(Compiled compiled) {
|
||||||
for (SurfaceColumn column : compiled.field().columns().values()) {
|
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||||
if (column.role() != SurfaceRole.CHANNEL || column.apron()) {
|
if (column.role() != SurfaceRole.CHANNEL || column.apron()) {
|
||||||
@@ -376,7 +513,7 @@ public class ErosionFieldCompilerTest {
|
|||||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
||||||
.solve(centerline, channel, terminal, terminalHead);
|
.solve(centerline, channel, terminal, terminalHead);
|
||||||
assertNull(valley.rejection());
|
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);
|
.compile(1234L, centerline, channel, valley, terminal, 8);
|
||||||
return new Compiled(field, valley);
|
return new Compiled(field, valley);
|
||||||
}
|
}
|
||||||
@@ -624,7 +761,7 @@ public class ErosionFieldCompilerTest {
|
|||||||
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
.solve(centerline, channel, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||||
assertNull(valley.rejection());
|
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);
|
.compile(99L, centerline, channel, valley, SurfaceTerminal.OCEAN_MOUTH, 3);
|
||||||
|
|
||||||
int firstApron = Integer.MAX_VALUE;
|
int firstApron = Integer.MAX_VALUE;
|
||||||
@@ -754,10 +891,12 @@ public class ErosionFieldCompilerTest {
|
|||||||
|
|
||||||
assertEquals(0, compiled.field().uncontainedWetCells());
|
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||||
assertChannelContained(compiled);
|
assertChannelContained(compiled);
|
||||||
assertNoWriteAboveNatural(compiled);
|
assertBoundedBankFill(compiled, 8);
|
||||||
assertEquals(80, compiled.field().column(0, 0).headY());
|
assertEquals(80, compiled.field().column(0, 0).headY());
|
||||||
SurfaceColumn nearRim = compiled.field().column(0, 4);
|
SurfaceColumn nearRim = compiled.field().column(0, 4);
|
||||||
assertEquals(SurfaceRole.CHANNEL, nearRim.role());
|
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()) {
|
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||||
if (column.role() == SurfaceRole.CHANNEL) {
|
if (column.role() == SurfaceRole.CHANNEL) {
|
||||||
assertTrue(Math.abs(column.z()) <= 7);
|
assertTrue(Math.abs(column.z()) <= 7);
|
||||||
@@ -805,19 +944,21 @@ public class ErosionFieldCompilerTest {
|
|||||||
HydrologyTerrainSampler shelf = (int x, int z) ->
|
HydrologyTerrainSampler shelf = (int x, int z) ->
|
||||||
HydrologyTerrainSample.openLand(80 + Math.min(16, Math.max(0, Math.abs(z) - 4) * 4), 0D, "land");
|
HydrologyTerrainSample.openLand(80 + Math.min(16, Math.max(0, Math.abs(z) - 4) * 4), 0D, "land");
|
||||||
Compiled terraced = compile(
|
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,
|
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);
|
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
assertNull(terraced.field().rejection());
|
||||||
Set<Integer> steps = bankHeights(terraced, 150, 9, 40);
|
Set<Integer> steps = bankHeights(terraced, 150, 9, 40);
|
||||||
assertTrue(steps.toString(), steps.size() >= 2 && steps.size() <= 4);
|
assertTrue(steps.toString(), steps.size() >= 2 && steps.size() <= 4);
|
||||||
assertHolds(terraced);
|
assertHolds(terraced);
|
||||||
|
|
||||||
Compiled cliff = compile(
|
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,
|
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);
|
shelf, 300, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
assertNull(cliff.field().rejection());
|
||||||
Set<Integer> bench = bankHeights(cliff, 150, 9, 40);
|
Set<Integer> bench = bankHeights(cliff, 150, 9, 40);
|
||||||
assertEquals(bench.toString(), 2, bench.size());
|
assertEquals(bench.toString(), 2, bench.size());
|
||||||
assertTrue(bench.contains(80));
|
assertTrue(bench.contains(80));
|
||||||
@@ -873,7 +1014,43 @@ public class ErosionFieldCompilerTest {
|
|||||||
assertTrue(column.height() <= hillside.sample(150, z).naturalHeight());
|
assertTrue(column.height() <= hillside.sample(150, z).naturalHeight());
|
||||||
previous = column.height();
|
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
|
@Test
|
||||||
@@ -987,4 +1164,11 @@ public class ErosionFieldCompilerTest {
|
|||||||
assertEquals(SurfaceRole.SHORE, bare.field().column(150, 4).role());
|
assertEquals(SurfaceRole.SHORE, bare.field().column(150, 4).role());
|
||||||
assertHolds(bare);
|
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)
|
ValleyProfile valley = new ValleyProfileSolver(surface, FLAT, 60, 32)
|
||||||
.solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
|
.solve(centerline, channel, SurfaceTerminal.SINKHOLE, 40);
|
||||||
assertTrue(valley.accepted());
|
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);
|
SurfaceTerminal.SINKHOLE, 8, HydrologyPlannerSettings.Ponds.none(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,4 +191,11 @@ public class SurfaceBendProfileTest {
|
|||||||
HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
|
HydrologyPlannerSettings.Inlet.none(), 2.5D, 24, true,
|
||||||
HydrologyPlannerSettings.Erosion.defaults(), HydrologyPlannerSettings.Ponds.none()));
|
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 java.util.List;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ public class SurfaceCourseBuilderTest {
|
|||||||
7L,
|
7L,
|
||||||
COURSE_ID,
|
COURSE_ID,
|
||||||
"water",
|
"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,
|
SurfaceTerminal.OCEAN_MOUTH,
|
||||||
SEA_LEVEL,
|
SEA_LEVEL,
|
||||||
64
|
64
|
||||||
@@ -72,7 +74,7 @@ public class SurfaceCourseBuilderTest {
|
|||||||
7L,
|
7L,
|
||||||
COURSE_ID,
|
COURSE_ID,
|
||||||
"water",
|
"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,
|
SurfaceTerminal.SINKHOLE,
|
||||||
40,
|
40,
|
||||||
64
|
64
|
||||||
@@ -85,6 +87,41 @@ public class SurfaceCourseBuilderTest {
|
|||||||
assertEquals(201, result.segments().getFirst().centerline().size());
|
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
|
@Test
|
||||||
public void cliffProducesAWaterfallSegment() {
|
public void cliffProducesAWaterfallSegment() {
|
||||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
||||||
@@ -92,7 +129,8 @@ public class SurfaceCourseBuilderTest {
|
|||||||
7L,
|
7L,
|
||||||
COURSE_ID,
|
COURSE_ID,
|
||||||
"water",
|
"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,
|
SurfaceTerminal.SINKHOLE,
|
||||||
40,
|
40,
|
||||||
64
|
64
|
||||||
@@ -106,14 +144,32 @@ public class SurfaceCourseBuilderTest {
|
|||||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().get(2).type());
|
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
|
@Test
|
||||||
public void gentleSlopeProducesRifflesAndSteepSlopeProducesCascades() {
|
public void gentleSlopeProducesRifflesAndSteepSlopeProducesCascades() {
|
||||||
HydrologyTerrainSampler gentle = (int x, int z) -> HydrologyTerrainSample.openLand(120 - x / 10, 0D, "land");
|
HydrologyTerrainSampler gentle = (int x, int z) -> HydrologyTerrainSample.openLand(120 - x / 10, 0D, "land");
|
||||||
SurfaceCourseResult gentleResult = builder(gentle).build(7L, COURSE_ID, "water",
|
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");
|
HydrologyTerrainSampler steep = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
|
||||||
SurfaceCourseResult steepResult = builder(steep).build(7L, COURSE_ID, "water",
|
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(gentleResult.rejection());
|
||||||
assertNull("detail=" + steepResult.rejectionDetail(), steepResult.rejection());
|
assertNull("detail=" + steepResult.rejectionDetail(), steepResult.rejection());
|
||||||
@@ -138,7 +194,7 @@ public class SurfaceCourseBuilderTest {
|
|||||||
7L,
|
7L,
|
||||||
COURSE_ID,
|
COURSE_ID,
|
||||||
"water",
|
"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,
|
SurfaceTerminal.OCEAN_MOUTH,
|
||||||
SEA_LEVEL,
|
SEA_LEVEL,
|
||||||
64
|
64
|
||||||
@@ -153,7 +209,8 @@ public class SurfaceCourseBuilderTest {
|
|||||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
|
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
|
||||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||||
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
|
: 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.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||||
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
HydrologyPlannerSettings.Inlet inlet = defaults.banks().inlet();
|
||||||
SurfaceCourseResult result = builder(sampler).build(7L, COURSE_ID, "water", path, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL, 64);
|
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.HydraulicChannelProfile;
|
||||||
import art.arcane.iris.generation.hydrology.HydraulicSegment;
|
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.HydrologyColumnLayer;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||||
@@ -131,6 +132,33 @@ public class SurfaceFootprintCompilerTest {
|
|||||||
assertNull(column(footprint, 160, 12));
|
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
|
@Test
|
||||||
public void mouthOverTheOceanPublishesOnlyANonOwningApron() {
|
public void mouthOverTheOceanPublishesOnlyANonOwningApron() {
|
||||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 100
|
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
|
@Test
|
||||||
public void aContainedFallRetainsBothExposedRunsAndWholeCourseStationIndices() {
|
public void aContainedFallRetainsBothExposedRunsAndWholeCourseStationIndices() {
|
||||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
||||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||||
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
|
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)),
|
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
|
||||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
new HydrologyPoint(100, 90, 0), new HydrologyPoint(200, 90, 0)),
|
||||||
|
SurfaceTerminal.TRIBUTARY, 90, 64);
|
||||||
assertNull(built.rejection());
|
assertNull(built.rejection());
|
||||||
RiverCourse course = course(built.segments());
|
RiverCourse course = course(built.segments());
|
||||||
HydraulicSegment fall = course.segments().stream().filter(HydraulicSegment::fallingFluid).findFirst().orElseThrow();
|
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());
|
assertEquals(full.columns().stream().filter(value -> bounds.contains(value.x(), value.z())).toList(), bounded.columns());
|
||||||
for (SurfaceLayerColumn value : full.columns()) {
|
for (SurfaceLayerColumn value : full.columns()) {
|
||||||
if (value.layer().terrainOwned()) {
|
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");
|
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
|
||||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||||
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
|
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)),
|
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 300, 0), new HydrologyPoint(200, 100, 0)),
|
||||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
SurfaceTerminal.TRIBUTARY, 100, 64);
|
||||||
assertNull("detail=" + built.rejectionDetail(), built.rejection());
|
assertNull("detail=" + built.rejectionDetail(), built.rejection());
|
||||||
SurfaceFootprint footprint = compiler(sampler).compile(course(built.segments()));
|
SurfaceFootprint footprint = compiler(sampler).compile(course(built.segments()));
|
||||||
assertNull("detail=" + footprint.rejectionDetail(), footprint.rejection());
|
assertNull("detail=" + footprint.rejectionDetail(), footprint.rejection());
|
||||||
@@ -276,8 +398,9 @@ public class SurfaceFootprintCompilerTest {
|
|||||||
x + z < 200 ? 100 : 86, 0D, "land");
|
x + z < 200 ? 100 : 86, 0D, "land");
|
||||||
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
|
||||||
SurfaceCourseResult built = new SurfaceCourseBuilder(settings.surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL)
|
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)),
|
.build(7L, 1L, "water", List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 99),
|
||||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
new HydrologyPoint(100, 86, 100), new HydrologyPoint(200, 86, 200)),
|
||||||
|
SurfaceTerminal.TRIBUTARY, 86, 64);
|
||||||
assertNull(built.rejection());
|
assertNull(built.rejection());
|
||||||
RiverCourse course = course(built.segments());
|
RiverCourse course = course(built.segments());
|
||||||
assertTrue(course.segments().stream().anyMatch(segment -> segment.fallingFluid() && segment.drop() == 14));
|
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");
|
: HydrologyTerrainSample.openLand((x < 100 ? 100 : 90) + Math.abs(z) / 3, 0D, "land");
|
||||||
SurfaceCourseResult built = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler,
|
SurfaceCourseResult built = new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler,
|
||||||
CONSTANT_GEOMETRY, SEA_LEVEL).build(7L, 1L, "water",
|
CONSTANT_GEOMETRY, SEA_LEVEL).build(7L, 1L, "water",
|
||||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(180, 0, 0)),
|
List.of(new HydrologyPoint(0, 100, 0), new HydrologyPoint(99, 100, 0),
|
||||||
SurfaceTerminal.TRIBUTARY, 40, 64);
|
new HydrologyPoint(100, 90, 0), new HydrologyPoint(180, 90, 0)),
|
||||||
|
SurfaceTerminal.TRIBUTARY, 90, 64);
|
||||||
assertNull(built.rejection());
|
assertNull(built.rejection());
|
||||||
RiverCourse falling = course(built.segments());
|
RiverCourse falling = course(built.segments());
|
||||||
RiverCourse mouth = course(List.of(segment(1L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL,
|
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());
|
assertNull(valley.rejection());
|
||||||
assertEquals(100, valley.head()[130]);
|
assertEquals(100, valley.head()[130]);
|
||||||
assertEquals(98, valley.head()[149]);
|
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,
|
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
|
||||||
HydrologyPlannerSettings.Ponds.none());
|
HydrologyPlannerSettings.Ponds.none());
|
||||||
assertEquals(0, field.uncontainedWetCells());
|
assertEquals(0, field.uncontainedWetCells());
|
||||||
@@ -214,7 +214,7 @@ public class ValleyProfileSolverTest {
|
|||||||
assertEquals(120, valley.head()[110]);
|
assertEquals(120, valley.head()[110]);
|
||||||
assertEquals(118, valley.head()[128]);
|
assertEquals(118, valley.head()[128]);
|
||||||
assertContained(valley);
|
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,
|
.compile(77L, centerline, channel, valley, SurfaceTerminal.TRIBUTARY, 0,
|
||||||
HydrologyPlannerSettings.Ponds.none());
|
HydrologyPlannerSettings.Ponds.none());
|
||||||
assertEquals(0, field.uncontainedWetCells());
|
assertEquals(0, field.uncontainedWetCells());
|
||||||
@@ -231,7 +231,7 @@ public class ValleyProfileSolverTest {
|
|||||||
assertNull(valley.rejection());
|
assertNull(valley.rejection());
|
||||||
assertTrue(valley.exposedStations() < 240);
|
assertTrue(valley.exposedStations() < 240);
|
||||||
assertEquals(92, valley.head()[valley.exposedStations() - 1]);
|
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,
|
.compile(77L, centerline, channel, valley, SurfaceTerminal.COASTAL_GROTTO, 0,
|
||||||
HydrologyPlannerSettings.Ponds.none());
|
HydrologyPlannerSettings.Ponds.none());
|
||||||
assertEquals(0, field.uncontainedWetCells());
|
assertEquals(0, field.uncontainedWetCells());
|
||||||
@@ -492,4 +492,11 @@ public class ValleyProfileSolverTest {
|
|||||||
private static List<HydrologyPoint> path(int stations) {
|
private static List<HydrologyPoint> path(int stations) {
|
||||||
return List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
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
|
@Test
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public void translatedTreeSchedulesDistantOwnerChunks() throws Exception {
|
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
|
||||||
File objectFile = temporaryFolder.newFile("tree.iob");
|
File objectFile = temporaryFolder.newFile("tree.iob");
|
||||||
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
|
||||||
output.writeInt(1);
|
output.writeInt(1);
|
||||||
@@ -293,8 +293,8 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
|
|
||||||
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
|
||||||
assertEquals(33, component.getRadius());
|
assertEquals(33, component.getRadius());
|
||||||
assertEquals(33, component.getOutputRadius());
|
assertEquals(0, component.getOutputRadius());
|
||||||
assertEquals(0, component.getInputRadius());
|
assertEquals(82, component.getInputRadius());
|
||||||
|
|
||||||
Mantle<Matter> mantle = mock(Mantle.class);
|
Mantle<Matter> mantle = mock(Mantle.class);
|
||||||
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
MantleChunk<Matter> chunk = mock(MantleChunk.class);
|
||||||
@@ -322,9 +322,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
|
|
||||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||||
|
|
||||||
assertEquals(1, directOrigins.get());
|
assertEquals(49, directOrigins.get());
|
||||||
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
|
||||||
assertEquals(2, directOrigins.get());
|
assertEquals(49, directOrigins.get());
|
||||||
|
|
||||||
List<String> generatedChunks = new ArrayList<>();
|
List<String> generatedChunks = new ArrayList<>();
|
||||||
doAnswer(invocation -> {
|
doAnswer(invocation -> {
|
||||||
@@ -342,7 +342,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
|
|||||||
));
|
));
|
||||||
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
|
generator.generateMatter(0, 0, false, mock(ChunkContext.class));
|
||||||
|
|
||||||
assertEquals(49, generatedChunks.size());
|
assertEquals(List.of("0,0"), generatedChunks);
|
||||||
|
|
||||||
List<String> replayedSources = new ArrayList<>();
|
List<String> replayedSources = new ArrayList<>();
|
||||||
MantleObjectComponent.replaySourceChunks(
|
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);
|
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
|
@Test
|
||||||
public void sourcePlanRejectsAnInvalidCheckpoint() {
|
public void sourcePlanRejectsAnInvalidCheckpoint() {
|
||||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer(), 0, 0);
|
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer(), 0, 0);
|
||||||
|
|||||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user