mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Split hydrology tile planner
- Keep planner state, caches, cross-tile entry points and terrain sampling in HydrologyPlanner and move tile planning into ten collaborators for cross-tile resolution, outlets, sources, route geometry, route paths, surface courses, tributaries, underground courses, hydraulic segments and feature sites - Promote the shared planning records to top level so the collaborators address routes, grids, drafts and course paths directly - Pin a full four-tile plan digest for two seeds in HydrologyPlanHashTest - Requalify the reflective and package-private planner call sites in the hydrology tests onto the new owners
This commit is contained in:
1 parent
422618282e
commit
a31d494f45
54 files changed
+8734
-8231
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
record CompiledGraph(
|
||||||
|
List<DrainageNode> nodes,
|
||||||
|
List<DrainageEdge> edges,
|
||||||
|
List<RiverOutlet> outlets,
|
||||||
|
Map<Integer, DrainageEdge> edgeByUpstream
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CourseEdgeKey(RiverCourseType type, long edgeId) {
|
||||||
|
CourseEdgeKey {
|
||||||
|
Objects.requireNonNull(type, "type");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CrossTileBlockers(
|
||||||
|
List<HydrologyCrossTileCaveAdmission.RankedClaim> caveClaims,
|
||||||
|
List<HydrologyCrossTileSurfaceAdmission.RankedClaim> surfaceClaims
|
||||||
|
) {
|
||||||
|
CrossTileBlockers {
|
||||||
|
caveClaims = List.copyOf(Objects.requireNonNull(caveClaims, "caveClaims"));
|
||||||
|
surfaceClaims = List.copyOf(Objects.requireNonNull(surfaceClaims, "surfaceClaims"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
interface CrossTileDraftAdmission {
|
||||||
|
void prepare();
|
||||||
|
|
||||||
|
CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result);
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CrossTilePublicationAdmission(
|
||||||
|
HydrologyCaveCourseFilter.Result result,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||||
|
boolean rejectedCourses
|
||||||
|
) {
|
||||||
|
CrossTilePublicationAdmission {
|
||||||
|
Objects.requireNonNull(result, "result");
|
||||||
|
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CrossTileRejectedCourse(RiverCourse course, long winnerSourceId) {
|
||||||
|
CrossTileRejectedCourse {
|
||||||
|
Objects.requireNonNull(course, "course");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record CrossTileRejectionKey(long courseId, long winnerCourseId) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CrossTileResolution(
|
||||||
|
HydrologyOwnerDraft draft,
|
||||||
|
List<CrossTileRejectedCourse> observedRejections,
|
||||||
|
int iterations,
|
||||||
|
int ownerCount,
|
||||||
|
long resolutionNanos
|
||||||
|
) {
|
||||||
|
CrossTileResolution {
|
||||||
|
Objects.requireNonNull(draft, "draft");
|
||||||
|
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class CrossTileResolutionContext {
|
||||||
|
final Map<HydrologyTileKey, CrossTileResolvedOwner> resolved;
|
||||||
|
final Set<HydrologyTileKey> resolving;
|
||||||
|
final HydrologyTileKey root;
|
||||||
|
final long maximumTileOffset;
|
||||||
|
final int maximumOwners;
|
||||||
|
int iterations;
|
||||||
|
|
||||||
|
CrossTileResolutionContext(
|
||||||
|
HydrologyTileKey root,
|
||||||
|
long maximumTileOffset,
|
||||||
|
int maximumOwners
|
||||||
|
) {
|
||||||
|
this.resolved = new HashMap<>();
|
||||||
|
this.resolving = new HashSet<>();
|
||||||
|
this.root = Objects.requireNonNull(root, "root");
|
||||||
|
this.maximumTileOffset = maximumTileOffset;
|
||||||
|
this.maximumOwners = maximumOwners;
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossTileResolvedOwner resolved(HydrologyTileKey key) {
|
||||||
|
return resolved.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remember(HydrologyTileKey key, CrossTileResolvedOwner owner) {
|
||||||
|
long offsetX = Math.abs((long) key.tileX() - root.tileX());
|
||||||
|
long offsetZ = Math.abs((long) key.tileZ() - root.tileZ());
|
||||||
|
if (offsetX > maximumTileOffset || offsetZ > maximumTileOffset) {
|
||||||
|
throw new IllegalStateException("Cross-tile owner dependency exceeded its color-ranked geometry bound.");
|
||||||
|
}
|
||||||
|
CrossTileResolvedOwner existing = resolved.putIfAbsent(key, owner);
|
||||||
|
if (existing != null
|
||||||
|
&& !existing.withoutFootprintCompiler().equals(owner.withoutFootprintCompiler())) {
|
||||||
|
throw new IllegalStateException("Cross-tile owner resolution produced inconsistent cached results.");
|
||||||
|
}
|
||||||
|
if (resolved.size() > maximumOwners) {
|
||||||
|
throw new IllegalStateException("Cross-tile owner dependencies exceeded their color-ranked count bound.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean begin(HydrologyTileKey key) {
|
||||||
|
return resolving.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void end(HydrologyTileKey key) {
|
||||||
|
resolving.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordIteration() {
|
||||||
|
iterations++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int iterations() {
|
||||||
|
return iterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ownerCount() {
|
||||||
|
return resolved.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyTileKey root() {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
record CrossTileResolvedOwner(
|
||||||
|
HydrologyOwnerDraft draft,
|
||||||
|
List<CrossTileRejectedCourse> observedRejections
|
||||||
|
) {
|
||||||
|
CrossTileResolvedOwner {
|
||||||
|
Objects.requireNonNull(draft, "draft");
|
||||||
|
observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections"));
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossTileResolvedOwner withoutFootprintCompiler() {
|
||||||
|
HydrologyOwnerDraft uncachedDraft = draft.withoutFootprintCompiler();
|
||||||
|
return uncachedDraft == draft
|
||||||
|
? this
|
||||||
|
: new CrossTileResolvedOwner(uncachedDraft, observedRejections);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
enum DraftPhase {
|
||||||
|
CONTEXT,
|
||||||
|
SELECT,
|
||||||
|
SETTLE,
|
||||||
|
PUBLISH
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wall-clock breakdown of one owner draft for the debug log: sampling and routing context, source
|
||||||
|
* selection, settling, publication passes, and the time spent waiting on lower-rank neighbour drafts.
|
||||||
|
*/
|
||||||
|
final class DraftProfile {
|
||||||
|
final long[] phaseNanos = new long[DraftPhase.values().length];
|
||||||
|
long dependencyNanos;
|
||||||
|
int dependencies;
|
||||||
|
int admissions;
|
||||||
|
int earlyOwners;
|
||||||
|
int routeCalls;
|
||||||
|
int routeSolves;
|
||||||
|
long routeSolveNanos;
|
||||||
|
int rasterCalls;
|
||||||
|
long rasterNanos;
|
||||||
|
int filterCalls;
|
||||||
|
long filterNanos;
|
||||||
|
|
||||||
|
long record(DraftPhase phase, long started) {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
phaseNanos[phase.ordinal()] += now - started;
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
void recordDependencies(int count, long started) {
|
||||||
|
dependencyNanos += System.nanoTime() - started;
|
||||||
|
dependencies += count;
|
||||||
|
admissions++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void log(HydrologyTileKey key, int rank, long totalNanos) {
|
||||||
|
IrisLogging.debug(
|
||||||
|
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d earlyOwners=%d routes=%d reuses=%d routeSolve=%dms rasters=%d raster=%dms filters=%d filter=%dms on %s",
|
||||||
|
key.tileX(),
|
||||||
|
key.tileZ(),
|
||||||
|
rank,
|
||||||
|
totalNanos / 1_000_000L,
|
||||||
|
phaseNanos[DraftPhase.CONTEXT.ordinal()] / 1_000_000L,
|
||||||
|
phaseNanos[DraftPhase.SELECT.ordinal()] / 1_000_000L,
|
||||||
|
phaseNanos[DraftPhase.SETTLE.ordinal()] / 1_000_000L,
|
||||||
|
phaseNanos[DraftPhase.PUBLISH.ordinal()] / 1_000_000L,
|
||||||
|
dependencies,
|
||||||
|
dependencyNanos / 1_000_000L,
|
||||||
|
admissions,
|
||||||
|
earlyOwners,
|
||||||
|
routeCalls,
|
||||||
|
routeCalls - routeSolves,
|
||||||
|
routeSolveNanos / 1_000_000L,
|
||||||
|
rasterCalls,
|
||||||
|
rasterNanos / 1_000_000L,
|
||||||
|
filterCalls,
|
||||||
|
filterNanos / 1_000_000L,
|
||||||
|
Thread.currentThread().getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A routed source-to-outlet path, or, when {@code junction} is set, the cut prefix of one that ends
|
||||||
|
* on the stem it joins: the path then owns only the drainage edges upstream of the junction.
|
||||||
|
*/
|
||||||
|
record HydrologyCoursePath(
|
||||||
|
List<HydrologyPoint> points,
|
||||||
|
List<DrainageEdge> pairEdges,
|
||||||
|
List<DrainageEdge> edges,
|
||||||
|
RiverOutlet outlet,
|
||||||
|
boolean reachesOutlet,
|
||||||
|
boolean organicSurfaceRequired,
|
||||||
|
HydrologyPoint junction
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,913 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.cave.CavePosition;
|
||||||
|
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||||
|
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate;
|
||||||
|
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveContainmentPlanner;
|
||||||
|
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
final class HydrologyCrossTileResolver {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologyCrossTileResolver(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyOwnerDraft compileOwnerDraft(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
CrossTileDraftAdmission crossTileAdmission,
|
||||||
|
boolean retainFootprintCompiler
|
||||||
|
) {
|
||||||
|
HydrologyPlanner.PlanningSamples previous = planner.planningSamples.get();
|
||||||
|
if (previous == null) {
|
||||||
|
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||||
|
}
|
||||||
|
DraftProfile previousProfile = planner.draftProfiles.get();
|
||||||
|
DraftProfile profile = new DraftProfile();
|
||||||
|
planner.draftProfiles.set(profile);
|
||||||
|
long started = System.nanoTime();
|
||||||
|
try {
|
||||||
|
HydrologyOwnerDraft draft = compileScopedOwnerDraft(
|
||||||
|
key,
|
||||||
|
crossTileAdmission,
|
||||||
|
retainFootprintCompiler
|
||||||
|
);
|
||||||
|
profile.log(key, ownerColorRank(key), System.nanoTime() - started);
|
||||||
|
return draft;
|
||||||
|
} finally {
|
||||||
|
if (previous == null) {
|
||||||
|
planner.planningSamples.remove();
|
||||||
|
}
|
||||||
|
if (previousProfile == null) {
|
||||||
|
planner.draftProfiles.remove();
|
||||||
|
} else {
|
||||||
|
planner.draftProfiles.set(previousProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyOwnerDraft compileScopedOwnerDraft(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
CrossTileDraftAdmission crossTileAdmission,
|
||||||
|
boolean retainFootprintCompiler
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(key, "key");
|
||||||
|
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||||
|
HydrologySampledGrid grid = null;
|
||||||
|
HydrologyRoutingPlan surfaceRouting = null;
|
||||||
|
HydrologyRoutingPlan undergroundRouting = null;
|
||||||
|
SourceSelection surfaceSelection = SourceSelection.empty(true);
|
||||||
|
SourceSelection undergroundSelection = SourceSelection.empty(false);
|
||||||
|
DraftProfile profile = planner.currentDraftProfile();
|
||||||
|
long phaseStarted = System.nanoTime();
|
||||||
|
if (planner.sourcePlanner.hasRoutedSourceSearch()) {
|
||||||
|
SourceRoutingContext primaryContext = planner.sourceRoutingContext(key);
|
||||||
|
phaseStarted = profile.record(DraftPhase.CONTEXT, phaseStarted);
|
||||||
|
grid = primaryContext.grid();
|
||||||
|
surfaceRouting = primaryContext.surfaceRouting();
|
||||||
|
undergroundRouting = primaryContext.undergroundRouting();
|
||||||
|
diagnostics.addAll(primaryContext.diagnostics());
|
||||||
|
HashMap<HydrologyTileKey, SourceRoutingContext> sourceRoutingContexts = new HashMap<>();
|
||||||
|
sourceRoutingContexts.put(key, primaryContext);
|
||||||
|
surfaceSelection = planner.sourcePlanner.selectSources(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
diagnostics,
|
||||||
|
sourceRoutingContexts
|
||||||
|
);
|
||||||
|
undergroundSelection = planner.sourcePlanner.selectSources(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
undergroundRouting,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
diagnostics,
|
||||||
|
sourceRoutingContexts
|
||||||
|
);
|
||||||
|
phaseStarted = profile.record(DraftPhase.SELECT, phaseStarted);
|
||||||
|
if (crossTileAdmission != null && (!surfaceSelection.selectedCandidateIndices.isEmpty()
|
||||||
|
|| !undergroundSelection.selectedCandidateIndices.isEmpty())) {
|
||||||
|
crossTileAdmission.prepare();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HydrologyFootprintCompiler footprintCompiler = new HydrologyFootprintCompiler(
|
||||||
|
planner.settings,
|
||||||
|
new HydrologyFootprintCompiler.Sampling(
|
||||||
|
planner.sampler,
|
||||||
|
planner.geometrySampler,
|
||||||
|
planner.naturalSampler
|
||||||
|
)
|
||||||
|
);
|
||||||
|
HashMap<Long, List<HydrologyPoint>> refinedEdges = new HashMap<>();
|
||||||
|
HashMap<SourceCompilationKey, SourceCompilation> sourceCompilations = new HashMap<>();
|
||||||
|
settleSelection(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
surfaceSelection,
|
||||||
|
true,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
settleSelection(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
undergroundSelection,
|
||||||
|
false,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
phaseStarted = profile.record(DraftPhase.SETTLE, phaseStarted);
|
||||||
|
PublicationAttempt publication = null;
|
||||||
|
List<OutletCandidate> fallbackOutlets = grid == null || surfaceRouting == null
|
||||||
|
? List.of()
|
||||||
|
: planner.outletPlanner.resolveSurfaceFallbackOutlets(grid);
|
||||||
|
int fallbackOutletIndex = 0;
|
||||||
|
while (true) {
|
||||||
|
int maximumPublicationPasses = Math.addExact(
|
||||||
|
1,
|
||||||
|
Math.addExact(
|
||||||
|
surfaceSelection.candidateCount(),
|
||||||
|
undergroundSelection.candidateCount()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (int publicationPass = 0; publicationPass < maximumPublicationPasses; publicationPass++) {
|
||||||
|
publication = compilePublication(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
surfaceSelection,
|
||||||
|
undergroundSelection,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
diagnostics.addAll(publication.diagnostics());
|
||||||
|
if (crossTileAdmission != null) {
|
||||||
|
CrossTilePublicationAdmission admitted = crossTileAdmission.admit(publication.result());
|
||||||
|
publication = new PublicationAttempt(admitted.result(), publication.diagnostics());
|
||||||
|
diagnostics.addAll(admitted.diagnostics());
|
||||||
|
}
|
||||||
|
if (grid == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
boolean surfaceChanged = surfaceSelection.advanceAfterPublication(
|
||||||
|
publication.result().courses(),
|
||||||
|
grid
|
||||||
|
);
|
||||||
|
boolean undergroundChanged = undergroundSelection.advanceAfterPublication(
|
||||||
|
publication.result().courses(),
|
||||||
|
grid
|
||||||
|
);
|
||||||
|
if (!surfaceChanged && !undergroundChanged) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (surfaceChanged) {
|
||||||
|
settleSelection(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
surfaceSelection,
|
||||||
|
true,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (undergroundChanged) {
|
||||||
|
settleSelection(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
undergroundSelection,
|
||||||
|
false,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (publicationPass + 1 >= maximumPublicationPasses) {
|
||||||
|
throw new IllegalStateException("Hydrology source publication exceeded its candidate bound.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (grid == null || !surfaceSelection.needsSurfaceFallback()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
boolean fallbackSelected = false;
|
||||||
|
while (fallbackOutletIndex < fallbackOutlets.size()) {
|
||||||
|
OutletCandidate fallbackOutlet = fallbackOutlets.get(fallbackOutletIndex++);
|
||||||
|
HydrologyRoutingPlan fallbackRouting = planner.sourcePlanner.requireOrganicSurface(planner.sourcePlanner.buildRouting(grid, List.of(fallbackOutlet), true));
|
||||||
|
HashMap<HydrologyTileKey, SourceRoutingContext> fallbackContexts = new HashMap<>();
|
||||||
|
fallbackContexts.put(key, new SourceRoutingContext(
|
||||||
|
grid,
|
||||||
|
fallbackRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
List.of()
|
||||||
|
));
|
||||||
|
SourceSelection fallbackSelection = planner.sourcePlanner.selectSources(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
fallbackRouting,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
diagnostics,
|
||||||
|
fallbackContexts
|
||||||
|
);
|
||||||
|
settleSelection(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
fallbackRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
fallbackSelection,
|
||||||
|
true,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
if (!fallbackSelection.hasAcceptedSelection()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
surfaceRouting = fallbackRouting;
|
||||||
|
surfaceSelection = fallbackSelection;
|
||||||
|
fallbackSelected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!fallbackSelected) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profile.record(DraftPhase.PUBLISH, phaseStarted);
|
||||||
|
if (publication == null) {
|
||||||
|
throw new IllegalStateException("Hydrology publication did not execute.");
|
||||||
|
}
|
||||||
|
if (grid != null) {
|
||||||
|
surfaceSelection.addFinalAdmissionDiagnostics(grid, diagnostics);
|
||||||
|
undergroundSelection.addFinalAdmissionDiagnostics(grid, diagnostics);
|
||||||
|
}
|
||||||
|
HydrologyCaveCourseFilter.Result result = publication.result();
|
||||||
|
List<HydrologyDiagnosticCandidate> uniqueDiagnostics = planner.sourcePlanner.uniqueDiagnostics(diagnostics);
|
||||||
|
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(
|
||||||
|
key,
|
||||||
|
result,
|
||||||
|
uniqueDiagnostics,
|
||||||
|
retainFootprintCompiler ? footprintCompiler : null
|
||||||
|
);
|
||||||
|
validateOwnerDraftReach(draft);
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyTile materializeAcceptedTile(CrossTileResolution resolution) {
|
||||||
|
HydrologyCaveCourseFilter.Result result = resolution.draft().result();
|
||||||
|
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>(resolution.draft().diagnostics());
|
||||||
|
for (CrossTileRejectedCourse rejection : resolution.observedRejections()) {
|
||||||
|
diagnostics.add(rejection.course().type() == RiverCourseType.SURFACE
|
||||||
|
? surfaceConflictDiagnostic(rejection.course(), rejection.winnerSourceId())
|
||||||
|
: HydrologyCaveCourseFilter.overlapDiagnostic(
|
||||||
|
rejection.course(),
|
||||||
|
rejection.winnerSourceId()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
HydrologyFootprintCompiler footprintCompiler = resolution.draft().footprintCompiler();
|
||||||
|
if (footprintCompiler == null) {
|
||||||
|
footprintCompiler = new HydrologyFootprintCompiler(
|
||||||
|
planner.settings,
|
||||||
|
new HydrologyFootprintCompiler.Sampling(
|
||||||
|
planner.sampler,
|
||||||
|
planner.geometrySampler,
|
||||||
|
planner.naturalSampler
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
MaterializedHydrology materialized = materializeFinalHydrology(
|
||||||
|
result,
|
||||||
|
diagnostics,
|
||||||
|
footprintCompiler,
|
||||||
|
!resolution.observedRejections().isEmpty()
|
||||||
|
);
|
||||||
|
result = materialized.result();
|
||||||
|
RiverFootprint footprint = materialized.footprint();
|
||||||
|
if (footprintCompiler.fullMaterializationCount() < 1) {
|
||||||
|
throw new IllegalStateException("Hydrology publication did not materialize its full footprint.");
|
||||||
|
}
|
||||||
|
return new HydrologyTile(
|
||||||
|
resolution.draft().key(),
|
||||||
|
planner.worldSeed,
|
||||||
|
planner.settings.fingerprint(),
|
||||||
|
planner.settings.routing().tileSize(),
|
||||||
|
result.nodes(),
|
||||||
|
result.edges(),
|
||||||
|
result.outlets(),
|
||||||
|
result.courses(),
|
||||||
|
result.cavePlans(),
|
||||||
|
planner.sourcePlanner.uniqueDiagnostics(diagnostics),
|
||||||
|
footprint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterializedHydrology materializeFinalHydrology(
|
||||||
|
HydrologyCaveCourseFilter.Result initial,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||||
|
HydrologyFootprintCompiler footprintCompiler,
|
||||||
|
boolean revalidationRequired
|
||||||
|
) {
|
||||||
|
HydrologyCaveCourseFilter.Result current = initial;
|
||||||
|
int maximumPasses = Math.addExact(initial.courses().size(), 1);
|
||||||
|
for (int pass = 0; pass < maximumPasses; pass++) {
|
||||||
|
RiverFootprint footprint = footprintCompiler.compile(current.courses());
|
||||||
|
if (current.cavePlans().isEmpty()) {
|
||||||
|
return new MaterializedHydrology(current, footprint);
|
||||||
|
}
|
||||||
|
HydrologyFootprintCompiler.ValidationRaster compactValidation = footprintCompiler.compileValidation(
|
||||||
|
current.courses()
|
||||||
|
);
|
||||||
|
if (!revalidationRequired
|
||||||
|
&& compactSurfaceMatchesMaterializedPlans(current, compactValidation, footprint)) {
|
||||||
|
return new MaterializedHydrology(current, footprint);
|
||||||
|
}
|
||||||
|
HydrologyFootprintCompiler.ValidationRaster validation = compactValidation.withMaterializedSurface(
|
||||||
|
footprint
|
||||||
|
);
|
||||||
|
HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface(
|
||||||
|
validation.plannedSurface()
|
||||||
|
);
|
||||||
|
CaveVoxelView caveView = Objects.requireNonNull(
|
||||||
|
planner.caveViewFactory.create(plannedSurface),
|
||||||
|
"Hydrology cave view factory returned null"
|
||||||
|
);
|
||||||
|
HydrologyCaveCourseFilter.Result revalidated = new HydrologyCaveCourseFilter(
|
||||||
|
caveView,
|
||||||
|
new HydrologyCaveCourseFilter.Options(
|
||||||
|
planner.settings.underground().connectToExistingCaves(),
|
||||||
|
planner.settings.outlets().coastalGrotto().maximumVolume(),
|
||||||
|
planner.settings.outlets().inlandGrotto().maximumVolume()
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
plannedSurface
|
||||||
|
).filter(
|
||||||
|
current.nodes(),
|
||||||
|
current.edges(),
|
||||||
|
current.outlets(),
|
||||||
|
current.courses(),
|
||||||
|
validation,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
if (revalidated.courses().equals(current.courses())) {
|
||||||
|
return new MaterializedHydrology(revalidated, footprint);
|
||||||
|
}
|
||||||
|
current = revalidated;
|
||||||
|
revalidationRequired = true;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Final hydrology cave publication exceeded its course bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean compactSurfaceMatchesMaterializedPlans(
|
||||||
|
HydrologyCaveCourseFilter.Result result,
|
||||||
|
HydrologyFootprintCompiler.ValidationRaster validation,
|
||||||
|
RiverFootprint footprint
|
||||||
|
) {
|
||||||
|
HashSet<Long> visitedColumns = new HashSet<>();
|
||||||
|
for (HydrologyCavePlan plan : result.cavePlans()) {
|
||||||
|
for (CavePosition position : plan.baselinePreconditions().keySet()) {
|
||||||
|
long packed = RiverFootprint.pack(position.x(), position.z());
|
||||||
|
if (!visitedColumns.add(packed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyColumnSample sample = footprint.sample(position.x(), position.z()).orElse(null);
|
||||||
|
HydrologyTerrainSample terrain = sample == null
|
||||||
|
? Objects.requireNonNull(planner.sampler.sample(position.x(), position.z()))
|
||||||
|
: null;
|
||||||
|
int naturalHeight = sample == null ? terrain.naturalHeight() : sample.naturalHeight();
|
||||||
|
int materializedHeight = sample == null ? naturalHeight : sample.terrainHeight();
|
||||||
|
int compactHeight = validation.plannedSurface().resolve(
|
||||||
|
position.x(),
|
||||||
|
position.z(),
|
||||||
|
naturalHeight
|
||||||
|
);
|
||||||
|
if (compactHeight != materializedHeight) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossTileBlockers lowerRankBlockers(
|
||||||
|
HydrologyTileKey ownerKey,
|
||||||
|
HydrologyCaveCourseFilter.Result result,
|
||||||
|
int ownerRank,
|
||||||
|
CrossTileResolutionContext context,
|
||||||
|
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners
|
||||||
|
) {
|
||||||
|
ArrayList<HydrologyCrossTileCaveAdmission.RankedClaim> blockers = new ArrayList<>();
|
||||||
|
ArrayList<HydrologyCrossTileSurfaceAdmission.RankedClaim> surfaceBlockers = new ArrayList<>();
|
||||||
|
ArrayList<HydrologyTileKey> candidateKeys = new ArrayList<>();
|
||||||
|
for (HydrologyTileKey candidateKey : conflictOwnerKeys(ownerKey, result)) {
|
||||||
|
if (candidateKey.equals(ownerKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int candidateRank = ownerColorRank(candidateKey);
|
||||||
|
if (candidateRank == ownerRank) {
|
||||||
|
throw new IllegalStateException("Equal owner colors cannot share a cave-action conflict envelope.");
|
||||||
|
}
|
||||||
|
if (candidateRank > ownerRank) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
candidateKeys.add(candidateKey);
|
||||||
|
}
|
||||||
|
long dependenciesStarted = System.nanoTime();
|
||||||
|
List<CrossTileResolvedOwner> resolvedCandidates = planner.resolveLowerRankOwners(candidateKeys, context, preparedOwners);
|
||||||
|
planner.currentDraftProfile().recordDependencies(candidateKeys.size(), dependenciesStarted);
|
||||||
|
for (int index = 0; index < candidateKeys.size(); index++) {
|
||||||
|
HydrologyTileKey candidateKey = candidateKeys.get(index);
|
||||||
|
int candidateRank = ownerColorRank(candidateKey);
|
||||||
|
CrossTileResolvedOwner resolved = resolvedCandidates.get(index);
|
||||||
|
for (HydrologyCrossTileCaveAdmission.Claim claim : caveClaims(resolved.draft())) {
|
||||||
|
blockers.add(new HydrologyCrossTileCaveAdmission.RankedClaim(
|
||||||
|
candidateKey,
|
||||||
|
candidateRank,
|
||||||
|
claim
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (HydrologyCrossTileSurfaceAdmission.Claim claim : surfaceClaims(resolved.draft())) {
|
||||||
|
surfaceBlockers.add(new HydrologyCrossTileSurfaceAdmission.RankedClaim(
|
||||||
|
candidateKey,
|
||||||
|
candidateRank,
|
||||||
|
claim
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new CrossTileBlockers(List.copyOf(blockers), List.copyOf(surfaceBlockers));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyTileKey> conflictOwnerKeys(HydrologyOwnerDraft target) {
|
||||||
|
return conflictOwnerKeys(target.key(), target.result());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyTileKey> conflictOwnerKeys(
|
||||||
|
HydrologyTileKey ownerKey,
|
||||||
|
HydrologyCaveCourseFilter.Result result
|
||||||
|
) {
|
||||||
|
int tileSize = planner.settings.routing().tileSize();
|
||||||
|
int reach = Math.addExact(planner.settings.publicationRadius(), 1);
|
||||||
|
int colorPeriod = planner.settings.crossTileColorPeriod();
|
||||||
|
LongOpenHashSet columns = new LongOpenHashSet();
|
||||||
|
for (HydrologyCavePlan plan : result.cavePlans()) {
|
||||||
|
for (CavePosition position : plan.actions().keySet()) {
|
||||||
|
columns.add(RiverFootprint.pack(position.x(), position.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (RiverCourse course : result.courses()) {
|
||||||
|
if (course.type() != RiverCourseType.SURFACE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (HydraulicSegment segment : course.segments()) {
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
columns.add(RiverFootprint.pack(point.x(), point.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkedHashSet<HydrologyTileKey> owners = new LinkedHashSet<>();
|
||||||
|
owners.add(ownerKey);
|
||||||
|
LongIterator iterator = columns.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
long column = iterator.nextLong();
|
||||||
|
int x = RiverFootprint.unpackX(column);
|
||||||
|
int z = RiverFootprint.unpackZ(column);
|
||||||
|
int minimumTileX = ownerCoordinate((long) x - reach, tileSize);
|
||||||
|
int maximumTileX = ownerCoordinate((long) x + reach, tileSize);
|
||||||
|
int minimumTileZ = ownerCoordinate((long) z - reach, tileSize);
|
||||||
|
int maximumTileZ = ownerCoordinate((long) z + reach, tileSize);
|
||||||
|
long ownerWidth = Math.addExact((long) maximumTileX - minimumTileX, 1L);
|
||||||
|
long ownerDepth = Math.addExact((long) maximumTileZ - minimumTileZ, 1L);
|
||||||
|
if (ownerWidth > colorPeriod || ownerDepth > colorPeriod) {
|
||||||
|
throw new IllegalStateException("A cave action exceeds its bounded owner color neighborhood.");
|
||||||
|
}
|
||||||
|
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||||
|
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||||
|
owners.add(new HydrologyTileKey(tileX, tileZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int sourceReach = Math.max(0, planner.settings.surface().sources().minimumSpacing() - 1);
|
||||||
|
for (HydrologyCrossTileSurfaceAdmission.Claim claim : surfaceClaims(result)) {
|
||||||
|
HydrologyPoint source = claim.centerline().getFirst();
|
||||||
|
int minimumTileX = ownerCoordinate((long) source.x() - sourceReach, tileSize);
|
||||||
|
int maximumTileX = ownerCoordinate((long) source.x() + sourceReach, tileSize);
|
||||||
|
int minimumTileZ = ownerCoordinate((long) source.z() - sourceReach, tileSize);
|
||||||
|
int maximumTileZ = ownerCoordinate((long) source.z() + sourceReach, tileSize);
|
||||||
|
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||||
|
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||||
|
owners.add(new HydrologyTileKey(tileX, tileZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyTileKey> ordered = new ArrayList<>(owners);
|
||||||
|
ordered.sort(HydrologyTileKey::compareTo);
|
||||||
|
return List.copyOf(ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ownerColorRank(HydrologyTileKey key) {
|
||||||
|
int colorPeriod = planner.settings.crossTileColorPeriod();
|
||||||
|
int colorX = Math.floorMod(key.tileX(), colorPeriod);
|
||||||
|
int colorZ = Math.floorMod(key.tileZ(), colorPeriod);
|
||||||
|
return Math.addExact(Math.multiplyExact(colorZ, colorPeriod), colorX);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyCrossTileCaveAdmission.Claim> caveClaims(HydrologyOwnerDraft draft) {
|
||||||
|
return caveClaims(draft.result());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyCrossTileCaveAdmission.Claim> caveClaims(
|
||||||
|
HydrologyCaveCourseFilter.Result result
|
||||||
|
) {
|
||||||
|
if (result.cavePlans().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Map<Long, RiverCourse> coursesById = coursesById(result.courses());
|
||||||
|
ArrayList<HydrologyCrossTileCaveAdmission.Claim> claims =
|
||||||
|
new ArrayList<>(result.cavePlans().size());
|
||||||
|
for (HydrologyCavePlan plan : result.cavePlans()) {
|
||||||
|
RiverCourse course = coursesById.get(plan.source().sourceId());
|
||||||
|
if (course == null) {
|
||||||
|
throw new IllegalStateException("Accepted cave plan has no owner course.");
|
||||||
|
}
|
||||||
|
claims.add(new HydrologyCrossTileCaveAdmission.Claim(course.profileKey(), plan));
|
||||||
|
}
|
||||||
|
return List.copyOf(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyCrossTileSurfaceAdmission.Claim> surfaceClaims(HydrologyOwnerDraft draft) {
|
||||||
|
return surfaceClaims(draft.result());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyCrossTileSurfaceAdmission.Claim> surfaceClaims(
|
||||||
|
HydrologyCaveCourseFilter.Result result
|
||||||
|
) {
|
||||||
|
ArrayList<HydrologyCrossTileSurfaceAdmission.Claim> claims = new ArrayList<>();
|
||||||
|
for (RiverCourse course : result.courses()) {
|
||||||
|
if (course.type() != RiverCourseType.SURFACE || course.outletId().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> centerline = new ArrayList<>();
|
||||||
|
int maximumWidth = 1;
|
||||||
|
for (HydraulicSegment segment : course.segments()) {
|
||||||
|
maximumWidth = Math.max(maximumWidth, segment.width());
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
if (centerline.isEmpty()
|
||||||
|
|| point.x() != centerline.getLast().x()
|
||||||
|
|| point.z() != centerline.getLast().z()) {
|
||||||
|
centerline.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (centerline.size() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyFeatureType terminalType = course.segments().getLast().type();
|
||||||
|
boolean reachesOutlet = terminalType == HydrologyFeatureType.MOUTH
|
||||||
|
|| terminalType == HydrologyFeatureType.COASTAL_GROTTO
|
||||||
|
|| terminalType == HydrologyFeatureType.INLAND_GROTTO;
|
||||||
|
if (!reachesOutlet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
claims.add(new HydrologyCrossTileSurfaceAdmission.Claim(
|
||||||
|
course.id(),
|
||||||
|
course.outletId().getAsLong(),
|
||||||
|
centerline.getLast(),
|
||||||
|
reachesOutlet,
|
||||||
|
maximumWidth,
|
||||||
|
centerline
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return List.copyOf(claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, RiverCourse> coursesById(List<RiverCourse> courses) {
|
||||||
|
LinkedHashMap<Long, RiverCourse> indexed = new LinkedHashMap<>(courses.size());
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
RiverCourse existing = indexed.putIfAbsent(course.id(), course);
|
||||||
|
if (existing != null && !existing.equals(course)) {
|
||||||
|
throw new IllegalStateException("Hydrology course id collision.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Map.copyOf(indexed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void validateOwnerDraftReach(HydrologyOwnerDraft draft) {
|
||||||
|
int tileSize = planner.settings.routing().tileSize();
|
||||||
|
int reach = Math.addExact(planner.settings.publicationRadius(), 1);
|
||||||
|
long minimumX = (long) draft.key().minimumBlockX(tileSize) - reach;
|
||||||
|
long minimumZ = (long) draft.key().minimumBlockZ(tileSize) - reach;
|
||||||
|
long maximumX = (long) draft.key().minimumBlockX(tileSize) + tileSize - 1L + reach;
|
||||||
|
long maximumZ = (long) draft.key().minimumBlockZ(tileSize) + tileSize - 1L + reach;
|
||||||
|
for (HydrologyCavePlan plan : draft.result().cavePlans()) {
|
||||||
|
for (CavePosition position : plan.actions().keySet()) {
|
||||||
|
if (position.x() < minimumX || position.x() > maximumX
|
||||||
|
|| position.z() < minimumZ || position.z() > maximumZ) {
|
||||||
|
throw new IllegalStateException("Hydrology cave plan exceeds its bounded owner reach.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ownerCoordinate(long blockCoordinate, int tileSize) {
|
||||||
|
return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
void settleSelection(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan surfaceRouting,
|
||||||
|
HydrologyRoutingPlan undergroundRouting,
|
||||||
|
SourceSelection selection,
|
||||||
|
boolean surface,
|
||||||
|
HydrologyFootprintCompiler footprintCompiler,
|
||||||
|
Map<Long, List<HydrologyPoint>> refinedEdges,
|
||||||
|
Map<SourceCompilationKey, SourceCompilation> sourceCompilations,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
HydrologyRoutingPlan selectedRouting = surface ? surfaceRouting : undergroundRouting;
|
||||||
|
if (grid == null
|
||||||
|
|| selectedRouting == null
|
||||||
|
|| selection.selectedCandidateIndices.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SourceSelection emptySelection = SourceSelection.empty(!surface);
|
||||||
|
int maximumPasses = Math.addExact(1, selection.candidateCount());
|
||||||
|
for (int pass = 0; pass < maximumPasses; pass++) {
|
||||||
|
PublicationAttempt attempt = compilePublication(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
surface ? selection : emptySelection,
|
||||||
|
surface ? emptySelection : selection,
|
||||||
|
footprintCompiler,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
diagnostics.addAll(attempt.diagnostics());
|
||||||
|
if (!selection.advanceAfterPublication(attempt.result().courses(), grid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Hydrology source trial exceeded its candidate bound.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PublicationAttempt compilePublication(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan surfaceRouting,
|
||||||
|
HydrologyRoutingPlan undergroundRouting,
|
||||||
|
SourceSelection surfaceSelection,
|
||||||
|
SourceSelection undergroundSelection,
|
||||||
|
HydrologyFootprintCompiler footprintCompiler,
|
||||||
|
Map<Long, List<HydrologyPoint>> refinedEdges,
|
||||||
|
Map<SourceCompilationKey, SourceCompilation> sourceCompilations,
|
||||||
|
boolean includeDeepFluids
|
||||||
|
) {
|
||||||
|
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||||
|
ArrayList<RiverCourse> courses = new ArrayList<>();
|
||||||
|
CompiledGraph graph = new CompiledGraph(List.of(), List.of(), List.of(), Map.of());
|
||||||
|
if (grid != null) {
|
||||||
|
List<Integer> surfaceSources = surfaceSelection.selectedNodeIndices();
|
||||||
|
List<Integer> undergroundSources = undergroundSelection.selectedNodeIndices();
|
||||||
|
if (surfaceRouting != null && surfaceRouting == undergroundRouting) {
|
||||||
|
int[] surfaceContributions = planner.sourcePlanner.countContributions(surfaceSources, surfaceRouting);
|
||||||
|
int[] undergroundContributions = planner.sourcePlanner.countContributions(undergroundSources, surfaceRouting);
|
||||||
|
graph = planner.sourcePlanner.compileGraph(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
surfaceContributions,
|
||||||
|
undergroundContributions,
|
||||||
|
refinedEdges
|
||||||
|
);
|
||||||
|
if (planner.settings.surface().enabled()) {
|
||||||
|
planner.surfaceCourses.compileSurfaceCourses(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
graph,
|
||||||
|
surfaceSources,
|
||||||
|
courses,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (planner.settings.underground().enabled()) {
|
||||||
|
planner.undergroundCourses.compileUndergroundCourses(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
graph,
|
||||||
|
undergroundSources,
|
||||||
|
courses,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CompiledGraph surfaceGraph = new CompiledGraph(List.of(), List.of(), List.of(), Map.of());
|
||||||
|
CompiledGraph undergroundGraph = new CompiledGraph(List.of(), List.of(), List.of(), Map.of());
|
||||||
|
if (surfaceRouting != null && !surfaceSources.isEmpty()) {
|
||||||
|
SourceCompilation surfaceCompilation = compileSourceSelection(
|
||||||
|
true,
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
surfaceSources,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations
|
||||||
|
);
|
||||||
|
surfaceGraph = surfaceCompilation.graph();
|
||||||
|
courses.addAll(surfaceCompilation.courses());
|
||||||
|
diagnostics.addAll(surfaceCompilation.diagnostics());
|
||||||
|
}
|
||||||
|
if (undergroundRouting != null && !undergroundSources.isEmpty()) {
|
||||||
|
SourceCompilation undergroundCompilation = compileSourceSelection(
|
||||||
|
false,
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
undergroundRouting,
|
||||||
|
undergroundSources,
|
||||||
|
refinedEdges,
|
||||||
|
sourceCompilations
|
||||||
|
);
|
||||||
|
undergroundGraph = undergroundCompilation.graph();
|
||||||
|
courses.addAll(undergroundCompilation.courses());
|
||||||
|
diagnostics.addAll(undergroundCompilation.diagnostics());
|
||||||
|
}
|
||||||
|
graph = planner.sourcePlanner.mergeGraphs(surfaceGraph, undergroundGraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<RiverCourse> normalizedTrunkCourses = planner.tributaries.normalizeSharedTrunks(courses, diagnostics);
|
||||||
|
List<RiverCourse> normalizedOutletCourses = planner.tributaries.normalizeOutletContinuations(normalizedTrunkCourses);
|
||||||
|
courses.clear();
|
||||||
|
courses.addAll(normalizedOutletCourses);
|
||||||
|
if (includeDeepFluids) {
|
||||||
|
planner.featureSites.compileDeepFluidCourses(key, courses, diagnostics);
|
||||||
|
}
|
||||||
|
planner.featureSites.compileSurfacePools(key, courses, diagnostics);
|
||||||
|
if (includeDeepFluids && grid != null) {
|
||||||
|
planner.featureSites.compileSeaCaves(grid, courses, diagnostics);
|
||||||
|
}
|
||||||
|
DraftProfile profile = planner.currentDraftProfile();
|
||||||
|
long rasterStarted = System.nanoTime();
|
||||||
|
HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(courses);
|
||||||
|
profile.rasterNanos += System.nanoTime() - rasterStarted;
|
||||||
|
profile.rasterCalls++;
|
||||||
|
HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface(
|
||||||
|
validation.plannedSurface()
|
||||||
|
);
|
||||||
|
CaveVoxelView caveView = Objects.requireNonNull(
|
||||||
|
planner.caveViewFactory.create(plannedSurface),
|
||||||
|
"Hydrology cave view factory returned null"
|
||||||
|
);
|
||||||
|
HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get();
|
||||||
|
if (samples != null && caveView instanceof HydrologyTerrainCaveVoxelView terrainCaveView) {
|
||||||
|
caveView = terrainCaveView.withSampleCache(samples.detailed);
|
||||||
|
}
|
||||||
|
Map<HydrologyCaveCourseFilter.CandidateKey, HydrologyCaveCandidate> candidateCache =
|
||||||
|
samples == null ? null : samples.caveCandidates;
|
||||||
|
HydrologyCaveContainmentPlanner.ValidationCache validationCache =
|
||||||
|
samples == null ? null : samples.caveValidations;
|
||||||
|
long filterStarted = System.nanoTime();
|
||||||
|
HydrologyCaveCourseFilter.Result containment = new HydrologyCaveCourseFilter(
|
||||||
|
caveView,
|
||||||
|
new HydrologyCaveCourseFilter.Options(
|
||||||
|
planner.settings.underground().connectToExistingCaves(),
|
||||||
|
planner.settings.outlets().coastalGrotto().maximumVolume(),
|
||||||
|
planner.settings.outlets().inlandGrotto().maximumVolume()
|
||||||
|
),
|
||||||
|
candidateCache,
|
||||||
|
validationCache,
|
||||||
|
plannedSurface
|
||||||
|
).filter(
|
||||||
|
graph.nodes(),
|
||||||
|
graph.edges(),
|
||||||
|
graph.outlets(),
|
||||||
|
courses,
|
||||||
|
validation,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
profile.filterNanos += System.nanoTime() - filterStarted;
|
||||||
|
profile.filterCalls++;
|
||||||
|
return new PublicationAttempt(containment, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceCompilation compileSourceSelection(
|
||||||
|
boolean surface,
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
List<Integer> sources,
|
||||||
|
Map<Long, List<HydrologyPoint>> refinedEdges,
|
||||||
|
Map<SourceCompilationKey, SourceCompilation> sourceCompilations
|
||||||
|
) {
|
||||||
|
SourceCompilationKey compilationKey = new SourceCompilationKey(surface, routing, sources);
|
||||||
|
SourceCompilation cached = sourceCompilations.get(compilationKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
int[] contributions = planner.sourcePlanner.countContributions(sources, routing);
|
||||||
|
CompiledGraph graph = planner.sourcePlanner.compileGraph(
|
||||||
|
key,
|
||||||
|
grid,
|
||||||
|
routing,
|
||||||
|
surface ? contributions : new int[contributions.length],
|
||||||
|
surface ? new int[contributions.length] : contributions,
|
||||||
|
refinedEdges
|
||||||
|
);
|
||||||
|
ArrayList<RiverCourse> courses = new ArrayList<>();
|
||||||
|
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||||
|
if (surface && planner.settings.surface().enabled()) {
|
||||||
|
planner.surfaceCourses.compileSurfaceCourses(key, grid, routing, graph, sources, courses, diagnostics);
|
||||||
|
} else if (!surface && planner.settings.underground().enabled()) {
|
||||||
|
planner.undergroundCourses.compileUndergroundCourses(key, grid, routing, graph, sources, courses, diagnostics);
|
||||||
|
}
|
||||||
|
SourceCompilation compiled = new SourceCompilation(
|
||||||
|
graph,
|
||||||
|
List.copyOf(courses),
|
||||||
|
List.copyOf(diagnostics)
|
||||||
|
);
|
||||||
|
sourceCompilations.put(compilationKey, compiled);
|
||||||
|
return compiled;
|
||||||
|
}
|
||||||
|
|
||||||
|
record SourceCompilationKey(
|
||||||
|
boolean surface,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
List<Integer> sources
|
||||||
|
) {
|
||||||
|
SourceCompilationKey {
|
||||||
|
Objects.requireNonNull(routing, "routing");
|
||||||
|
sources = List.copyOf(sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record SourceCompilation(
|
||||||
|
CompiledGraph graph,
|
||||||
|
List<RiverCourse> courses,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record PublicationAttempt(
|
||||||
|
HydrologyCaveCourseFilter.Result result,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MaterializedHydrology(
|
||||||
|
HydrologyCaveCourseFilter.Result result,
|
||||||
|
RiverFootprint footprint
|
||||||
|
) {
|
||||||
|
MaterializedHydrology {
|
||||||
|
Objects.requireNonNull(result);
|
||||||
|
Objects.requireNonNull(footprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyDiagnosticCandidate surfaceConflictDiagnostic(
|
||||||
|
RiverCourse course,
|
||||||
|
long winnerCourseId
|
||||||
|
) {
|
||||||
|
HydraulicSegment representative = course.segments().getFirst();
|
||||||
|
return new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(course.id(), HydrologySourcePlanner.DIAGNOSTIC_SALT, winnerCourseId),
|
||||||
|
HydrologyCandidateKind.SOURCE,
|
||||||
|
HydrologyFeatureType.SURFACE_POOL,
|
||||||
|
representative.start(),
|
||||||
|
HydrologyCandidateRejection.SOURCE_SPACING, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,767 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.ChannelProfile;
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.ValleyProfile;
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.ValleyProfileSolver;
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline;
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
|
final class HydrologyFeatureSitePlanner {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologyFeatureSitePlanner(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final long DEEP_FLUID_SALT = 0x44454550464cL;
|
||||||
|
static final long SURFACE_POOL_SALT = 0x504f4f4cL;
|
||||||
|
static final long SEA_CAVE_SALT = 0x534541434156L;
|
||||||
|
static final long DEEP_FLUID_X_OFFSET_SALT = 0x44454550584fL;
|
||||||
|
static final long DEEP_FLUID_Z_OFFSET_SALT = 0x444545505a4fL;
|
||||||
|
static final long DEEP_CHANNEL_HEADING_SALT = 0x44454550484447L;
|
||||||
|
static final long DEEP_CHANNEL_FIRST_BEND_SALT = 0x444545504231L;
|
||||||
|
static final long DEEP_CHANNEL_SECOND_BEND_SALT = 0x444545504232L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standing surface pools: a jittered lattice of candidate sites per pool profile, admitted where the
|
||||||
|
* policy lists the pool, the ground is open land clear of every accepted course, and the bowl fits
|
||||||
|
* the incision cap. Each accepted pool is an independent course with one STANDING_POOL segment.
|
||||||
|
*/
|
||||||
|
void compileSurfacePools(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
List<RiverCourse> courses,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
int tileSize = planner.settings.routing().tileSize();
|
||||||
|
for (HydrologyPlannerSettings.SurfacePool pool : planner.settings.surfacePools()) {
|
||||||
|
if (!pool.enabled() || pool.maximumPerTile() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long profileSeed = HydrologyHash.mix(planner.worldSeed, SURFACE_POOL_SALT, HydrologyHash.text(pool.id()));
|
||||||
|
int target = Math.min(pool.maximumPerTile(), planner.sourcePlanner.expectedCount(pool.density(), HydrologyHash.mix(
|
||||||
|
profileSeed, key.tileX(), key.tileZ())));
|
||||||
|
int accepted = 0;
|
||||||
|
for (DeepSite site : poolSites(key, pool, profileSeed, tileSize)) {
|
||||||
|
if (accepted >= target) {
|
||||||
|
addPoolDiagnostic(site, HydrologyCandidateRejection.SOURCE_QUOTA, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyCandidateRejection rejection = poolAdmission(pool, site, courses);
|
||||||
|
if (rejection != null) {
|
||||||
|
addPoolDiagnostic(site, rejection, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RiverCourse course = buildSurfacePoolCourse(pool, site);
|
||||||
|
if (course == null) {
|
||||||
|
addPoolDiagnostic(site, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
courses.add(course);
|
||||||
|
accepted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<DeepSite> poolSites(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologyPlannerSettings.SurfacePool pool,
|
||||||
|
long profileSeed,
|
||||||
|
int tileSize
|
||||||
|
) {
|
||||||
|
int minimumX = key.minimumBlockX(tileSize);
|
||||||
|
int minimumZ = key.minimumBlockZ(tileSize);
|
||||||
|
int spacing = pool.spacing();
|
||||||
|
int xOffset = HydrologyHash.between(HydrologyHash.mix(profileSeed, DEEP_FLUID_X_OFFSET_SALT), 0, spacing - 1);
|
||||||
|
int zOffset = HydrologyHash.between(HydrologyHash.mix(profileSeed, DEEP_FLUID_Z_OFFSET_SALT), 0, spacing - 1);
|
||||||
|
long firstCellX = ceilDiv((long) minimumX - xOffset, spacing);
|
||||||
|
long firstCellZ = ceilDiv((long) minimumZ - zOffset, spacing);
|
||||||
|
long lastCellX = Math.floorDiv((long) minimumX + tileSize - 1L - xOffset, spacing);
|
||||||
|
long lastCellZ = Math.floorDiv((long) minimumZ + tileSize - 1L - zOffset, spacing);
|
||||||
|
ArrayList<DeepSite> sites = new ArrayList<>();
|
||||||
|
for (long cellZ = firstCellZ; cellZ <= lastCellZ; cellZ++) {
|
||||||
|
for (long cellX = firstCellX; cellX <= lastCellX; cellX++) {
|
||||||
|
long stable = HydrologyHash.mix(profileSeed, cellX, cellZ);
|
||||||
|
// Jitter inside the cell keeps pools off a visible grid.
|
||||||
|
int jitter = Math.max(1, spacing / 3);
|
||||||
|
int x = Math.toIntExact(xOffset + cellX * spacing + HydrologyHash.between(HydrologyHash.mix(stable, 11), -jitter, jitter));
|
||||||
|
int z = Math.toIntExact(zOffset + cellZ * spacing + HydrologyHash.between(HydrologyHash.mix(stable, 12), -jitter, jitter));
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(x, z);
|
||||||
|
sites.add(new DeepSite(x, z, terrain == null ? 0 : terrain.naturalHeight(), stable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sites.sort(Comparator.comparingLong(DeepSite::stableId));
|
||||||
|
return sites;
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyCandidateRejection poolAdmission(
|
||||||
|
HydrologyPlannerSettings.SurfacePool pool,
|
||||||
|
DeepSite site,
|
||||||
|
List<RiverCourse> courses
|
||||||
|
) {
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(site.x(), site.z());
|
||||||
|
if (terrain == null || terrain.ocean() || terrain.naturalHeight() <= planner.settings.seaLevel() + 1) {
|
||||||
|
return HydrologyCandidateRejection.SURFACE_EXPOSURE;
|
||||||
|
}
|
||||||
|
if (!terrain.transitAllowed() || !terrain.surfacePoolKeys().contains(pool.id())) {
|
||||||
|
return HydrologyCandidateRejection.POLICY_EXCLUDED;
|
||||||
|
}
|
||||||
|
long clearance = pool.maximumRadius() + (long) StrictMath.ceil(planner.settings.surface().shoreWidth())
|
||||||
|
+ planner.settings.surface().banks().maximumBlendWidth() + planner.settings.surface().maximumWidth();
|
||||||
|
long clearanceSquared = clearance * clearance;
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
for (HydraulicSegment segment : course.segments()) {
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
long deltaX = (long) point.x() - site.x();
|
||||||
|
long deltaZ = (long) point.z() - site.z();
|
||||||
|
if (deltaX * deltaX + deltaZ * deltaZ < clearanceSquared) {
|
||||||
|
return HydrologyCandidateRejection.SOURCE_SPACING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse buildSurfacePoolCourse(HydrologyPlannerSettings.SurfacePool pool, DeepSite site) {
|
||||||
|
int radius = HydrologyHash.between(HydrologyHash.mix(site.stableId(), 3), pool.minimumRadius(), pool.maximumRadius());
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(site.x(), site.z());
|
||||||
|
List<HydrologyPoint> points = List.of(
|
||||||
|
new HydrologyPoint(site.x() - 1, site.head(), site.z()),
|
||||||
|
new HydrologyPoint(site.x(), site.head(), site.z()),
|
||||||
|
new HydrologyPoint(site.x() + 1, site.head(), site.z())
|
||||||
|
);
|
||||||
|
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
|
||||||
|
int count = centerline.size();
|
||||||
|
double[] halfWidth = new double[count];
|
||||||
|
double[] depth = new double[count];
|
||||||
|
double[] bank = new double[count];
|
||||||
|
Arrays.fill(halfWidth, radius);
|
||||||
|
Arrays.fill(depth, pool.depth());
|
||||||
|
Arrays.fill(bank, terrain == null ? 1D : terrain.bankMultiplier());
|
||||||
|
ChannelProfile profile = new ChannelProfile(halfWidth, depth, bank);
|
||||||
|
ValleyProfile valley = new ValleyProfileSolver(planner.settings.surface(), planner::sampleBasis, planner.settings.seaLevel(), 0)
|
||||||
|
.solve(centerline, profile, SurfaceTerminal.SINKHOLE, Integer.MIN_VALUE);
|
||||||
|
if (!valley.accepted()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int center = Math.min(1, valley.exposedStations() - 1);
|
||||||
|
int head = valley.head()[center];
|
||||||
|
// A pool belongs in a hollow or on level ground: the whole rim must sit within a few blocks of the
|
||||||
|
// water, or the bowl becomes a scar dug into a slope.
|
||||||
|
int rimAllowance = pool.depth() + planner.settings.surface().banks().sink() + 2;
|
||||||
|
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||||
|
if (valley.crossMax()[station] - valley.head()[station] > rimAllowance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long courseId = HydrologyHash.mix(planner.worldSeed, SURFACE_POOL_SALT, HydrologyHash.text(pool.id()), site.stableId());
|
||||||
|
long segmentId = HydrologyHash.mix(courseId, HydrologySegmentBuilder.SEGMENT_SALT, HydrologyFeatureType.STANDING_POOL.ordinal());
|
||||||
|
ArrayList<HydrologyPoint> centerlinePoints = new ArrayList<>(points.size());
|
||||||
|
for (HydrologyPoint point : points) {
|
||||||
|
centerlinePoints.add(new HydrologyPoint(point.x(), head, point.z()));
|
||||||
|
}
|
||||||
|
HydraulicSegment segment = new HydraulicSegment(
|
||||||
|
segmentId,
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.STANDING_POOL,
|
||||||
|
head,
|
||||||
|
head,
|
||||||
|
radius * 2,
|
||||||
|
pool.depth(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
centerlinePoints
|
||||||
|
);
|
||||||
|
return new RiverCourse(
|
||||||
|
courseId,
|
||||||
|
RiverCourseType.SURFACE_POOL,
|
||||||
|
OptionalLong.empty(),
|
||||||
|
OptionalLong.empty(),
|
||||||
|
pool.id(),
|
||||||
|
1,
|
||||||
|
List.of(),
|
||||||
|
List.of(segment)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPoolDiagnostic(
|
||||||
|
DeepSite site,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(site.stableId(), HydrologySourcePlanner.DIAGNOSTIC_SALT, rejection.ordinal()),
|
||||||
|
HydrologyCandidateKind.POOL,
|
||||||
|
HydrologyFeatureType.STANDING_POOL,
|
||||||
|
new HydrologyPoint(site.x(), site.head(), site.z()),
|
||||||
|
rejection,
|
||||||
|
0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sea caves: coastal grottos the sea opens into without a river. Every owned land node with a proven
|
||||||
|
* ocean boundary is a site, ranked by how high the coast stands over the sea; each accepted site is an
|
||||||
|
* independent course whose chamber is swept inland from the shoreline and opens only through its ocean
|
||||||
|
* face. Rivers keep their outlets: sea caves stay clear of every mouth and grotto already planned.
|
||||||
|
*/
|
||||||
|
void compileSeaCaves(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<RiverCourse> courses,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
HydrologyPlannerSettings.SeaCaves seaCaves = planner.settings.seaCaves();
|
||||||
|
if (!seaCaves.enabled() || seaCaves.maximumPerTile() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HydrologyPlannerSettings.Grotto grotto = planner.settings.outlets().coastalGrotto();
|
||||||
|
boolean fits = sweptGrottoVolume(grotto, seaCaves.depth()) <= grotto.maximumVolume();
|
||||||
|
ArrayList<SeaCaveSite> accepted = new ArrayList<>();
|
||||||
|
for (SeaCaveSite site : seaCaveSites(grid)) {
|
||||||
|
if (accepted.size() >= seaCaves.maximumPerTile()) {
|
||||||
|
addSeaCaveDiagnostic(site, HydrologyCandidateRejection.SOURCE_QUOTA, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!fits) {
|
||||||
|
addSeaCaveDiagnostic(site, HydrologyCandidateRejection.VOLUME_LIMIT, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyCandidateRejection rejection = seaCaveAdmission(site, accepted, courses);
|
||||||
|
if (rejection != null) {
|
||||||
|
addSeaCaveDiagnostic(site, rejection, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
courses.add(buildSeaCaveCourse(site));
|
||||||
|
accepted.add(site);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<SeaCaveSite> seaCaveSites(HydrologySampledGrid grid) {
|
||||||
|
int depth = planner.settings.seaCaves().depth();
|
||||||
|
ArrayList<SeaCaveSite> sites = new ArrayList<>();
|
||||||
|
for (HydrologyGridNode land : grid.nodes()) {
|
||||||
|
HydrologyTerrainSample terrain = land.terrain();
|
||||||
|
if (!grid.owns(land.x(), land.z())
|
||||||
|
|| terrain.ocean()
|
||||||
|
|| !terrain.transitAllowed()
|
||||||
|
|| !terrain.outletAllowed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyGridNode ocean = planner.outletPlanner.firstOceanNeighbor(grid, land);
|
||||||
|
if (ocean == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyOceanBoundaryRefiner.Result boundary = planner.outletPlanner.refineOceanBoundary(land, ocean);
|
||||||
|
if (boundary == null
|
||||||
|
|| !boundary.landwardTerrain().transitAllowed()
|
||||||
|
|| !boundary.landwardTerrain().outletAllowed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long stableId = HydrologyHash.mix(planner.worldSeed, SEA_CAVE_SALT, land.id(), ocean.id());
|
||||||
|
HydrologyPoint inner = seaCaveInnerPoint(land, ocean, boundary.landwardPoint(), stableId, depth);
|
||||||
|
int seaLevel = planner.settingsSeaLevel(ocean.terrain());
|
||||||
|
int coastHeight = seaCaveCoastHeight(boundary, inner, seaLevel);
|
||||||
|
sites.add(new SeaCaveSite(land, ocean, boundary, inner, coastHeight, stableId));
|
||||||
|
}
|
||||||
|
sites.sort(Comparator.comparingInt(SeaCaveSite::coastHeight).reversed()
|
||||||
|
.thenComparingLong(SeaCaveSite::stableId));
|
||||||
|
return sites;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How high the coast stands over the sea across the chamber: the lowest land at the shoreline, at the
|
||||||
|
* back of the chamber and at both flanks of each. Flanks in the sea are ignored (the chamber opens
|
||||||
|
* there like its face); a back in the sea leaves no coast at all.
|
||||||
|
*/
|
||||||
|
int seaCaveCoastHeight(HydrologyOceanBoundaryRefiner.Result boundary, HydrologyPoint inner, int seaLevel) {
|
||||||
|
HydrologyTerrainSample innerTerrain = planner.sampleLandBasis(inner.x(), inner.z());
|
||||||
|
if (innerTerrain == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
HydrologyPoint landward = boundary.landwardPoint();
|
||||||
|
int lowest = Math.min(boundary.landwardTerrain().naturalHeight(), innerTerrain.naturalHeight());
|
||||||
|
int radius = planner.settings.outlets().coastalGrotto().horizontalRadius();
|
||||||
|
// Flanks lie along the shore, across the crossing from the landward point to the sea.
|
||||||
|
int spanX = boundary.oceanPoint().x() - landward.x();
|
||||||
|
int spanZ = boundary.oceanPoint().z() - landward.z();
|
||||||
|
double length = StrictMath.hypot(spanX, spanZ);
|
||||||
|
int flankX = (int) StrictMath.round(-spanZ / length * radius);
|
||||||
|
int flankZ = (int) StrictMath.round(spanX / length * radius);
|
||||||
|
int[][] flanks = {
|
||||||
|
{landward.x() + flankX, landward.z() + flankZ},
|
||||||
|
{landward.x() - flankX, landward.z() - flankZ},
|
||||||
|
{inner.x() + flankX, inner.z() + flankZ},
|
||||||
|
{inner.x() - flankX, inner.z() - flankZ}
|
||||||
|
};
|
||||||
|
for (int[] flank : flanks) {
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(flank[0], flank[1]);
|
||||||
|
if (terrain != null) {
|
||||||
|
lowest = Math.min(lowest, terrain.naturalHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lowest - seaLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The chamber runs inland along the coast normal, turned by a stable jitter so caves do not all
|
||||||
|
// face the same way along a straight shore.
|
||||||
|
HydrologyPoint seaCaveInnerPoint(
|
||||||
|
HydrologyGridNode land,
|
||||||
|
HydrologyGridNode ocean,
|
||||||
|
HydrologyPoint landward,
|
||||||
|
long stableId,
|
||||||
|
int depth
|
||||||
|
) {
|
||||||
|
double normalX = land.x() - ocean.x();
|
||||||
|
double normalZ = land.z() - ocean.z();
|
||||||
|
double length = StrictMath.hypot(normalX, normalZ);
|
||||||
|
double jitter = StrictMath.toRadians(
|
||||||
|
(HydrologyHash.unit(HydrologyHash.mix(stableId, 7)) * 2D - 1D)
|
||||||
|
* planner.settings.seaCaves().sweepJitterDegrees()
|
||||||
|
);
|
||||||
|
double cos = StrictMath.cos(jitter);
|
||||||
|
double sin = StrictMath.sin(jitter);
|
||||||
|
double unitX = normalX / length;
|
||||||
|
double unitZ = normalZ / length;
|
||||||
|
double inlandX = unitX * cos - unitZ * sin;
|
||||||
|
double inlandZ = unitX * sin + unitZ * cos;
|
||||||
|
return new HydrologyPoint(
|
||||||
|
(int) StrictMath.round(landward.x() + inlandX * depth),
|
||||||
|
landward.y(),
|
||||||
|
(int) StrictMath.round(landward.z() + inlandZ * depth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyCandidateRejection seaCaveAdmission(
|
||||||
|
SeaCaveSite site,
|
||||||
|
List<SeaCaveSite> accepted,
|
||||||
|
List<RiverCourse> courses
|
||||||
|
) {
|
||||||
|
HydrologyPlannerSettings.SeaCaves seaCaves = planner.settings.seaCaves();
|
||||||
|
if (site.coastHeight() < seaCaves.minimumCoastHeight()) {
|
||||||
|
return HydrologyCandidateRejection.SURFACE_HEAD_RANGE;
|
||||||
|
}
|
||||||
|
HydrologyPoint landward = site.boundary().landwardPoint();
|
||||||
|
long spacingSquared = (long) seaCaves.minimumSpacing() * seaCaves.minimumSpacing();
|
||||||
|
for (SeaCaveSite other : accepted) {
|
||||||
|
if (landward.distanceSquared2D(other.boundary().landwardPoint()) < spacingSquared) {
|
||||||
|
return HydrologyCandidateRejection.SOURCE_SPACING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long clearance = 2L * planner.settings.outlets().coastalGrotto().horizontalRadius() + seaCaves.depth();
|
||||||
|
long clearanceSquared = clearance * clearance;
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
if (course.type() == RiverCourseType.SEA_CAVE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (HydraulicSegment segment : course.segments()) {
|
||||||
|
if (segment.type() != HydrologyFeatureType.MOUTH
|
||||||
|
&& segment.type() != HydrologyFeatureType.COASTAL_GROTTO
|
||||||
|
&& segment.type() != HydrologyFeatureType.INLAND_GROTTO) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
if (landward.distanceSquared2D(point) < clearanceSquared) {
|
||||||
|
return HydrologyCandidateRejection.SOURCE_SPACING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse buildSeaCaveCourse(SeaCaveSite site) {
|
||||||
|
HydrologyPlannerSettings.Grotto grotto = planner.settings.outlets().coastalGrotto();
|
||||||
|
long courseId = HydrologyHash.mix(planner.worldSeed, HydrologySurfaceCoursePlanner.COURSE_SALT, SEA_CAVE_SALT, site.stableId());
|
||||||
|
String profileKey = planner.segments.chooseProfile(site.boundary().landwardTerrain(), courseId);
|
||||||
|
int seaLevel = planner.settingsSeaLevel(site.ocean().terrain());
|
||||||
|
// The last point is the ocean connection: the footprint opens the chamber there and nowhere else.
|
||||||
|
List<HydrologyPoint> centerline = planner.segments.line(
|
||||||
|
HydrologyPlanner.withY(site.inner(), seaLevel),
|
||||||
|
HydrologyPlanner.withY(site.boundary().oceanPoint(), seaLevel),
|
||||||
|
planner.settings.routing().refinementSpacing()
|
||||||
|
);
|
||||||
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(1);
|
||||||
|
planner.segments.addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.COASTAL_GROTTO,
|
||||||
|
seaLevel,
|
||||||
|
grotto.horizontalRadius() * 2,
|
||||||
|
grotto.verticalRadius(),
|
||||||
|
centerline,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
return new RiverCourse(
|
||||||
|
courseId,
|
||||||
|
RiverCourseType.SEA_CAVE,
|
||||||
|
OptionalLong.empty(),
|
||||||
|
OptionalLong.empty(),
|
||||||
|
profileKey,
|
||||||
|
1,
|
||||||
|
List.of(),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSeaCaveDiagnostic(
|
||||||
|
SeaCaveSite site,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(site.stableId(), HydrologySourcePlanner.DIAGNOSTIC_SALT, rejection.ordinal()),
|
||||||
|
HydrologyCandidateKind.OUTLET,
|
||||||
|
HydrologyFeatureType.COASTAL_GROTTO,
|
||||||
|
site.boundary().landwardPoint(),
|
||||||
|
rejection,
|
||||||
|
0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raster a swept grotto occupies: one ellipsoid plus its central cross-section extruded over the
|
||||||
|
* sweep. The containment filter measures the real carve; this only refuses chambers that cannot fit.
|
||||||
|
*/
|
||||||
|
static long sweptGrottoVolume(HydrologyPlannerSettings.Grotto grotto, int depth) {
|
||||||
|
int radius = grotto.horizontalRadius();
|
||||||
|
long section = 0L;
|
||||||
|
for (int deltaX = -radius; deltaX <= radius; deltaX++) {
|
||||||
|
double normalized = Math.min(1D, Math.abs(deltaX) / Math.max(1D, radius));
|
||||||
|
double scale = StrictMath.sqrt(Math.max(0D, 1D - normalized * normalized));
|
||||||
|
section += (long) StrictMath.ceil(grotto.verticalRadius() * scale)
|
||||||
|
+ (long) StrictMath.floor(grotto.headroom() * scale)
|
||||||
|
+ 1L;
|
||||||
|
}
|
||||||
|
return HydrologyPlannerSettings.ellipsoidVolume(radius, grotto.verticalRadius(), grotto.headroom())
|
||||||
|
+ section * depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
record SeaCaveSite(
|
||||||
|
HydrologyGridNode land,
|
||||||
|
HydrologyGridNode ocean,
|
||||||
|
HydrologyOceanBoundaryRefiner.Result boundary,
|
||||||
|
HydrologyPoint inner,
|
||||||
|
int coastHeight,
|
||||||
|
long stableId
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void compileDeepFluidCourses(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
List<RiverCourse> courses,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
for (HydrologyPlannerSettings.DeepFluid deepFluid : planner.settings.deepFluids()) {
|
||||||
|
if (!deepFluid.enabled() || deepFluid.maximumPerTile() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ArrayList<DeepSite> sites = deepSites(key, deepFluid, diagnostics);
|
||||||
|
int target = Math.min(
|
||||||
|
deepFluid.maximumPerTile(),
|
||||||
|
planner.sourcePlanner.expectedCount(deepFluid.density(), HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
DEEP_FLUID_SALT,
|
||||||
|
HydrologyHash.text(deepFluid.id()),
|
||||||
|
key.tileX(),
|
||||||
|
key.tileZ()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
for (int index = 0; index < sites.size(); index++) {
|
||||||
|
DeepSite site = sites.get(index);
|
||||||
|
if (index >= target) {
|
||||||
|
addDeepDiagnostic(
|
||||||
|
deepFluid,
|
||||||
|
site,
|
||||||
|
HydrologyCandidateRejection.SOURCE_QUOTA,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RiverCourse course = buildDeepFluidCourse(deepFluid, site, index);
|
||||||
|
if (course != null) {
|
||||||
|
courses.add(course);
|
||||||
|
} else {
|
||||||
|
addDeepDiagnostic(
|
||||||
|
deepFluid,
|
||||||
|
site,
|
||||||
|
HydrologyCandidateRejection.VOLUME_LIMIT,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<DeepSite> deepSites(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologyPlannerSettings.DeepFluid deepFluid,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
int tileSize = planner.settings.routing().tileSize();
|
||||||
|
int minimumX = key.minimumBlockX(tileSize);
|
||||||
|
int minimumZ = key.minimumBlockZ(tileSize);
|
||||||
|
int spacing = deepFluid.spacing();
|
||||||
|
long profileSeed = HydrologyHash.mix(planner.worldSeed, DEEP_FLUID_SALT, HydrologyHash.text(deepFluid.id()));
|
||||||
|
int xOffset = HydrologyHash.between(
|
||||||
|
HydrologyHash.mix(profileSeed, DEEP_FLUID_X_OFFSET_SALT),
|
||||||
|
0,
|
||||||
|
spacing - 1
|
||||||
|
);
|
||||||
|
int zOffset = HydrologyHash.between(
|
||||||
|
HydrologyHash.mix(profileSeed, DEEP_FLUID_Z_OFFSET_SALT),
|
||||||
|
0,
|
||||||
|
spacing - 1
|
||||||
|
);
|
||||||
|
long firstCellX = ceilDiv((long) minimumX - xOffset, spacing);
|
||||||
|
long firstCellZ = ceilDiv((long) minimumZ - zOffset, spacing);
|
||||||
|
long lastCellX = Math.floorDiv((long) minimumX + tileSize - 1L - xOffset, spacing);
|
||||||
|
long lastCellZ = Math.floorDiv((long) minimumZ + tileSize - 1L - zOffset, spacing);
|
||||||
|
ArrayList<DeepSite> sites = new ArrayList<>();
|
||||||
|
for (long cellZ = firstCellZ; cellZ <= lastCellZ; cellZ++) {
|
||||||
|
for (long cellX = firstCellX; cellX <= lastCellX; cellX++) {
|
||||||
|
long stable = HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
DEEP_FLUID_SALT,
|
||||||
|
HydrologyHash.text(deepFluid.id()),
|
||||||
|
cellX,
|
||||||
|
cellZ
|
||||||
|
);
|
||||||
|
int x = Math.toIntExact(xOffset + cellX * spacing);
|
||||||
|
int z = Math.toIntExact(zOffset + cellZ * spacing);
|
||||||
|
int head = planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.DEEP_FLUID_HEIGHT,
|
||||||
|
deepFluid.id(),
|
||||||
|
x,
|
||||||
|
z,
|
||||||
|
stable,
|
||||||
|
deepFluid.minimumY(),
|
||||||
|
deepFluid.maximumY()
|
||||||
|
);
|
||||||
|
DeepSite site = new DeepSite(x, z, head, stable);
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(x, z);
|
||||||
|
int verticalRadius = HydrologyHash.between(
|
||||||
|
HydrologyHash.mix(stable, 3),
|
||||||
|
deepFluid.minimumVerticalRadius(),
|
||||||
|
deepFluid.maximumVerticalRadius()
|
||||||
|
);
|
||||||
|
if (!deepSiteFits(terrain, deepFluid, head, verticalRadius, planner.minimumY)) {
|
||||||
|
if (terrain != null && !terrain.ocean()) {
|
||||||
|
addDeepDiagnostic(
|
||||||
|
deepFluid,
|
||||||
|
site,
|
||||||
|
HydrologyCandidateRejection.CAVE_CONTAINMENT,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sites.add(site);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sites.sort(Comparator.comparingLong(DeepSite::stableId));
|
||||||
|
return sites;
|
||||||
|
}
|
||||||
|
|
||||||
|
long ceilDiv(long value, int divisor) {
|
||||||
|
long quotient = Math.floorDiv(value, divisor);
|
||||||
|
return quotient * divisor == value ? quotient : quotient + 1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean deepSiteFits(
|
||||||
|
HydrologyTerrainSample terrain,
|
||||||
|
HydrologyPlannerSettings.DeepFluid deepFluid,
|
||||||
|
int head,
|
||||||
|
int verticalRadius,
|
||||||
|
int minimumY
|
||||||
|
) {
|
||||||
|
if (terrain == null || terrain.ocean()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int lowerEnvelope = deepFluid.containedPools()
|
||||||
|
? Math.max(deepFluid.channelDepth(), verticalRadius)
|
||||||
|
: deepFluid.channelDepth();
|
||||||
|
return (long) head - lowerEnvelope > minimumY
|
||||||
|
&& head + deepFluid.headroom() < terrain.naturalHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addDeepDiagnostic(
|
||||||
|
HydrologyPlannerSettings.DeepFluid deepFluid,
|
||||||
|
DeepSite site,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(site.stableId(), HydrologySourcePlanner.DIAGNOSTIC_SALT, rejection.ordinal()),
|
||||||
|
HydrologyCandidateKind.DEEP_FLUID,
|
||||||
|
deepFluid.containedPools() ? HydrologyFeatureType.DEEP_POOL : HydrologyFeatureType.DEEP_CHANNEL,
|
||||||
|
new HydrologyPoint(site.x(), site.head(), site.z()),
|
||||||
|
rejection, 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse buildDeepFluidCourse(
|
||||||
|
HydrologyPlannerSettings.DeepFluid deepFluid,
|
||||||
|
DeepSite site,
|
||||||
|
int siteIndex
|
||||||
|
) {
|
||||||
|
long courseId = HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
HydrologySurfaceCoursePlanner.COURSE_SALT,
|
||||||
|
DEEP_FLUID_SALT,
|
||||||
|
HydrologyHash.text(deepFluid.id()),
|
||||||
|
site.stableId(),
|
||||||
|
siteIndex
|
||||||
|
);
|
||||||
|
int horizontalRadius = HydrologyHash.between(
|
||||||
|
HydrologyHash.mix(site.stableId(), 2),
|
||||||
|
deepFluid.minimumHorizontalRadius(),
|
||||||
|
deepFluid.maximumHorizontalRadius()
|
||||||
|
);
|
||||||
|
int verticalRadius = HydrologyHash.between(
|
||||||
|
HydrologyHash.mix(site.stableId(), 3),
|
||||||
|
deepFluid.minimumVerticalRadius(),
|
||||||
|
deepFluid.maximumVerticalRadius()
|
||||||
|
);
|
||||||
|
ArrayList<HydraulicSegment> segments = new ArrayList<>();
|
||||||
|
HydrologyPoint center = new HydrologyPoint(site.x(), site.head(), site.z());
|
||||||
|
long usedVolume = 0L;
|
||||||
|
if (deepFluid.containedPools()) {
|
||||||
|
long poolVolume = HydrologyPlannerSettings.ellipsoidVolume(
|
||||||
|
horizontalRadius,
|
||||||
|
Math.max(deepFluid.channelDepth(), verticalRadius),
|
||||||
|
deepFluid.headroom()
|
||||||
|
);
|
||||||
|
if (poolVolume > deepFluid.maximumVolume()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
usedVolume = poolVolume;
|
||||||
|
long poolId = HydrologyHash.mix(planner.worldSeed, HydrologySegmentBuilder.SEGMENT_SALT, courseId, HydrologyFeatureType.DEEP_POOL.ordinal());
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
poolId,
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.DEEP_POOL,
|
||||||
|
site.head(),
|
||||||
|
site.head(),
|
||||||
|
horizontalRadius * 2 + 1,
|
||||||
|
Math.max(deepFluid.channelDepth(), verticalRadius),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
List.of(center)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (deepFluid.shortChannels() && deepFluid.maximumChannelLength() > 0) {
|
||||||
|
int volumeBoundedMaximum = deepFluid.maximumChannelLength();
|
||||||
|
while (volumeBoundedMaximum >= deepFluid.minimumChannelLength()
|
||||||
|
&& usedVolume + deepChannelVolumeBound(deepFluid, volumeBoundedMaximum)
|
||||||
|
> deepFluid.maximumVolume()) {
|
||||||
|
volumeBoundedMaximum--;
|
||||||
|
}
|
||||||
|
if (volumeBoundedMaximum >= deepFluid.minimumChannelLength()) {
|
||||||
|
int length = HydrologyHash.between(
|
||||||
|
HydrologyHash.mix(site.stableId(), 4),
|
||||||
|
deepFluid.minimumChannelLength(),
|
||||||
|
volumeBoundedMaximum
|
||||||
|
);
|
||||||
|
long channelId = HydrologyHash.mix(planner.worldSeed, HydrologySegmentBuilder.SEGMENT_SALT, courseId, HydrologyFeatureType.DEEP_CHANNEL.ordinal());
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
channelId,
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.DEEP_CHANNEL,
|
||||||
|
site.head(),
|
||||||
|
site.head(),
|
||||||
|
deepFluid.channelWidth(),
|
||||||
|
deepFluid.channelDepth(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
organicDeepChannelCenterline(
|
||||||
|
center,
|
||||||
|
length,
|
||||||
|
planner.settings.routing().refinementSpacing(),
|
||||||
|
site.stableId()
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (segments.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new RiverCourse(
|
||||||
|
courseId,
|
||||||
|
RiverCourseType.DEEP_FLUID,
|
||||||
|
OptionalLong.empty(),
|
||||||
|
OptionalLong.empty(),
|
||||||
|
deepFluid.id(),
|
||||||
|
1,
|
||||||
|
List.of(),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long deepChannelVolumeBound(HydrologyPlannerSettings.DeepFluid deepFluid, int length) {
|
||||||
|
int radius = Math.max(1, deepFluid.channelWidth() / 2);
|
||||||
|
int points = (int) StrictMath.ceil(
|
||||||
|
length * StrictMath.sqrt(2D) / planner.settings.routing().refinementSpacing()
|
||||||
|
) + 1;
|
||||||
|
long horizontalEnvelope = (long) (radius * 2 + 1) * (radius * 2 + 1);
|
||||||
|
long verticalEnvelope = deepFluid.channelDepth() + deepFluid.headroom() + 1L;
|
||||||
|
return horizontalEnvelope * verticalEnvelope * points;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> organicDeepChannelCenterline(
|
||||||
|
HydrologyPoint start,
|
||||||
|
int length,
|
||||||
|
int spacing,
|
||||||
|
long stableId
|
||||||
|
) {
|
||||||
|
double heading = HydrologyHash.unit(HydrologyHash.mix(stableId, DEEP_CHANNEL_HEADING_SALT))
|
||||||
|
* StrictMath.PI * 2D;
|
||||||
|
double forwardX = StrictMath.cos(heading);
|
||||||
|
double forwardZ = StrictMath.sin(heading);
|
||||||
|
double lateralX = -forwardZ;
|
||||||
|
double lateralZ = forwardX;
|
||||||
|
double bendEnvelope = Math.max(1.5D, Math.min(8D, length * 0.3D));
|
||||||
|
double firstBend = planner.segments.signedOrganicOffset(stableId, DEEP_CHANNEL_FIRST_BEND_SALT, bendEnvelope);
|
||||||
|
double secondBend = planner.segments.signedOrganicOffset(stableId, DEEP_CHANNEL_SECOND_BEND_SALT, bendEnvelope);
|
||||||
|
if (StrictMath.signum(firstBend) == StrictMath.signum(secondBend)) {
|
||||||
|
secondBend = -secondBend;
|
||||||
|
}
|
||||||
|
|
||||||
|
double endX = start.x() + forwardX * length;
|
||||||
|
double endZ = start.z() + forwardZ * length;
|
||||||
|
double firstControlX = start.x() + forwardX * length / 3D + lateralX * firstBend;
|
||||||
|
double firstControlZ = start.z() + forwardZ * length / 3D + lateralZ * firstBend;
|
||||||
|
double secondControlX = start.x() + forwardX * length * 2D / 3D + lateralX * secondBend;
|
||||||
|
double secondControlZ = start.z() + forwardZ * length * 2D / 3D + lateralZ * secondBend;
|
||||||
|
int steps = Math.max(4, (int) StrictMath.ceil(length / (double) Math.max(2, spacing)) * 2);
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(steps + 1);
|
||||||
|
for (int step = 0; step <= steps; step++) {
|
||||||
|
double progress = step / (double) steps;
|
||||||
|
double inverse = 1D - progress;
|
||||||
|
double x = inverse * inverse * inverse * start.x()
|
||||||
|
+ 3D * inverse * inverse * progress * firstControlX
|
||||||
|
+ 3D * inverse * progress * progress * secondControlX
|
||||||
|
+ progress * progress * progress * endX;
|
||||||
|
double z = inverse * inverse * inverse * start.z()
|
||||||
|
+ 3D * inverse * inverse * progress * firstControlZ
|
||||||
|
+ 3D * inverse * progress * progress * secondControlZ
|
||||||
|
+ progress * progress * progress * endZ;
|
||||||
|
HydrologyPoint point = new HydrologyPoint(
|
||||||
|
(int) StrictMath.round(x),
|
||||||
|
start.y(),
|
||||||
|
(int) StrictMath.round(z)
|
||||||
|
);
|
||||||
|
if (points.isEmpty() || point.x() != points.getLast().x() || point.z() != points.getLast().z()) {
|
||||||
|
points.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
record DeepSite(int x, int z, int head, long stableId) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record HydrologyGridNode(
|
||||||
|
int index,
|
||||||
|
int gridX,
|
||||||
|
int gridZ,
|
||||||
|
int x,
|
||||||
|
int z,
|
||||||
|
long id,
|
||||||
|
HydrologyTerrainSample terrain
|
||||||
|
) {
|
||||||
|
HydrologyPoint naturalPoint() {
|
||||||
|
return new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record HydrologyGridOffset(int x, int z) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,669 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
final class HydrologyOutletPlanner {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologyOutletPlanner(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final long NODE_SALT = 0x4e4f4445L;
|
||||||
|
static final long OUTLET_SALT = 0x4f55544c4554L;
|
||||||
|
|
||||||
|
/** Sea outlet types in the order the coastal budget takes them, one of each type per turn. */
|
||||||
|
static final List<HydrologyFeatureType> COASTAL_OUTLET_TYPES = List.of(
|
||||||
|
HydrologyFeatureType.MOUTH,
|
||||||
|
HydrologyFeatureType.COASTAL_GROTTO
|
||||||
|
);
|
||||||
|
|
||||||
|
static final List<HydrologyGridOffset> CARDINAL_OFFSETS = List.of(
|
||||||
|
new HydrologyGridOffset(-1, 0),
|
||||||
|
new HydrologyGridOffset(1, 0),
|
||||||
|
new HydrologyGridOffset(0, -1),
|
||||||
|
new HydrologyGridOffset(0, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
HydrologySampledGrid sample(HydrologyTileKey key) {
|
||||||
|
HydrologyPlannerSettings.Routing routing = planner.settings.routing();
|
||||||
|
int ownerMinimumX = key.minimumBlockX(routing.tileSize());
|
||||||
|
int ownerMinimumZ = key.minimumBlockZ(routing.tileSize());
|
||||||
|
int halo = Math.min(
|
||||||
|
routing.maximumRouteLength(),
|
||||||
|
Math.multiplyExact(routing.sampleSpacing(), 2)
|
||||||
|
);
|
||||||
|
int alignedHalo = Math.floorDiv(halo, routing.sampleSpacing()) * routing.sampleSpacing();
|
||||||
|
int minimumX = ownerMinimumX - alignedHalo;
|
||||||
|
int minimumZ = ownerMinimumZ - alignedHalo;
|
||||||
|
int width = (routing.tileSize() + alignedHalo * 2) / routing.sampleSpacing() + 1;
|
||||||
|
int nodeCount = Math.multiplyExact(width, width);
|
||||||
|
HydrologyRoutingTerrainSampler.GridRequest gridRequest = new HydrologyRoutingTerrainSampler.GridRequest(
|
||||||
|
minimumX,
|
||||||
|
minimumZ,
|
||||||
|
width,
|
||||||
|
routing.sampleSpacing()
|
||||||
|
);
|
||||||
|
HydrologyTerrainSample[] terrainSamples = Objects.requireNonNull(
|
||||||
|
planner.routingSampler.sampleGrid(gridRequest),
|
||||||
|
"Hydrology routing terrain sampler returned null"
|
||||||
|
);
|
||||||
|
if (terrainSamples.length != nodeCount) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Hydrology routing terrain sampler returned " + terrainSamples.length
|
||||||
|
+ " samples for a " + width + " by " + width + " grid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyGridNode> nodes = new ArrayList<>(nodeCount);
|
||||||
|
for (int gridZ = 0; gridZ < width; gridZ++) {
|
||||||
|
int z = minimumZ + gridZ * routing.sampleSpacing();
|
||||||
|
for (int gridX = 0; gridX < width; gridX++) {
|
||||||
|
int x = minimumX + gridX * routing.sampleSpacing();
|
||||||
|
int index = gridZ * width + gridX;
|
||||||
|
HydrologyTerrainSample terrain = Objects.requireNonNull(
|
||||||
|
terrainSamples[index],
|
||||||
|
"Hydrology routing terrain sampler returned null at " + x + "," + z
|
||||||
|
);
|
||||||
|
long id = HydrologyHash.mix(planner.worldSeed, NODE_SALT, x, z);
|
||||||
|
nodes.add(new HydrologyGridNode(index, gridX, gridZ, x, z, id, terrain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new HydrologySampledGrid(
|
||||||
|
minimumX,
|
||||||
|
minimumZ,
|
||||||
|
ownerMinimumX,
|
||||||
|
ownerMinimumZ,
|
||||||
|
routing.tileSize(),
|
||||||
|
width,
|
||||||
|
routing.sampleSpacing(),
|
||||||
|
List.copyOf(nodes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OutletCandidate> resolveOutlets(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
boolean surface,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
ArrayList<OutletCandidate> oceanCandidates = oceanOutletCandidates(grid, surface);
|
||||||
|
if (!surface && planner.settings.outlets().inlandGrotto().enabled()) {
|
||||||
|
addOutletLevelDiagnostics(oceanCandidates, diagnostics);
|
||||||
|
}
|
||||||
|
sortOutletCandidates(key, grid, surface, oceanCandidates);
|
||||||
|
// Sea outlets and inland outlets are budgeted separately: a coast never starves the
|
||||||
|
// sinkholes behind it and a sinkhole never displaces a mouth.
|
||||||
|
List<OutletCandidate> selectedOcean = limitOutletsByType(
|
||||||
|
oceanCandidates,
|
||||||
|
planner.settings.outlets().maximumCoastalPerTile()
|
||||||
|
);
|
||||||
|
addRejectedOutlets(oceanCandidates, selectedOcean, diagnostics);
|
||||||
|
if (!planner.settings.outlets().inlandGrotto().enabled()) {
|
||||||
|
return selectedOcean;
|
||||||
|
}
|
||||||
|
boolean[] oceanReachable = outletReachable(grid, oceanCandidates, surface);
|
||||||
|
ArrayList<OutletCandidate> inlandCandidates = inlandOutletCandidates(grid, oceanReachable);
|
||||||
|
sortInlandOutletCandidates(grid, inlandCandidates);
|
||||||
|
List<OutletCandidate> selectedInland = styledInlandOutlets(
|
||||||
|
grid,
|
||||||
|
limitOutlets(inlandCandidates, planner.settings.outlets().maximumPerTile()),
|
||||||
|
surface
|
||||||
|
);
|
||||||
|
addRejectedOutlets(inlandCandidates, selectedInland, diagnostics);
|
||||||
|
ArrayList<OutletCandidate> selected = new ArrayList<>(selectedOcean.size() + selectedInland.size());
|
||||||
|
selected.addAll(selectedOcean);
|
||||||
|
selected.addAll(selectedInland);
|
||||||
|
return List.copyOf(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<OutletCandidate> oceanOutletCandidates(HydrologySampledGrid grid, boolean surface) {
|
||||||
|
ArrayList<OutletCandidate> oceanCandidates = new ArrayList<>();
|
||||||
|
for (HydrologyGridNode land : grid.nodes()) {
|
||||||
|
if (land.terrain().ocean() || !land.terrain().transitAllowed() || !land.terrain().outletAllowed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyGridNode ocean = firstOceanNeighbor(grid, land);
|
||||||
|
if (ocean == null || !land.terrain().drainsInto(ocean.terrain())) {
|
||||||
|
// A confined shore only reaches a sea that belongs to its own area.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyOceanBoundaryRefiner.Result boundary = refineOceanBoundary(land, ocean);
|
||||||
|
if (boundary == null
|
||||||
|
|| !boundary.landwardTerrain().transitAllowed()
|
||||||
|
|| !boundary.landwardTerrain().outletAllowed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyFeatureType type = coastalOutletType(boundary.landwardTerrain());
|
||||||
|
if (type == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long outletId = HydrologyHash.mix(planner.worldSeed, OUTLET_SALT, land.id(), ocean.id(), type.ordinal());
|
||||||
|
int seaLevel = planner.settingsSeaLevel(ocean.terrain());
|
||||||
|
RiverOutlet outlet = new RiverOutlet(
|
||||||
|
outletId,
|
||||||
|
type,
|
||||||
|
land.id(),
|
||||||
|
HydrologyPlanner.withY(boundary.landwardPoint(), seaLevel),
|
||||||
|
HydrologyPlanner.withY(boundary.oceanPoint(), seaLevel),
|
||||||
|
seaLevel,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
oceanCandidates.add(new OutletCandidate(land.index(), ocean.index(), outlet));
|
||||||
|
}
|
||||||
|
return oceanCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OutletCandidate> resolveSurfaceFallbackOutlets(HydrologySampledGrid grid) {
|
||||||
|
ArrayList<OutletCandidate> oceanCandidates = oceanOutletCandidates(grid, true);
|
||||||
|
ArrayList<OutletCandidate> inlandCandidates = new ArrayList<>();
|
||||||
|
if (planner.settings.outlets().surfaceSinkholesEnabled()
|
||||||
|
&& planner.settings.outlets().inlandGrotto().enabled()) {
|
||||||
|
inlandCandidates.addAll(styledInlandOutlets(
|
||||||
|
grid,
|
||||||
|
inlandOutletCandidates(grid, new boolean[grid.nodes().size()]),
|
||||||
|
true
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (oceanCandidates.isEmpty() && inlandCandidates.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
sortSurfaceFallbackOutletCandidates(grid, oceanCandidates);
|
||||||
|
sortSurfaceFallbackOutletCandidates(grid, inlandCandidates);
|
||||||
|
// Trials alternate sea and inland outlets, sea first: a coast with more mouths than trials
|
||||||
|
// still gives a sinkhole its turn, and a rejected mouth is retried at the next coast.
|
||||||
|
ArrayList<OutletCandidate> candidates = new ArrayList<>(oceanCandidates.size() + inlandCandidates.size());
|
||||||
|
for (int index = 0; index < Math.max(oceanCandidates.size(), inlandCandidates.size()); index++) {
|
||||||
|
if (index < oceanCandidates.size()) {
|
||||||
|
candidates.add(oceanCandidates.get(index));
|
||||||
|
}
|
||||||
|
if (index < inlandCandidates.size()) {
|
||||||
|
candidates.add(inlandCandidates.get(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int maximumTrials = Math.max(
|
||||||
|
HydrologySourcePlanner.OPTIONAL_SOURCE_REJECTIONS_PER_TARGET,
|
||||||
|
Math.multiplyExact(
|
||||||
|
Math.max(planner.settings.outlets().maximumPerTile(), planner.settings.outlets().maximumCoastalPerTile()),
|
||||||
|
16
|
||||||
|
)
|
||||||
|
);
|
||||||
|
maximumTrials = Math.min(16, maximumTrials);
|
||||||
|
return limitOutlets(candidates, maximumTrials);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<OutletCandidate> inlandOutletCandidates(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
boolean[] oceanReachable
|
||||||
|
) {
|
||||||
|
ArrayList<OutletCandidate> inlandCandidates = new ArrayList<>();
|
||||||
|
for (HydrologyGridNode node : grid.nodes()) {
|
||||||
|
if (!grid.owns(node.x(), node.z())
|
||||||
|
|| node.terrain().ocean() || !node.terrain().caveAvailable()
|
||||||
|
|| !node.terrain().transitAllowed() || !node.terrain().outletAllowed()
|
||||||
|
|| oceanReachable[node.index()]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
InlandConnection connection = inlandConnection(node);
|
||||||
|
int poolY = HydrologyPlanner.clamp(
|
||||||
|
connection.terrain().caveFluidY(),
|
||||||
|
planner.settings.underground().minimumFluidY(),
|
||||||
|
planner.settings.underground().maximumFluidY()
|
||||||
|
);
|
||||||
|
if (planner.settings.outlets().surfaceSinkholesEnabled()
|
||||||
|
&& node.terrain().naturalHeight() - planner.settings.surface().banks().sink() <= poolY) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long outletId = HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
OUTLET_SALT,
|
||||||
|
node.id(),
|
||||||
|
connection.point().x(),
|
||||||
|
connection.point().z(),
|
||||||
|
HydrologyFeatureType.INLAND_GROTTO.ordinal()
|
||||||
|
);
|
||||||
|
RiverOutlet outlet = new RiverOutlet(
|
||||||
|
outletId,
|
||||||
|
HydrologyFeatureType.INLAND_GROTTO,
|
||||||
|
node.id(),
|
||||||
|
new HydrologyPoint(node.x(), node.terrain().naturalHeight(), node.z()),
|
||||||
|
HydrologyPlanner.withY(connection.point(), poolY),
|
||||||
|
planner.settingsSeaLevel(node.terrain()),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
inlandCandidates.add(new OutletCandidate(node.index(), -1, outlet));
|
||||||
|
}
|
||||||
|
return inlandCandidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortInlandOutletCandidates(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<OutletCandidate> inlandCandidates
|
||||||
|
) {
|
||||||
|
inlandCandidates.sort(Comparator
|
||||||
|
.comparing((OutletCandidate candidate) -> !isDrainageBasin(grid, candidate.landIndex()))
|
||||||
|
.thenComparingInt((OutletCandidate candidate) -> grid.node(candidate.landIndex()).terrain().naturalHeight())
|
||||||
|
.thenComparingLong((OutletCandidate candidate) -> candidate.outlet().id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortSurfaceFallbackOutletCandidates(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<OutletCandidate> candidates
|
||||||
|
) {
|
||||||
|
HashMap<Integer, Long> capacities = new HashMap<>(candidates.size());
|
||||||
|
for (OutletCandidate candidate : candidates) {
|
||||||
|
capacities.put(candidate.landIndex(), surfaceOutletCapacity(grid, candidate.landIndex()));
|
||||||
|
}
|
||||||
|
candidates.sort(Comparator
|
||||||
|
.comparing((OutletCandidate candidate) -> !isDrainageBasin(grid, candidate.landIndex()))
|
||||||
|
.thenComparingInt((OutletCandidate candidate) ->
|
||||||
|
capacities.get(candidate.landIndex()) > 0L ? 0 : 1)
|
||||||
|
.thenComparingInt((OutletCandidate candidate) -> ownedOutletRank(grid, candidate))
|
||||||
|
.thenComparing(Comparator
|
||||||
|
.comparingLong((OutletCandidate candidate) -> capacities.get(candidate.landIndex()))
|
||||||
|
.reversed())
|
||||||
|
.thenComparingInt((OutletCandidate candidate) ->
|
||||||
|
grid.node(candidate.landIndex()).terrain().naturalHeight())
|
||||||
|
.thenComparingLong((OutletCandidate candidate) -> candidate.outlet().id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isDrainageBasin(HydrologySampledGrid grid, int nodeIndex) {
|
||||||
|
HydrologyGridNode node = grid.node(nodeIndex);
|
||||||
|
int height = node.terrain().naturalHeight();
|
||||||
|
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||||
|
HydrologyGridNode neighbor = grid.nodeAt(node.gridX() + offset.x(), node.gridZ() + offset.z());
|
||||||
|
if (neighbor != null
|
||||||
|
&& !neighbor.terrain().ocean()
|
||||||
|
&& neighbor.terrain().transitAllowed()
|
||||||
|
&& neighbor.terrain().naturalHeight() < height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean[] outletReachable(HydrologySampledGrid grid, List<OutletCandidate> outlets, boolean surface) {
|
||||||
|
int maximumRise = surface ? planner.sourcePlanner.maximumSurfaceEdgeRise() : Integer.MAX_VALUE;
|
||||||
|
boolean[] reachable = new boolean[grid.nodes().size()];
|
||||||
|
int[] queue = new int[reachable.length];
|
||||||
|
int readIndex = 0;
|
||||||
|
int writeIndex = 0;
|
||||||
|
for (OutletCandidate outlet : outlets) {
|
||||||
|
if (reachable[outlet.landIndex()]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reachable[outlet.landIndex()] = true;
|
||||||
|
queue[writeIndex++] = outlet.landIndex();
|
||||||
|
}
|
||||||
|
while (readIndex < writeIndex) {
|
||||||
|
HydrologyGridNode node = grid.node(queue[readIndex++]);
|
||||||
|
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||||
|
HydrologyGridNode neighbor = grid.nodeAt(node.gridX() + offset.x(), node.gridZ() + offset.z());
|
||||||
|
if (neighbor == null || reachable[neighbor.index()] || neighbor.terrain().ocean()
|
||||||
|
|| !neighbor.terrain().transitAllowed()
|
||||||
|
|| node.terrain().naturalHeight() - neighbor.terrain().naturalHeight() > maximumRise) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reachable[neighbor.index()] = true;
|
||||||
|
queue[writeIndex++] = neighbor.index();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OutletCandidate> styledInlandOutlets(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<OutletCandidate> candidates,
|
||||||
|
boolean surface
|
||||||
|
) {
|
||||||
|
ArrayList<OutletCandidate> styled = new ArrayList<>(candidates.size());
|
||||||
|
for (OutletCandidate candidate : candidates) {
|
||||||
|
HydrologyGridNode node = grid.node(candidate.landIndex());
|
||||||
|
RiverOutlet outlet = candidate.outlet();
|
||||||
|
HydrologyPoint landward = planner.routeGeometry.routeAnchor(node);
|
||||||
|
HydrologyTerrainSample landwardTerrain = Objects.requireNonNull(
|
||||||
|
planner.sampleDetailed(landward.x(), landward.z()),
|
||||||
|
"Hydrology inland outlet anchor left sampled terrain"
|
||||||
|
);
|
||||||
|
int connectionOffsetX = Integer.compare(outlet.connectionPoint().x(), outlet.landwardPoint().x());
|
||||||
|
int connectionOffsetZ = Integer.compare(outlet.connectionPoint().z(), outlet.landwardPoint().z());
|
||||||
|
HydrologyPoint connection = new HydrologyPoint(
|
||||||
|
landward.x() + connectionOffsetX,
|
||||||
|
outlet.connectionPoint().y(),
|
||||||
|
landward.z() + connectionOffsetZ
|
||||||
|
);
|
||||||
|
int poolY = planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
||||||
|
"",
|
||||||
|
connection.x(),
|
||||||
|
connection.z(),
|
||||||
|
0L,
|
||||||
|
planner.settings.underground().minimumFluidY(),
|
||||||
|
planner.settings.underground().maximumFluidY()
|
||||||
|
);
|
||||||
|
if (surface && planner.settings.outlets().surfaceSinkholesEnabled()) {
|
||||||
|
poolY = Math.min(
|
||||||
|
poolY,
|
||||||
|
landwardTerrain.naturalHeight() - planner.settings.surface().banks().sink() - 1
|
||||||
|
);
|
||||||
|
poolY = Math.max(
|
||||||
|
poolY,
|
||||||
|
landwardTerrain.naturalHeight() - planner.settings.routing().sampleSpacing()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
styled.add(new OutletCandidate(
|
||||||
|
candidate.landIndex(),
|
||||||
|
candidate.oceanIndex(),
|
||||||
|
new RiverOutlet(
|
||||||
|
outlet.id(),
|
||||||
|
outlet.type(),
|
||||||
|
outlet.drainageNodeId(),
|
||||||
|
HydrologyPlanner.withY(landward, landwardTerrain.naturalHeight()),
|
||||||
|
HydrologyPlanner.withY(connection, poolY),
|
||||||
|
outlet.seaLevel(),
|
||||||
|
outlet.directOcean()
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return List.copyOf(styled);
|
||||||
|
}
|
||||||
|
|
||||||
|
InlandConnection inlandConnection(HydrologyGridNode node) {
|
||||||
|
HydrologyGridOffset offset = CARDINAL_OFFSETS.getFirst();
|
||||||
|
long selectedTie = inlandConnectionTie(node, offset);
|
||||||
|
for (int index = 1; index < CARDINAL_OFFSETS.size(); index++) {
|
||||||
|
HydrologyGridOffset candidate = CARDINAL_OFFSETS.get(index);
|
||||||
|
long candidateTie = inlandConnectionTie(node, candidate);
|
||||||
|
if (candidateTie < selectedTie) {
|
||||||
|
offset = candidate;
|
||||||
|
selectedTie = candidateTie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HydrologyTerrainSample terrain = node.terrain();
|
||||||
|
return new InlandConnection(
|
||||||
|
new HydrologyPoint(
|
||||||
|
node.x() + offset.x(),
|
||||||
|
terrain.caveFluidY(),
|
||||||
|
node.z() + offset.z()
|
||||||
|
),
|
||||||
|
terrain
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long inlandConnectionTie(HydrologyGridNode node, HydrologyGridOffset offset) {
|
||||||
|
return HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
OUTLET_SALT,
|
||||||
|
node.id(),
|
||||||
|
node.x() + offset.x(),
|
||||||
|
node.z() + offset.z()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyGridNode firstOceanNeighbor(HydrologySampledGrid grid, HydrologyGridNode land) {
|
||||||
|
ArrayList<HydrologyGridNode> oceans = new ArrayList<>();
|
||||||
|
for (HydrologyGridOffset offset : CARDINAL_OFFSETS) {
|
||||||
|
HydrologyGridNode neighbor = grid.nodeAt(land.gridX() + offset.x(), land.gridZ() + offset.z());
|
||||||
|
if (neighbor != null && neighbor.terrain().ocean()) {
|
||||||
|
oceans.add(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oceans.sort(Comparator.comparingLong(HydrologyGridNode::id));
|
||||||
|
return oceans.isEmpty() ? null : oceans.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyOceanBoundaryRefiner.Result refineOceanBoundary(HydrologyGridNode land, HydrologyGridNode ocean) {
|
||||||
|
List<HydrologyPoint> crossing = planner.segments.line(land.naturalPoint(), ocean.naturalPoint(), 1);
|
||||||
|
return HydrologyOceanBoundaryRefiner.refine(crossing, planner.sampler, planner.routingSampler);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyFeatureType coastalOutletType(HydrologyTerrainSample land) {
|
||||||
|
HydrologyPlannerSettings.Outlets outlets = planner.settings.outlets();
|
||||||
|
// A zero slope factor turns the slope rule off, so only the coast's height over the sea decides.
|
||||||
|
boolean cliff = land.naturalHeight() - planner.settingsSeaLevel(land)
|
||||||
|
>= outlets.coastalCliffMinimumHeight()
|
||||||
|
|| outlets.coastalCliffSlopeFactor() > 0D
|
||||||
|
&& land.slope() >= outlets.coastalCliffMinimumHeight() * outlets.coastalCliffSlopeFactor();
|
||||||
|
if (cliff && outlets.coastalGrotto().enabled()) {
|
||||||
|
return HydrologyFeatureType.COASTAL_GROTTO;
|
||||||
|
}
|
||||||
|
return outlets.oceanEnabled() ? HydrologyFeatureType.MOUTH : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparator<OutletCandidate> outletComparator(HydrologyTileKey key) {
|
||||||
|
long tileHash = HydrologyHash.mix(planner.worldSeed, key.tileX(), key.tileZ(), OUTLET_SALT);
|
||||||
|
return Comparator.comparingLong((OutletCandidate candidate) -> HydrologyHash.mix(tileHash, candidate.outlet().id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortOutletCandidates(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
boolean surface,
|
||||||
|
List<OutletCandidate> candidates
|
||||||
|
) {
|
||||||
|
// An owned coast outranks a neighbour's: a halo mouth only takes a slot when no owned
|
||||||
|
// coast can serve the tile's sources.
|
||||||
|
Comparator<OutletCandidate> owned = Comparator.comparingInt(
|
||||||
|
(OutletCandidate candidate) -> ownedOutletRank(grid, candidate));
|
||||||
|
if (!surface) {
|
||||||
|
candidates.sort(owned.thenComparing(outletComparator(key)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HashMap<Integer, Long> capacities = new HashMap<>(candidates.size());
|
||||||
|
for (OutletCandidate candidate : candidates) {
|
||||||
|
capacities.put(candidate.landIndex(), surfaceOutletCapacity(grid, candidate.landIndex()));
|
||||||
|
}
|
||||||
|
Comparator<OutletCandidate> stable = outletComparator(key);
|
||||||
|
candidates.sort(Comparator
|
||||||
|
.comparingInt((OutletCandidate candidate) ->
|
||||||
|
capacities.get(candidate.landIndex()) > 0L ? 0 : 1)
|
||||||
|
.thenComparing(owned)
|
||||||
|
.thenComparingInt(this::surfaceOutletPriority)
|
||||||
|
.thenComparing(Comparator
|
||||||
|
.comparingLong((OutletCandidate candidate) -> capacities.get(candidate.landIndex()))
|
||||||
|
.reversed())
|
||||||
|
.thenComparing(stable));
|
||||||
|
}
|
||||||
|
|
||||||
|
int ownedOutletRank(HydrologySampledGrid grid, OutletCandidate candidate) {
|
||||||
|
HydrologyGridNode land = grid.node(candidate.landIndex());
|
||||||
|
return grid.owns(land.x(), land.z()) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int surfaceOutletPriority(OutletCandidate candidate) {
|
||||||
|
return switch (candidate.outlet().type()) {
|
||||||
|
case MOUTH -> 0;
|
||||||
|
case COASTAL_GROTTO -> 1;
|
||||||
|
case INLAND_GROTTO -> 2;
|
||||||
|
default -> 3;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
long surfaceOutletCapacity(HydrologySampledGrid grid, int outletNodeIndex) {
|
||||||
|
boolean[] visited = new boolean[grid.nodes().size()];
|
||||||
|
int[] distances = new int[grid.nodes().size()];
|
||||||
|
Arrays.fill(distances, -1);
|
||||||
|
int[] queue = new int[visited.length];
|
||||||
|
int readIndex = 0;
|
||||||
|
int writeIndex = 0;
|
||||||
|
visited[outletNodeIndex] = true;
|
||||||
|
distances[outletNodeIndex] = 0;
|
||||||
|
queue[writeIndex++] = outletNodeIndex;
|
||||||
|
long capacity = 0L;
|
||||||
|
while (readIndex < writeIndex) {
|
||||||
|
HydrologyGridNode downstream = grid.node(queue[readIndex++]);
|
||||||
|
if (grid.owns(downstream.x(), downstream.z())
|
||||||
|
&& downstream.terrain().surfaceSourceAllowed()
|
||||||
|
&& downstream.terrain().naturalHeight() >= planner.settings.surface().sources().minimumElevation()
|
||||||
|
&& distances[downstream.index()]
|
||||||
|
>= planner.settings.routing().minimumSurfaceCourseLength()) {
|
||||||
|
capacity = Math.addExact(
|
||||||
|
capacity,
|
||||||
|
1_000_000L
|
||||||
|
+ distances[downstream.index()]
|
||||||
|
+ Math.max(0, downstream.terrain().naturalHeight() - planner.settings.seaLevel())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||||
|
HydrologyGridNode upstream = grid.nodeAt(
|
||||||
|
downstream.gridX() + offset.x(),
|
||||||
|
downstream.gridZ() + offset.z()
|
||||||
|
);
|
||||||
|
if (upstream == null
|
||||||
|
|| visited[upstream.index()]
|
||||||
|
|| upstream.terrain().ocean()
|
||||||
|
|| !upstream.terrain().transitAllowed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited[upstream.index()] = true;
|
||||||
|
distances[upstream.index()] = Math.addExact(
|
||||||
|
distances[downstream.index()],
|
||||||
|
planner.settings.routing().sampleSpacing()
|
||||||
|
);
|
||||||
|
queue[writeIndex++] = upstream.index();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OutletCandidate> limitOutlets(List<OutletCandidate> candidates, int maximum) {
|
||||||
|
if (maximum <= 0 || candidates.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
ArrayList<OutletCandidate> selected = new ArrayList<>(Math.min(maximum, candidates.size()));
|
||||||
|
long minimumSpacingSquared = outletSpacingSquared();
|
||||||
|
for (OutletCandidate candidate : candidates) {
|
||||||
|
if (!withinOutletSpacing(candidate, selected, minimumSpacingSquared)) {
|
||||||
|
selected.add(candidate);
|
||||||
|
if (selected.size() == maximum) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected.isEmpty() && !candidates.isEmpty()) {
|
||||||
|
selected.add(candidates.getFirst());
|
||||||
|
}
|
||||||
|
return List.copyOf(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes sea outlets in turns by type (the best mouth, the best coastal grotto, the next mouth, ...)
|
||||||
|
* from a sorted candidate list until the budget is spent or no type has a candidate left that
|
||||||
|
* keeps the outlet spacing. A coast with both cliffs and beaches gets both kinds of opening.
|
||||||
|
*/
|
||||||
|
List<OutletCandidate> limitOutletsByType(List<OutletCandidate> candidates, int maximum) {
|
||||||
|
if (maximum <= 0 || candidates.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
ArrayList<OutletCandidate> selected = new ArrayList<>(Math.min(maximum, candidates.size()));
|
||||||
|
long minimumSpacingSquared = outletSpacingSquared();
|
||||||
|
boolean[] consumed = new boolean[candidates.size()];
|
||||||
|
boolean progressed = true;
|
||||||
|
while (selected.size() < maximum && progressed) {
|
||||||
|
progressed = false;
|
||||||
|
for (HydrologyFeatureType type : COASTAL_OUTLET_TYPES) {
|
||||||
|
for (int index = 0; index < candidates.size(); index++) {
|
||||||
|
OutletCandidate candidate = candidates.get(index);
|
||||||
|
if (consumed[index] || candidate.outlet().type() != type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
consumed[index] = true;
|
||||||
|
if (withinOutletSpacing(candidate, selected, minimumSpacingSquared)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selected.add(candidate);
|
||||||
|
progressed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (selected.size() == maximum) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
long outletSpacingSquared() {
|
||||||
|
int minimumSpacing = planner.settings.routing().sampleSpacing() * 2;
|
||||||
|
return (long) minimumSpacing * minimumSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean withinOutletSpacing(
|
||||||
|
OutletCandidate candidate,
|
||||||
|
List<OutletCandidate> selected,
|
||||||
|
long minimumSpacingSquared
|
||||||
|
) {
|
||||||
|
for (OutletCandidate existing : selected) {
|
||||||
|
if (candidate.outlet().landwardPoint().distanceSquared2D(existing.outlet().landwardPoint())
|
||||||
|
< minimumSpacingSquared) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addRejectedOutlets(
|
||||||
|
List<OutletCandidate> candidates,
|
||||||
|
List<OutletCandidate> selected,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
for (OutletCandidate candidate : candidates) {
|
||||||
|
boolean accepted = false;
|
||||||
|
for (OutletCandidate acceptedCandidate : selected) {
|
||||||
|
if (candidate.outlet().id() == acceptedCandidate.outlet().id()) {
|
||||||
|
accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!accepted) {
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(candidate.outlet().id(), HydrologySourcePlanner.DIAGNOSTIC_SALT),
|
||||||
|
HydrologyCandidateKind.OUTLET,
|
||||||
|
candidate.outlet().type(),
|
||||||
|
candidate.outlet().landwardPoint(),
|
||||||
|
HydrologyCandidateRejection.OUTLET_LIMIT, 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the sea outlets an underground river cannot reach because the sea sits above the
|
||||||
|
* underground fluid ceiling, reporting each as OUTLET_LEVEL with the shortfall as the detail.
|
||||||
|
*/
|
||||||
|
void addOutletLevelDiagnostics(
|
||||||
|
List<OutletCandidate> candidates,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
int maximumFluidY = planner.settings.underground().maximumFluidY();
|
||||||
|
ArrayList<OutletCandidate> retained = new ArrayList<>(candidates.size());
|
||||||
|
for (OutletCandidate candidate : candidates) {
|
||||||
|
int seaY = candidate.outlet().connectionPoint().y();
|
||||||
|
if (seaY <= maximumFluidY) {
|
||||||
|
retained.add(candidate);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(
|
||||||
|
candidate.outlet().id(),
|
||||||
|
HydrologySourcePlanner.DIAGNOSTIC_SALT,
|
||||||
|
HydrologyCandidateRejection.OUTLET_LEVEL.ordinal()
|
||||||
|
),
|
||||||
|
HydrologyCandidateKind.OUTLET,
|
||||||
|
candidate.outlet().type(),
|
||||||
|
candidate.outlet().landwardPoint(),
|
||||||
|
HydrologyCandidateRejection.OUTLET_LEVEL,
|
||||||
|
seaY - maximumFluidY
|
||||||
|
));
|
||||||
|
}
|
||||||
|
candidates.clear();
|
||||||
|
candidates.addAll(retained);
|
||||||
|
}
|
||||||
|
|
||||||
|
record InlandConnection(HydrologyPoint point, HydrologyTerrainSample terrain) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record HydrologyOwnerDraft(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologyCaveCourseFilter.Result result,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||||
|
HydrologyFootprintCompiler footprintCompiler
|
||||||
|
) {
|
||||||
|
HydrologyOwnerDraft withoutFootprintCompiler() {
|
||||||
|
if (footprintCompiler == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return new HydrologyOwnerDraft(key, result, diagnostics, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,312 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class HydrologyRoutePath {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologyRoutePath(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean containsTerrainPit(List<HydrologyPoint> route) {
|
||||||
|
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
|
||||||
|
for (int pointIndex = 1; pointIndex < route.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint previous = route.get(pointIndex - 1);
|
||||||
|
HydrologyPoint point = route.get(pointIndex);
|
||||||
|
HydrologyPoint next = route.get(pointIndex + 1);
|
||||||
|
if (previous.y() - point.y() >= threshold && next.y() - point.y() >= threshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double bilinear(
|
||||||
|
double northwest,
|
||||||
|
double northeast,
|
||||||
|
double southwest,
|
||||||
|
double southeast,
|
||||||
|
double progressX,
|
||||||
|
double progressZ
|
||||||
|
) {
|
||||||
|
double north = northwest + (northeast - northwest) * progressX;
|
||||||
|
double south = southwest + (southeast - southwest) * progressX;
|
||||||
|
return north + (south - north) * progressZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
int removeTerrainPits(List<List<RouteCandidate>> layers, RouteCandidate[] route) {
|
||||||
|
int removed = 0;
|
||||||
|
for (int routeIndex = 1; routeIndex < route.length - 1; routeIndex++) {
|
||||||
|
RouteCandidate candidate = route[routeIndex];
|
||||||
|
if (!isTerrainPit(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ArrayList<RouteCandidate> candidates = new ArrayList<>(layers.get(routeIndex));
|
||||||
|
candidates.remove(candidate);
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
layers.set(routeIndex, List.copyOf(candidates));
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTerrainPit(RouteCandidate candidate) {
|
||||||
|
HydrologyPoint point = candidate.point();
|
||||||
|
RouteDirection tangent = candidate.tangent();
|
||||||
|
int refinement = planner.settings.routing().refinementSpacing();
|
||||||
|
int offsetX = (int) StrictMath.round(tangent.x() * refinement);
|
||||||
|
int offsetZ = (int) StrictMath.round(tangent.z() * refinement);
|
||||||
|
if (offsetX == 0 && offsetZ == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HydrologyTerrainSample before = planner.sampleLandBasisWithoutSlope(point.x() - offsetX, point.z() - offsetZ);
|
||||||
|
HydrologyTerrainSample after = planner.sampleLandBasisWithoutSlope(point.x() + offsetX, point.z() + offsetZ);
|
||||||
|
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
|
||||||
|
return before != null
|
||||||
|
&& after != null
|
||||||
|
&& before.naturalHeight() - point.y() >= threshold
|
||||||
|
&& after.naturalHeight() - point.y() >= threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> smoothTerrainRoute(List<HydrologyPoint> route) {
|
||||||
|
List<HydrologyPoint> current = List.copyOf(route);
|
||||||
|
List<HydrologyPoint> best = current;
|
||||||
|
double bestTurnScore = planner.surfaceCourses.routeTurnScore(current);
|
||||||
|
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||||
|
int maximumPasses = Math.max(8, meanders.smoothingPasses() * 4);
|
||||||
|
for (int pass = 0; pass < maximumPasses; pass++) {
|
||||||
|
ArrayList<HydrologyPoint> smoothed = new ArrayList<>(current.size());
|
||||||
|
smoothed.add(current.getFirst());
|
||||||
|
for (int pointIndex = 1; pointIndex < current.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint previous = current.get(pointIndex - 1);
|
||||||
|
HydrologyPoint point = current.get(pointIndex);
|
||||||
|
HydrologyPoint next = current.get(pointIndex + 1);
|
||||||
|
int x = (int) StrictMath.round(previous.x() * 0.25D + point.x() * 0.5D + next.x() * 0.25D);
|
||||||
|
int z = (int) StrictMath.round(previous.z() * 0.25D + point.z() * 0.5D + next.z() * 0.25D);
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x, z);
|
||||||
|
smoothed.add(terrain == null || !terrain.transitAllowed()
|
||||||
|
? point
|
||||||
|
: new HydrologyPoint(x, terrain.naturalHeight(), z));
|
||||||
|
}
|
||||||
|
smoothed.add(current.getLast());
|
||||||
|
List<HydrologyPoint> candidate = withoutDuplicateRoutePoints(smoothed);
|
||||||
|
if (candidate.size() < 2 || !traversableRoute(candidate)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = candidate;
|
||||||
|
double candidateTurnScore = planner.surfaceCourses.routeTurnScore(candidate);
|
||||||
|
if (candidateTurnScore < bestTurnScore) {
|
||||||
|
best = candidate;
|
||||||
|
bestTurnScore = candidateTurnScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> smoothed = removeRouteHairpins(best, meanders.maximumTurnDegrees());
|
||||||
|
return containsTerrainPit(smoothed) ? List.of() : smoothed;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> removeRouteHairpins(List<HydrologyPoint> route, double maximumTurnDegrees) {
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(route);
|
||||||
|
while (points.size() > 2) {
|
||||||
|
int sharpestIndex = sharpestRoutePoint(points);
|
||||||
|
if (sharpestIndex < 1 || routeTurnDegrees(
|
||||||
|
points.get(sharpestIndex - 1),
|
||||||
|
points.get(sharpestIndex),
|
||||||
|
points.get(sharpestIndex + 1)
|
||||||
|
) <= maximumTurnDegrees) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
HydrologyPoint previous = points.get(sharpestIndex - 1);
|
||||||
|
HydrologyPoint next = points.get(sharpestIndex + 1);
|
||||||
|
if (!traversableHop(previous, next) || crossesTerrainCrevasse(previous, next)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
points.remove(sharpestIndex);
|
||||||
|
}
|
||||||
|
return List.copyOf(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharpestRoutePoint(List<HydrologyPoint> route) {
|
||||||
|
int selectedIndex = -1;
|
||||||
|
double selectedTurn = 0D;
|
||||||
|
for (int pointIndex = 1; pointIndex < route.size() - 1; pointIndex++) {
|
||||||
|
double turn = routeTurnDegrees(
|
||||||
|
route.get(pointIndex - 1),
|
||||||
|
route.get(pointIndex),
|
||||||
|
route.get(pointIndex + 1)
|
||||||
|
);
|
||||||
|
if (turn > selectedTurn) {
|
||||||
|
selectedIndex = pointIndex;
|
||||||
|
selectedTurn = turn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static double routeTurnDegrees(HydrologyPoint previous, HydrologyPoint point, HydrologyPoint next) {
|
||||||
|
double incomingX = point.x() - previous.x();
|
||||||
|
double incomingZ = point.z() - previous.z();
|
||||||
|
double outgoingX = next.x() - point.x();
|
||||||
|
double outgoingZ = next.z() - point.z();
|
||||||
|
double incomingLength = StrictMath.hypot(incomingX, incomingZ);
|
||||||
|
double outgoingLength = StrictMath.hypot(outgoingX, outgoingZ);
|
||||||
|
if (incomingLength == 0D || outgoingLength == 0D) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
double cosine = (incomingX * outgoingX + incomingZ * outgoingZ)
|
||||||
|
/ (incomingLength * outgoingLength);
|
||||||
|
return StrictMath.toDegrees(StrictMath.acos(Math.max(-1D, Math.min(1D, cosine))));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> withoutDuplicateRoutePoints(List<HydrologyPoint> points) {
|
||||||
|
ArrayList<HydrologyPoint> unique = new ArrayList<>(points.size());
|
||||||
|
for (HydrologyPoint point : points) {
|
||||||
|
if (unique.isEmpty()
|
||||||
|
|| point.x() != unique.getLast().x()
|
||||||
|
|| point.z() != unique.getLast().z()) {
|
||||||
|
unique.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean traversableRoute(List<HydrologyPoint> route) {
|
||||||
|
for (int pointIndex = 0; pointIndex < route.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint start = route.get(pointIndex);
|
||||||
|
HydrologyPoint end = route.get(pointIndex + 1);
|
||||||
|
if (!traversableHop(start, end) || crossesTerrainCrevasse(start, end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double routeTransitionCost(
|
||||||
|
RouteCandidate previous,
|
||||||
|
RouteCandidate current,
|
||||||
|
double targetStepLength
|
||||||
|
) {
|
||||||
|
if (!traversableTerrainTransition(previous.point(), current.point())) {
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
return routeTransitionGeometryCost(previous, current, targetStepLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
double routeTransitionGeometryCost(
|
||||||
|
RouteCandidate previous,
|
||||||
|
RouteCandidate current,
|
||||||
|
double targetStepLength
|
||||||
|
) {
|
||||||
|
double deltaX = current.point().x() - previous.point().x();
|
||||||
|
double deltaZ = current.point().z() - previous.point().z();
|
||||||
|
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||||
|
if (length <= 0D || length > targetStepLength * 5D + 8D) {
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
double transverseChange = StrictMath.abs(current.offset() - previous.offset());
|
||||||
|
double alignment = (deltaX * current.tangent().x() + deltaZ * current.tangent().z()) / length;
|
||||||
|
if (alignment < -0.5D) {
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
int rise = Math.max(0, current.point().y() - previous.point().y());
|
||||||
|
return StrictMath.abs(length - targetStepLength) * 1.4D
|
||||||
|
+ transverseChange * 5D
|
||||||
|
+ (1D - alignment) * 32D
|
||||||
|
+ rise * planner.settings.routing().uphillPenalty();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean traversableTerrainTransition(HydrologyPoint start, HydrologyPoint end) {
|
||||||
|
List<HydrologyPoint> crossing = planner.segments.rasterLine(start, end);
|
||||||
|
HydrologyTerrainSample startTerrain = planner.sampleLandBasisWithoutSlope(start.x(), start.z());
|
||||||
|
HydrologyTerrainSample endTerrain = planner.sampleLandBasisWithoutSlope(end.x(), end.z());
|
||||||
|
if (startTerrain == null || endTerrain == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int boundaryHeight = Math.min(startTerrain.naturalHeight(), endTerrain.naturalHeight());
|
||||||
|
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
|
||||||
|
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint point = crossing.get(pointIndex);
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
|
||||||
|
if (terrain == null
|
||||||
|
|| !terrain.transitAllowed()
|
||||||
|
|| boundaryHeight - terrain.naturalHeight() >= threshold) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean crossesTerrainCrevasse(HydrologyPoint start, HydrologyPoint end) {
|
||||||
|
List<HydrologyPoint> crossing = planner.segments.rasterLine(start, end);
|
||||||
|
if (crossing.size() < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HydrologyTerrainSample startTerrain = planner.sampleLandBasisWithoutSlope(start.x(), start.z());
|
||||||
|
HydrologyTerrainSample endTerrain = planner.sampleLandBasisWithoutSlope(end.x(), end.z());
|
||||||
|
if (startTerrain == null || endTerrain == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
int boundaryHeight = Math.min(startTerrain.naturalHeight(), endTerrain.naturalHeight());
|
||||||
|
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
|
||||||
|
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint point = crossing.get(pointIndex);
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
|
||||||
|
if (terrain == null || boundaryHeight - terrain.naturalHeight() >= threshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RouteDirection direction(int startX, int startZ, int endX, int endZ) {
|
||||||
|
double x = endX - startX;
|
||||||
|
double z = endZ - startZ;
|
||||||
|
double length = StrictMath.hypot(x, z);
|
||||||
|
if (length <= 0D) {
|
||||||
|
return new RouteDirection(0D, 0D);
|
||||||
|
}
|
||||||
|
return new RouteDirection(x / length, z / length);
|
||||||
|
}
|
||||||
|
|
||||||
|
double hermite(double start, double end, double startTangent, double endTangent, double progress) {
|
||||||
|
double squared = progress * progress;
|
||||||
|
double cubed = squared * progress;
|
||||||
|
double startBasis = 2D * cubed - 3D * squared + 1D;
|
||||||
|
double startTangentBasis = cubed - 2D * squared + progress;
|
||||||
|
double endBasis = -2D * cubed + 3D * squared;
|
||||||
|
double endTangentBasis = cubed - squared;
|
||||||
|
return startBasis * start
|
||||||
|
+ startTangentBasis * startTangent
|
||||||
|
+ endBasis * end
|
||||||
|
+ endTangentBasis * endTangent;
|
||||||
|
}
|
||||||
|
|
||||||
|
double hermiteDerivative(
|
||||||
|
double start,
|
||||||
|
double end,
|
||||||
|
double startTangent,
|
||||||
|
double endTangent,
|
||||||
|
double progress
|
||||||
|
) {
|
||||||
|
double squared = progress * progress;
|
||||||
|
return (6D * squared - 6D * progress) * start
|
||||||
|
+ (3D * squared - 4D * progress + 1D) * startTangent
|
||||||
|
+ (-6D * squared + 6D * progress) * end
|
||||||
|
+ (3D * squared - 2D * progress) * endTangent;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean traversableHop(HydrologyPoint start, HydrologyPoint end) {
|
||||||
|
List<HydrologyPoint> crossing = planner.segments.rasterLine(start, end);
|
||||||
|
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint point = crossing.get(pointIndex);
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
|
||||||
|
if (terrain == null || terrain.ocean() || !terrain.transitAllowed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record HydrologyRoutingPlan(
|
||||||
|
double[] potential,
|
||||||
|
int[] parent,
|
||||||
|
int[] outletIndex,
|
||||||
|
int[] routeLengths,
|
||||||
|
List<OutletCandidate> outlets,
|
||||||
|
boolean organicSurfaceRequired
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record HydrologySampledGrid(
|
||||||
|
int minimumX,
|
||||||
|
int minimumZ,
|
||||||
|
int ownerMinimumX,
|
||||||
|
int ownerMinimumZ,
|
||||||
|
int ownerSize,
|
||||||
|
int width,
|
||||||
|
int spacing,
|
||||||
|
List<HydrologyGridNode> nodes
|
||||||
|
) {
|
||||||
|
HydrologyGridNode node(int index) {
|
||||||
|
return nodes.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyGridNode nodeAt(int gridX, int gridZ) {
|
||||||
|
if (gridX < 0 || gridZ < 0 || gridX >= width || gridZ >= width) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return nodes.get(gridZ * width + gridX);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyGridNode nodeAtWorld(int x, int z) {
|
||||||
|
long deltaX = (long) x - minimumX;
|
||||||
|
long deltaZ = (long) z - minimumZ;
|
||||||
|
if (deltaX < 0L || deltaZ < 0L || deltaX % spacing != 0L || deltaZ % spacing != 0L) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long gridX = deltaX / spacing;
|
||||||
|
long gridZ = deltaZ / spacing;
|
||||||
|
if (gridX >= width || gridZ >= width) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return nodeAt((int) gridX, (int) gridZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean owns(int x, int z) {
|
||||||
|
return x >= ownerMinimumX && x < ownerMinimumX + ownerSize
|
||||||
|
&& z >= ownerMinimumZ && z < ownerMinimumZ + ownerSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,781 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class HydrologySegmentBuilder {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologySegmentBuilder(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final long SEGMENT_SALT = 0x5345474d454e54L;
|
||||||
|
static final long GRADED_DROP_APPROACH_SALT = 0x474441505052L;
|
||||||
|
static final long GRADED_DROP_RUN_SALT = 0x474452554eL;
|
||||||
|
static final long GRADED_DROP_OUTFLOW_SALT = 0x47444f5554L;
|
||||||
|
|
||||||
|
boolean addHydraulicSegments(
|
||||||
|
long courseId,
|
||||||
|
int pairIndex,
|
||||||
|
HydrologyFeatureType type,
|
||||||
|
int upstreamHead,
|
||||||
|
int downstreamHead,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
List<HydrologyPoint> centerline,
|
||||||
|
boolean surfaceCourse,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
int drop = upstreamHead - downstreamHead;
|
||||||
|
if (drop <= 0 || centerline.size() < 2) {
|
||||||
|
long segmentId = HydrologyHash.mix(planner.worldSeed, SEGMENT_SALT, courseId, pairIndex, type.ordinal());
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
segmentId,
|
||||||
|
courseId,
|
||||||
|
type,
|
||||||
|
upstreamHead,
|
||||||
|
downstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
centerline
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (type == HydrologyFeatureType.WATERFALL) {
|
||||||
|
return addFallingDropSegments(
|
||||||
|
courseId,
|
||||||
|
pairIndex,
|
||||||
|
upstreamHead,
|
||||||
|
downstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
centerline,
|
||||||
|
surfaceCourse,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return addGradedDropSegments(
|
||||||
|
courseId,
|
||||||
|
pairIndex,
|
||||||
|
type,
|
||||||
|
upstreamHead,
|
||||||
|
downstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
centerline,
|
||||||
|
surfaceCourse,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean addFallingDropSegments(
|
||||||
|
long courseId,
|
||||||
|
int pairIndex,
|
||||||
|
int upstreamHead,
|
||||||
|
int downstreamHead,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
List<HydrologyPoint> centerline,
|
||||||
|
boolean surfaceCourse,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
List<HydrologyPoint> raster = rasterCenterline(centerline);
|
||||||
|
if (raster.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int lipIndex = Math.min(waterfallLipIndex(raster), raster.size() - 2);
|
||||||
|
int receiverIndex = lipIndex + 1;
|
||||||
|
addGradedTransitionSegment(
|
||||||
|
courseId,
|
||||||
|
pairIndex,
|
||||||
|
GRADED_DROP_APPROACH_SALT,
|
||||||
|
HydrologyFeatureType.SURFACE_POOL,
|
||||||
|
upstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
levelSlice(raster, 0, lipIndex, upstreamHead),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
HydrologyPlannerSettings.Drops drops = planner.settings.geometry().drops();
|
||||||
|
int flowWidth = hydraulicDropWidth(width, surfaceCourse);
|
||||||
|
int flowDepth = drops.flowDepth(depth);
|
||||||
|
HydrologyPoint lip = HydrologyPlanner.withY(raster.get(lipIndex), upstreamHead);
|
||||||
|
HydrologyPoint receiver = HydrologyPlanner.withY(raster.get(receiverIndex), downstreamHead);
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
HydrologyHash.mix(planner.worldSeed, SEGMENT_SALT, courseId, pairIndex, GRADED_DROP_RUN_SALT),
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.WATERFALL,
|
||||||
|
upstreamHead,
|
||||||
|
downstreamHead,
|
||||||
|
flowWidth,
|
||||||
|
flowDepth,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
List.of(lip, receiver)
|
||||||
|
));
|
||||||
|
addGradedTransitionSegment(
|
||||||
|
courseId,
|
||||||
|
pairIndex,
|
||||||
|
GRADED_DROP_OUTFLOW_SALT,
|
||||||
|
HydrologyFeatureType.SURFACE_POOL,
|
||||||
|
downstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
levelSlice(raster, receiverIndex, raster.size() - 1, downstreamHead),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean addGradedDropSegments(
|
||||||
|
long courseId,
|
||||||
|
int pairIndex,
|
||||||
|
HydrologyFeatureType type,
|
||||||
|
int upstreamHead,
|
||||||
|
int downstreamHead,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
List<HydrologyPoint> centerline,
|
||||||
|
boolean surfaceCourse,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
int drop = upstreamHead - downstreamHead;
|
||||||
|
HydrologyPlannerSettings.Drops drops = planner.settings.geometry().drops();
|
||||||
|
boolean continuousSurfaceBore = surfaceCourse && type == HydrologyFeatureType.UNDERGROUND_DROP;
|
||||||
|
List<HydrologyPoint> directRaster = continuousSurfaceBore
|
||||||
|
? rasterCenterline(centerline)
|
||||||
|
: List.of();
|
||||||
|
int maximumStep = drops.stepLimit(type);
|
||||||
|
if (continuousSurfaceBore) {
|
||||||
|
if (directRaster.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int requiredStep = Math.floorDiv(drop + directRaster.size() - 2, directRaster.size() - 1);
|
||||||
|
if (requiredStep > HydrologyRouteGeometry.MAXIMUM_SURFACE_BORE_STEP) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
maximumStep = Math.max(maximumStep, requiredStep);
|
||||||
|
}
|
||||||
|
int minimumRun = Math.floorDiv(drop + maximumStep - 1, maximumStep) + 1;
|
||||||
|
int maximumGeneratedRun = Math.addExact(planner.settings.routing().maximumRouteLength(), 1);
|
||||||
|
if (minimumRun > maximumGeneratedRun) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int preferredRun = Math.addExact(Math.multiplyExact(drop, drops.cascadeRunPerBlock()), 1);
|
||||||
|
int undergroundRun = drops.undergroundCascadeRunPerBlock();
|
||||||
|
int desiredRun = type.isUnderground()
|
||||||
|
? (undergroundRun == 0
|
||||||
|
? minimumRun
|
||||||
|
: Math.min(
|
||||||
|
Math.max(minimumRun, Math.addExact(Math.multiplyExact(drop, undergroundRun), 1)),
|
||||||
|
maximumGeneratedRun
|
||||||
|
))
|
||||||
|
: Math.min(preferredRun, maximumGeneratedRun);
|
||||||
|
List<HydrologyPoint> raster = continuousSurfaceBore
|
||||||
|
? directRaster
|
||||||
|
: type.isUnderground()
|
||||||
|
? organicDropRaster(courseId, pairIndex, centerline, desiredRun)
|
||||||
|
: rasterCenterline(centerline);
|
||||||
|
if (raster.size() < minimumRun) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int runLength = Math.min(raster.size(), Math.max(minimumRun, desiredRun));
|
||||||
|
int lipIndex = waterfallLipIndex(raster);
|
||||||
|
int runStart = continuousSurfaceBore
|
||||||
|
? 0
|
||||||
|
: type.isUnderground()
|
||||||
|
? HydrologyPlanner.clamp(lipIndex - runLength / 3, 0, raster.size() - runLength)
|
||||||
|
: 0;
|
||||||
|
int runEnd = continuousSurfaceBore
|
||||||
|
? raster.size() - 1
|
||||||
|
: type.isUnderground() ? runStart + runLength - 1 : raster.size() - 1;
|
||||||
|
HydrologyFeatureType flatType = type.isUnderground()
|
||||||
|
? HydrologyFeatureType.UNDERGROUND_POOL
|
||||||
|
: HydrologyFeatureType.SURFACE_POOL;
|
||||||
|
if (!continuousSurfaceBore) {
|
||||||
|
addGradedTransitionSegment(
|
||||||
|
courseId,
|
||||||
|
pairIndex,
|
||||||
|
GRADED_DROP_APPROACH_SALT,
|
||||||
|
flatType,
|
||||||
|
upstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
levelSlice(raster, 0, runStart, upstreamHead),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> graded = gradedDropCenterline(
|
||||||
|
raster.subList(runStart, runEnd + 1),
|
||||||
|
upstreamHead,
|
||||||
|
downstreamHead,
|
||||||
|
drops,
|
||||||
|
maximumStep
|
||||||
|
);
|
||||||
|
int dropWidth = hydraulicDropWidth(width, surfaceCourse);
|
||||||
|
int dropDepth = drops.flowDepth(depth);
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
HydrologyHash.mix(planner.worldSeed, SEGMENT_SALT, courseId, pairIndex, GRADED_DROP_RUN_SALT),
|
||||||
|
courseId,
|
||||||
|
type,
|
||||||
|
upstreamHead,
|
||||||
|
downstreamHead,
|
||||||
|
dropWidth,
|
||||||
|
dropDepth,
|
||||||
|
false,
|
||||||
|
!continuousSurfaceBore,
|
||||||
|
graded
|
||||||
|
));
|
||||||
|
if (!continuousSurfaceBore) {
|
||||||
|
addGradedTransitionSegment(
|
||||||
|
courseId,
|
||||||
|
pairIndex,
|
||||||
|
GRADED_DROP_OUTFLOW_SALT,
|
||||||
|
flatType,
|
||||||
|
downstreamHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
levelSlice(raster, runEnd, raster.size() - 1, downstreamHead),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hydraulicDropWidth(int channelWidth, boolean surfaceCourse) {
|
||||||
|
int dropWidth = planner.settings.geometry().drops().flowWidth(channelWidth);
|
||||||
|
return surfaceCourse ? Math.max(planner.settings.surface().minimumWidth(), dropWidth) : dropWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> organicDropRaster(
|
||||||
|
long courseId,
|
||||||
|
int pairIndex,
|
||||||
|
List<HydrologyPoint> centerline,
|
||||||
|
int desiredRun
|
||||||
|
) {
|
||||||
|
List<HydrologyPoint> base = rasterCenterline(centerline);
|
||||||
|
if (base.size() >= desiredRun) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
int wavelength = planner.settings.geometry().meanders().detailWavelength();
|
||||||
|
double phase = HydrologyHash.unit(HydrologyHash.mix(planner.worldSeed, courseId, pairIndex, GRADED_DROP_RUN_SALT))
|
||||||
|
* StrictMath.PI * 2D;
|
||||||
|
int maximumAmplitude = Math.max(3, planner.settings.routing().sampleSpacing() / 2);
|
||||||
|
List<HydrologyPoint> longest = base;
|
||||||
|
for (int amplitude = 3; amplitude <= maximumAmplitude; amplitude += 2) {
|
||||||
|
int cycles = Math.max(1, (int) StrictMath.ceil(
|
||||||
|
Math.max(0, desiredRun - base.size()) / (2D * amplitude)
|
||||||
|
));
|
||||||
|
int samples = Math.max(base.size(), desiredRun * 3);
|
||||||
|
ArrayList<HydrologyPoint> worm = new ArrayList<>(samples);
|
||||||
|
for (int sampleIndex = 0; sampleIndex < samples; sampleIndex++) {
|
||||||
|
double progress = sampleIndex / (double) (samples - 1);
|
||||||
|
double baseIndex = progress * (base.size() - 1);
|
||||||
|
int lowerIndex = Math.min(base.size() - 2, (int) StrictMath.floor(baseIndex));
|
||||||
|
int upperIndex = lowerIndex + 1;
|
||||||
|
double localProgress = baseIndex - lowerIndex;
|
||||||
|
HydrologyPoint lower = base.get(lowerIndex);
|
||||||
|
HydrologyPoint upper = base.get(upperIndex);
|
||||||
|
double baseX = lower.x() + (upper.x() - lower.x()) * localProgress;
|
||||||
|
double baseZ = lower.z() + (upper.z() - lower.z()) * localProgress;
|
||||||
|
double tangentX = upper.x() - lower.x();
|
||||||
|
double tangentZ = upper.z() - lower.z();
|
||||||
|
double tangentLength = StrictMath.hypot(tangentX, tangentZ);
|
||||||
|
if (tangentLength <= 0D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double envelope = StrictMath.pow(StrictMath.sin(StrictMath.PI * progress), 2D);
|
||||||
|
double displacement = StrictMath.sin(
|
||||||
|
progress * StrictMath.PI * 2D * cycles
|
||||||
|
+ baseIndex * StrictMath.PI * 2D / wavelength
|
||||||
|
+ phase
|
||||||
|
) * amplitude * envelope;
|
||||||
|
int x = (int) StrictMath.round(baseX - tangentZ / tangentLength * displacement);
|
||||||
|
int z = (int) StrictMath.round(baseZ + tangentX / tangentLength * displacement);
|
||||||
|
if (worm.isEmpty() || worm.getLast().x() != x || worm.getLast().z() != z) {
|
||||||
|
worm.add(new HydrologyPoint(x, lower.y(), z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> raster = rasterCenterline(worm);
|
||||||
|
if (raster.size() > longest.size()) {
|
||||||
|
longest = raster;
|
||||||
|
}
|
||||||
|
if (raster.size() >= desiredRun) {
|
||||||
|
return raster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return longest;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addGradedTransitionSegment(
|
||||||
|
long courseId,
|
||||||
|
int pairIndex,
|
||||||
|
long salt,
|
||||||
|
HydrologyFeatureType type,
|
||||||
|
int head,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
List<HydrologyPoint> centerline,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
if (centerline.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
HydrologyHash.mix(planner.worldSeed, SEGMENT_SALT, courseId, pairIndex, salt),
|
||||||
|
courseId,
|
||||||
|
type,
|
||||||
|
head,
|
||||||
|
head,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
List.copyOf(centerline)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> gradedDropCenterline(
|
||||||
|
List<HydrologyPoint> points,
|
||||||
|
int upstreamHead,
|
||||||
|
int downstreamHead,
|
||||||
|
HydrologyPlannerSettings.Drops drops,
|
||||||
|
int maximumStep
|
||||||
|
) {
|
||||||
|
int pointCount = points.size();
|
||||||
|
int drop = upstreamHead - downstreamHead;
|
||||||
|
if ((long) (pointCount - 1) * maximumStep < drop) {
|
||||||
|
throw new IllegalArgumentException("Graded drop centerline cannot contain its configured head loss.");
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> graded = new ArrayList<>(pointCount);
|
||||||
|
int previousHead = upstreamHead;
|
||||||
|
for (int pointIndex = 0; pointIndex < pointCount; pointIndex++) {
|
||||||
|
double progress = pointIndex / (double) (pointCount - 1);
|
||||||
|
int idealHead = upstreamHead - (int) StrictMath.round(
|
||||||
|
drop * StrictMath.pow(progress, drops.cascadeExponent())
|
||||||
|
);
|
||||||
|
int remainingSteps = pointCount - pointIndex - 1;
|
||||||
|
int latestReachableHead = downstreamHead + remainingSteps * maximumStep;
|
||||||
|
int head = Math.min(previousHead, Math.min(idealHead, latestReachableHead));
|
||||||
|
head = Math.max(downstreamHead, Math.max(previousHead - maximumStep, head));
|
||||||
|
if (pointIndex == pointCount - 1) {
|
||||||
|
head = downstreamHead;
|
||||||
|
}
|
||||||
|
HydrologyPoint point = points.get(pointIndex);
|
||||||
|
graded.add(new HydrologyPoint(point.x(), head, point.z()));
|
||||||
|
previousHead = head;
|
||||||
|
}
|
||||||
|
return List.copyOf(graded);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> levelSlice(
|
||||||
|
List<HydrologyPoint> points,
|
||||||
|
int startIndex,
|
||||||
|
int endIndex,
|
||||||
|
int head
|
||||||
|
) {
|
||||||
|
ArrayList<HydrologyPoint> leveled = new ArrayList<>(Math.max(0, endIndex - startIndex + 1));
|
||||||
|
for (int pointIndex = startIndex; pointIndex <= endIndex; pointIndex++) {
|
||||||
|
leveled.add(HydrologyPlanner.withY(points.get(pointIndex), head));
|
||||||
|
}
|
||||||
|
return List.copyOf(leveled);
|
||||||
|
}
|
||||||
|
|
||||||
|
int waterfallLipIndex(List<HydrologyPoint> raster) {
|
||||||
|
return waterfallLipIndex(raster, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int waterfallLipIndex(List<HydrologyPoint> raster, int requiredDecline) {
|
||||||
|
int fallback = Math.max(0, raster.size() / 2 - 1);
|
||||||
|
int selected = fallback;
|
||||||
|
long strongestDecline = Long.MIN_VALUE;
|
||||||
|
for (int pointIndex = 0; pointIndex < raster.size() - 1; pointIndex++) {
|
||||||
|
HydrologyPoint current = raster.get(pointIndex);
|
||||||
|
HydrologyPoint downstream = raster.get(pointIndex + 1);
|
||||||
|
HydrologyTerrainSample currentTerrain = planner.sampleBasisWithoutSlope(current.x(), current.z());
|
||||||
|
HydrologyTerrainSample downstreamTerrain = planner.sampleBasisWithoutSlope(downstream.x(), downstream.z());
|
||||||
|
long decline = (long) currentTerrain.naturalHeight() - downstreamTerrain.naturalHeight();
|
||||||
|
if (decline > strongestDecline) {
|
||||||
|
strongestDecline = decline;
|
||||||
|
selected = pointIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredDecline <= 0 || strongestDecline >= requiredDecline ? selected : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> rasterCenterline(List<HydrologyPoint> centerline) {
|
||||||
|
ArrayList<HydrologyPoint> raster = new ArrayList<>();
|
||||||
|
for (int pointIndex = 0; pointIndex < centerline.size() - 1; pointIndex++) {
|
||||||
|
List<HydrologyPoint> span = rasterLine(centerline.get(pointIndex), centerline.get(pointIndex + 1));
|
||||||
|
int first = raster.isEmpty() ? 0 : 1;
|
||||||
|
for (int spanIndex = first; spanIndex < span.size(); spanIndex++) {
|
||||||
|
raster.add(span.get(spanIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(raster);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> rasterLine(HydrologyPoint start, HydrologyPoint end) {
|
||||||
|
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||||
|
if (steps == 0) {
|
||||||
|
return List.of(start);
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(steps + 1);
|
||||||
|
for (int step = 0; step <= steps; step++) {
|
||||||
|
double progress = step / (double) steps;
|
||||||
|
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||||
|
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||||
|
points.add(new HydrologyPoint(x, start.y(), z));
|
||||||
|
}
|
||||||
|
return List.copyOf(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendOutletSegments(
|
||||||
|
RiverCourseType courseType,
|
||||||
|
long courseId,
|
||||||
|
HydrologyCoursePath path,
|
||||||
|
int head,
|
||||||
|
int[] widths,
|
||||||
|
int[] depths,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
RiverOutlet outlet = path.outlet();
|
||||||
|
int width = widths.length == 0 ? 1 : widths[widths.length - 1];
|
||||||
|
int depth = depths.length == 0 ? 1 : depths[depths.length - 1];
|
||||||
|
if (courseType != RiverCourseType.SURFACE
|
||||||
|
&& outlet.type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||||
|
width = planner.settings.underground().minimumWidth();
|
||||||
|
depth = planner.settings.underground().minimumDepth();
|
||||||
|
}
|
||||||
|
HydrologyPoint pathEnd = HydrologyPlanner.withY(path.points().getLast(), head);
|
||||||
|
HydrologyPoint landward = HydrologyPlanner.withY(outlet.landwardPoint(), head);
|
||||||
|
if (courseType == RiverCourseType.SURFACE
|
||||||
|
&& outlet.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||||
|
appendSurfaceCoastalGrotto(
|
||||||
|
courseId,
|
||||||
|
outlet,
|
||||||
|
head,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
pathEnd,
|
||||||
|
landward,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (outlet.type() == HydrologyFeatureType.MOUTH) {
|
||||||
|
if (courseType == RiverCourseType.SURFACE) {
|
||||||
|
appendSurfaceMouth(courseId, outlet, head, width, depth, pathEnd, segments);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HydrologyPoint connection = HydrologyPlanner.withY(outlet.connectionPoint(), head);
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.MOUTH,
|
||||||
|
head,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
line(pathEnd, connection, planner.settings.routing().refinementSpacing()),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (courseType == RiverCourseType.SURFACE && surfaceSinkhole(outlet)) {
|
||||||
|
appendSurfaceSinkhole(courseId, outlet, head, width, depth, pathEnd, landward, segments);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HydrologyPoint connection = HydrologyPlanner.withY(outlet.connectionPoint(), head);
|
||||||
|
if (pathEnd.x() != landward.x() || pathEnd.z() != landward.z()) {
|
||||||
|
HydrologyFeatureType connectorType = courseType == RiverCourseType.SURFACE
|
||||||
|
? HydrologyFeatureType.MOUTH
|
||||||
|
: HydrologyFeatureType.UNDERGROUND_POOL;
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
connectorType,
|
||||||
|
head,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
line(pathEnd, landward, planner.settings.routing().refinementSpacing()),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> centerline = landward.x() == connection.x() && landward.z() == connection.z()
|
||||||
|
? List.of(landward)
|
||||||
|
: List.of(landward, connection);
|
||||||
|
addFlatSegment(courseId, outlet.type(), head, width, depth, centerline, segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendSurfaceMouth(
|
||||||
|
long courseId,
|
||||||
|
RiverOutlet outlet,
|
||||||
|
int surfaceHead,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
HydrologyPoint pathEnd,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
int seaLevel = outletHead(outlet);
|
||||||
|
HydrologyPoint connection = HydrologyPlanner.withY(outlet.connectionPoint(), seaLevel);
|
||||||
|
if (surfaceHead != seaLevel) {
|
||||||
|
throw new IllegalStateException("A surface mouth reached the coastal crossing above sea level.");
|
||||||
|
}
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.MOUTH,
|
||||||
|
seaLevel,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
line(pathEnd, connection, planner.settings.routing().refinementSpacing()),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendSurfaceCoastalGrotto(
|
||||||
|
long courseId,
|
||||||
|
RiverOutlet outlet,
|
||||||
|
int surfaceHead,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
HydrologyPoint pathEnd,
|
||||||
|
HydrologyPoint landward,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
if (pathEnd.x() != landward.x() || pathEnd.z() != landward.z()) {
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.SURFACE_POOL,
|
||||||
|
surfaceHead,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
line(pathEnd, landward, planner.settings.routing().refinementSpacing()),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
int seaLevel = outletHead(outlet);
|
||||||
|
HydrologyPoint connection = HydrologyPlanner.withY(outlet.connectionPoint(), seaLevel);
|
||||||
|
if (surfaceHead > seaLevel) {
|
||||||
|
addFallingDropSegments(
|
||||||
|
courseId,
|
||||||
|
segments.size(),
|
||||||
|
surfaceHead,
|
||||||
|
seaLevel,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
List.of(landward, connection),
|
||||||
|
true,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.COASTAL_GROTTO,
|
||||||
|
seaLevel,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
List.of(HydrologyPlanner.withY(outlet.landwardPoint(), seaLevel), connection),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendSurfaceSinkhole(
|
||||||
|
long courseId,
|
||||||
|
RiverOutlet outlet,
|
||||||
|
int surfaceHead,
|
||||||
|
int surfaceWidth,
|
||||||
|
int surfaceDepth,
|
||||||
|
HydrologyPoint pathEnd,
|
||||||
|
HydrologyPoint landward,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
if (pathEnd.x() != landward.x() || pathEnd.z() != landward.z()) {
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.SURFACE_POOL,
|
||||||
|
surfaceHead,
|
||||||
|
surfaceWidth,
|
||||||
|
surfaceDepth,
|
||||||
|
line(pathEnd, landward, planner.settings.routing().refinementSpacing()),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
int undergroundHead = outletHead(outlet);
|
||||||
|
int throatWidth = Math.max(planner.settings.underground().minimumWidth(), surfaceWidth - 1);
|
||||||
|
int throatDepth = planner.settings.underground().minimumDepth();
|
||||||
|
HydrologyPoint receiving = HydrologyPlanner.withY(outlet.connectionPoint(), undergroundHead);
|
||||||
|
List<HydrologyPoint> descent = sinkholeDescent(
|
||||||
|
courseId,
|
||||||
|
landward,
|
||||||
|
receiving,
|
||||||
|
surfaceHead - undergroundHead
|
||||||
|
);
|
||||||
|
List<HydrologyPoint> gradedDescent = gradedDropCenterline(
|
||||||
|
rasterCenterline(descent),
|
||||||
|
surfaceHead,
|
||||||
|
undergroundHead,
|
||||||
|
planner.settings.geometry().drops(),
|
||||||
|
planner.settings.geometry().drops().stepLimit(HydrologyFeatureType.SINKHOLE)
|
||||||
|
);
|
||||||
|
int descentWidth = Math.max(throatWidth, hydraulicDropWidth(throatWidth, true));
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
SEGMENT_SALT,
|
||||||
|
courseId,
|
||||||
|
segments.size(),
|
||||||
|
HydrologyFeatureType.SINKHOLE.ordinal()
|
||||||
|
),
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.SINKHOLE,
|
||||||
|
surfaceHead,
|
||||||
|
undergroundHead,
|
||||||
|
descentWidth,
|
||||||
|
throatDepth,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
gradedDescent
|
||||||
|
));
|
||||||
|
addFlatSegment(
|
||||||
|
courseId,
|
||||||
|
HydrologyFeatureType.INLAND_GROTTO,
|
||||||
|
undergroundHead,
|
||||||
|
throatWidth,
|
||||||
|
throatDepth,
|
||||||
|
List.of(receiving),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> sinkholeDescent(
|
||||||
|
long courseId,
|
||||||
|
HydrologyPoint landward,
|
||||||
|
HydrologyPoint receiving,
|
||||||
|
int drop
|
||||||
|
) {
|
||||||
|
int desiredRun = Math.max(
|
||||||
|
4,
|
||||||
|
(int) StrictMath.ceil(drop / (double) planner.settings.geometry().drops().maximumCascadeStep()) + 1
|
||||||
|
);
|
||||||
|
double startX = landward.x() - receiving.x();
|
||||||
|
double startZ = landward.z() - receiving.z();
|
||||||
|
double startRadius = StrictMath.hypot(startX, startZ);
|
||||||
|
double startAngle = startRadius <= 0D
|
||||||
|
? HydrologyHash.unit(HydrologyHash.mix(planner.worldSeed, courseId, SEGMENT_SALT)) * StrictMath.PI * 2D
|
||||||
|
: StrictMath.atan2(startZ, startX);
|
||||||
|
double maximumRadius = Math.max(2D, planner.settings.outlets().inlandGrotto().horizontalRadius() - 1D);
|
||||||
|
int turns = Math.max(1, (int) StrictMath.ceil(desiredRun / (StrictMath.PI * maximumRadius)));
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(desiredRun * 4 + 1);
|
||||||
|
int samples = desiredRun * 4;
|
||||||
|
for (int sample = 0; sample <= samples; sample++) {
|
||||||
|
double progress = sample / (double) samples;
|
||||||
|
double radius = Math.min(
|
||||||
|
maximumRadius,
|
||||||
|
startRadius * (1D - progress) + maximumRadius * StrictMath.sin(StrictMath.PI * progress)
|
||||||
|
);
|
||||||
|
double angle = startAngle + turns * StrictMath.PI * 2D * progress;
|
||||||
|
int x = (int) StrictMath.round(receiving.x() + StrictMath.cos(angle) * radius);
|
||||||
|
int z = (int) StrictMath.round(receiving.z() + StrictMath.sin(angle) * radius);
|
||||||
|
if (points.isEmpty() || points.getLast().x() != x || points.getLast().z() != z) {
|
||||||
|
points.add(new HydrologyPoint(x, landward.y(), z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (points.getLast().x() != receiving.x() || points.getLast().z() != receiving.z()) {
|
||||||
|
points.add(receiving);
|
||||||
|
}
|
||||||
|
return List.copyOf(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean surfaceSinkhole(RiverOutlet outlet) {
|
||||||
|
return outlet.type() == HydrologyFeatureType.INLAND_GROTTO
|
||||||
|
&& planner.settings.outlets().surfaceSinkholesEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFlatSegment(
|
||||||
|
long courseId,
|
||||||
|
HydrologyFeatureType type,
|
||||||
|
int head,
|
||||||
|
int width,
|
||||||
|
int depth,
|
||||||
|
List<HydrologyPoint> centerline,
|
||||||
|
List<HydraulicSegment> segments
|
||||||
|
) {
|
||||||
|
long segmentId = HydrologyHash.mix(planner.worldSeed, SEGMENT_SALT, courseId, segments.size(), type.ordinal());
|
||||||
|
segments.add(new HydraulicSegment(
|
||||||
|
segmentId,
|
||||||
|
courseId,
|
||||||
|
type,
|
||||||
|
head,
|
||||||
|
head,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
centerline
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
double signedOrganicOffset(long stableId, long salt, double envelope) {
|
||||||
|
double unit = HydrologyHash.unit(HydrologyHash.mix(stableId, salt));
|
||||||
|
double magnitude = envelope * (0.55D + unit * 0.45D);
|
||||||
|
return (HydrologyHash.mix(stableId, salt, 1L) & 1L) == 0L ? magnitude : -magnitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> line(HydrologyPoint start, HydrologyPoint end, int spacing) {
|
||||||
|
double distance = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
|
||||||
|
int steps = Math.max(1, (int) StrictMath.ceil(distance / spacing));
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(steps + 1);
|
||||||
|
for (int step = 0; step <= steps; step++) {
|
||||||
|
double progress = step / (double) steps;
|
||||||
|
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||||
|
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||||
|
HydrologyPoint point = new HydrologyPoint(x, start.y(), z);
|
||||||
|
if (points.isEmpty() || point.x() != points.getLast().x() || point.z() != points.getLast().z()) {
|
||||||
|
points.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(points);
|
||||||
|
}
|
||||||
|
|
||||||
|
int scaledDimension(
|
||||||
|
int styledBase,
|
||||||
|
int minimum,
|
||||||
|
int maximum,
|
||||||
|
int discharge,
|
||||||
|
double multiplier
|
||||||
|
) {
|
||||||
|
double flowScale = Math.min(
|
||||||
|
1D,
|
||||||
|
StrictMath.log(discharge + 1D) / StrictMath.log(planner.settings.underground().wideningSources() + 1D)
|
||||||
|
);
|
||||||
|
int base = styledBase + (int) StrictMath.round((maximum - styledBase) * flowScale);
|
||||||
|
return HydrologyPlanner.clamp((int) StrictMath.round(base * multiplier), minimum, maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maximumSurfaceDischarge(List<DrainageEdge> edges) {
|
||||||
|
int maximum = 1;
|
||||||
|
for (DrainageEdge edge : edges) {
|
||||||
|
maximum = Math.max(maximum, edge.contributingSurfaceSources());
|
||||||
|
}
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maximumUndergroundDischarge(List<DrainageEdge> edges) {
|
||||||
|
int maximum = 1;
|
||||||
|
for (DrainageEdge edge : edges) {
|
||||||
|
maximum = Math.max(maximum, edge.contributingUndergroundSources());
|
||||||
|
}
|
||||||
|
return maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
String chooseProfile(HydrologyTerrainSample terrain, long courseId) {
|
||||||
|
List<String> profiles = terrain.preferredProfileKeys();
|
||||||
|
int index = HydrologyHash.between(HydrologyHash.mix(planner.worldSeed, courseId, HydrologyHash.text("profile")), 0, profiles.size() - 1);
|
||||||
|
return profiles.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
int outletHead(RiverOutlet outlet) {
|
||||||
|
return outlet.directOcean() ? outlet.seaLevel() : outlet.connectionPoint().y();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,913 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
|
import java.util.function.IntPredicate;
|
||||||
|
|
||||||
|
final class HydrologySourcePlanner {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologySourcePlanner(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final long EDGE_SALT = 0x45444745L;
|
||||||
|
static final long SURFACE_SOURCE_SALT = 0x53555246414345L;
|
||||||
|
static final long UNDERGROUND_SOURCE_SALT = 0x554e444552L;
|
||||||
|
static final long DIAGNOSTIC_SALT = 0x444941474e4f5354L;
|
||||||
|
static final int OPTIONAL_SOURCE_REJECTIONS_PER_TARGET = 4;
|
||||||
|
static final int SURFACE_OPTIONAL_SOURCE_REJECTIONS_PER_TARGET = 8;
|
||||||
|
|
||||||
|
static final List<HydrologyGridOffset> ROUTING_OFFSETS = List.of(
|
||||||
|
new HydrologyGridOffset(-1, 0),
|
||||||
|
new HydrologyGridOffset(1, 0),
|
||||||
|
new HydrologyGridOffset(0, -1),
|
||||||
|
new HydrologyGridOffset(0, 1),
|
||||||
|
new HydrologyGridOffset(-1, -1),
|
||||||
|
new HydrologyGridOffset(-1, 1),
|
||||||
|
new HydrologyGridOffset(1, -1),
|
||||||
|
new HydrologyGridOffset(1, 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
boolean hasRoutedSourceSearch() {
|
||||||
|
HydrologyPlannerSettings.Source surface = planner.settings.surface().sources();
|
||||||
|
if (planner.settings.surface().enabled() && surface.enabled()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
HydrologyPlannerSettings.Source underground = planner.settings.underground().sources();
|
||||||
|
return planner.settings.underground().enabled()
|
||||||
|
&& underground.enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyDiagnosticCandidate> uniqueDiagnostics(
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
LinkedHashMap<Long, HydrologyDiagnosticCandidate> unique = new LinkedHashMap<>();
|
||||||
|
for (HydrologyDiagnosticCandidate diagnostic : diagnostics) {
|
||||||
|
unique.putIfAbsent(diagnostic.id(), diagnostic);
|
||||||
|
}
|
||||||
|
return List.copyOf(unique.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyRoutingPlan buildRouting(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<OutletCandidate> outlets,
|
||||||
|
boolean surface
|
||||||
|
) {
|
||||||
|
// A surface river never climbs: any rise along its route becomes a cut of at least that rise,
|
||||||
|
// so lattice edges that rise more than a small saddle are not drainage at all.
|
||||||
|
int maximumRise = surface ? maximumSurfaceEdgeRise() : Integer.MAX_VALUE;
|
||||||
|
int nodeCount = grid.nodes().size();
|
||||||
|
double[] potential = new double[nodeCount];
|
||||||
|
int[] parent = new int[nodeCount];
|
||||||
|
int[] outletIndex = new int[nodeCount];
|
||||||
|
int[] minimumNeighborHeights = new int[nodeCount];
|
||||||
|
for (HydrologyGridNode node : grid.nodes()) {
|
||||||
|
minimumNeighborHeights[node.index()] = minimumNeighborHeight(grid, node);
|
||||||
|
}
|
||||||
|
Arrays.fill(potential, Double.POSITIVE_INFINITY);
|
||||||
|
Arrays.fill(parent, -1);
|
||||||
|
Arrays.fill(outletIndex, -1);
|
||||||
|
PriorityQueue<RoutingQueueEntry> queue = new PriorityQueue<>();
|
||||||
|
for (int index = 0; index < outlets.size(); index++) {
|
||||||
|
OutletCandidate outlet = outlets.get(index);
|
||||||
|
potential[outlet.landIndex()] = 0D;
|
||||||
|
outletIndex[outlet.landIndex()] = index;
|
||||||
|
queue.add(new RoutingQueueEntry(outlet.landIndex(), 0D, index));
|
||||||
|
}
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
RoutingQueueEntry currentEntry = queue.poll();
|
||||||
|
if (currentEntry.potential() != potential[currentEntry.nodeIndex()]
|
||||||
|
|| currentEntry.outletIndex() != outletIndex[currentEntry.nodeIndex()]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyGridNode downstream = grid.node(currentEntry.nodeIndex());
|
||||||
|
for (HydrologyGridOffset offset : ROUTING_OFFSETS) {
|
||||||
|
HydrologyGridNode upstream = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
|
||||||
|
if (upstream == null || upstream.terrain().ocean() || !upstream.terrain().transitAllowed()
|
||||||
|
|| !upstream.terrain().drainsInto(downstream.terrain())) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
if (downstream.terrain().naturalHeight() - upstream.terrain().naturalHeight() > maximumRise) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double edgeCost = routeCost(
|
||||||
|
upstream,
|
||||||
|
downstream,
|
||||||
|
offset,
|
||||||
|
minimumNeighborHeights[downstream.index()]
|
||||||
|
);
|
||||||
|
double candidatePotential = currentEntry.potential() + edgeCost;
|
||||||
|
int existingParent = parent[upstream.index()];
|
||||||
|
boolean replace = candidatePotential < potential[upstream.index()] - 1.0E-9D;
|
||||||
|
if (!replace && StrictMath.abs(candidatePotential - potential[upstream.index()]) <= 1.0E-9D) {
|
||||||
|
replace = existingParent < 0 || downstream.id() < grid.node(existingParent).id();
|
||||||
|
}
|
||||||
|
if (!replace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
potential[upstream.index()] = candidatePotential;
|
||||||
|
parent[upstream.index()] = downstream.index();
|
||||||
|
outletIndex[upstream.index()] = currentEntry.outletIndex();
|
||||||
|
queue.add(new RoutingQueueEntry(
|
||||||
|
upstream.index(),
|
||||||
|
candidatePotential,
|
||||||
|
currentEntry.outletIndex()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new HydrologyRoutingPlan(
|
||||||
|
potential,
|
||||||
|
parent,
|
||||||
|
outletIndex,
|
||||||
|
routeLengths(parent, grid.width(), planner.settings.routing()),
|
||||||
|
List.copyOf(outlets),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Largest rise a surface drainage edge may take. Heads only fall, so a rise along a route becomes a cut
|
||||||
|
* of at least that rise; the lattice may climb no more than the cut the valley solver still accepts.
|
||||||
|
*/
|
||||||
|
int maximumSurfaceEdgeRise() {
|
||||||
|
int permitted = planner.settings.surface().maximumIncision() - planner.settings.surface().banks().sink() - planner.settings.surface().minimumDepth();
|
||||||
|
return Math.max(1, permitted);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyRoutingPlan requireOrganicSurface(HydrologyRoutingPlan routing) {
|
||||||
|
return new HydrologyRoutingPlan(
|
||||||
|
routing.potential(),
|
||||||
|
routing.parent(),
|
||||||
|
routing.outletIndex(),
|
||||||
|
routing.routeLengths(),
|
||||||
|
routing.outlets(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int permittedSurfaceIncision(HydrologyTerrainSample terrain) {
|
||||||
|
return permittedSurfaceIncision(
|
||||||
|
planner.settings.surface().maximumIncision(),
|
||||||
|
terrain.incisionMultiplier()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int permittedSurfaceIncision(int maximumIncision, double incisionMultiplier) {
|
||||||
|
return Math.min(
|
||||||
|
maximumIncision,
|
||||||
|
(int) StrictMath.floor(maximumIncision * incisionMultiplier)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double routeCost(
|
||||||
|
HydrologyGridNode upstream,
|
||||||
|
HydrologyGridNode downstream,
|
||||||
|
HydrologyGridOffset offset,
|
||||||
|
int valleyFloor
|
||||||
|
) {
|
||||||
|
HydrologyPlannerSettings.Routing routing = planner.settings.routing();
|
||||||
|
double baseDistance = routing.sampleSpacing() * (offset.x() != 0 && offset.z() != 0 ? StrictMath.sqrt(2D) : 1D);
|
||||||
|
double downstreamRise = Math.max(0D, downstream.terrain().naturalHeight() - upstream.terrain().naturalHeight());
|
||||||
|
double valleyCost = Math.max(0D, downstream.terrain().naturalHeight() - valleyFloor) * routing.valleyPreference();
|
||||||
|
double policyCost = upstream.terrain().routingCost() + downstream.terrain().routingCost();
|
||||||
|
double policyMultiplier = Math.max(0.01D,
|
||||||
|
(upstream.terrain().routingMultiplier() + downstream.terrain().routingMultiplier()) * 0.5D);
|
||||||
|
double slopeCost = (upstream.terrain().slope() + downstream.terrain().slope())
|
||||||
|
* 0.5D * routing.slopePenalty();
|
||||||
|
double attractionDiscount = routing.confluenceAttraction()
|
||||||
|
* Math.max(0D, upstream.terrain().naturalHeight() - downstream.terrain().naturalHeight());
|
||||||
|
return Math.max(0.001D,
|
||||||
|
(baseDistance + downstreamRise * routing.uphillPenalty() + valleyCost + slopeCost + policyCost)
|
||||||
|
* policyMultiplier - attractionDiscount);
|
||||||
|
}
|
||||||
|
|
||||||
|
int minimumNeighborHeight(HydrologySampledGrid grid, HydrologyGridNode node) {
|
||||||
|
int minimum = node.terrain().naturalHeight();
|
||||||
|
for (HydrologyGridOffset offset : ROUTING_OFFSETS) {
|
||||||
|
HydrologyGridNode neighbor = grid.nodeAt(node.gridX() + offset.x(), node.gridZ() + offset.z());
|
||||||
|
if (neighbor != null && !neighbor.terrain().ocean()) {
|
||||||
|
minimum = Math.min(minimum, neighbor.terrain().naturalHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceSelection selectSources(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
boolean surface,
|
||||||
|
boolean enforceGlobalSpacing,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||||
|
Map<HydrologyTileKey, SourceRoutingContext> routingContexts
|
||||||
|
) {
|
||||||
|
HydrologyPlannerSettings.Source sourceSettings = surface
|
||||||
|
? planner.settings.surface().sources()
|
||||||
|
: planner.settings.underground().sources();
|
||||||
|
if (!sourceSettings.enabled()) {
|
||||||
|
return SourceSelection.empty(surface);
|
||||||
|
}
|
||||||
|
long sourceSalt = surface ? SURFACE_SOURCE_SALT : UNDERGROUND_SOURCE_SALT;
|
||||||
|
ArrayList<SourceCandidate> candidates = new ArrayList<>();
|
||||||
|
boolean hasRequiredCandidate = false;
|
||||||
|
for (HydrologyGridNode node : grid.nodes()) {
|
||||||
|
if (!grid.owns(node.x(), node.z())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyTerrainSample terrain = node.terrain();
|
||||||
|
boolean allowed = surface ? terrain.surfaceSourceAllowed() : terrain.undergroundSourceAllowed();
|
||||||
|
boolean required = surface ? terrain.surfaceSourceRequired() : terrain.undergroundSourceRequired();
|
||||||
|
if (!allowed || surface && terrain.naturalHeight() < sourceSettings.minimumElevation()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double weight = surface ? terrain.surfaceSourceWeight() : terrain.undergroundSourceWeight();
|
||||||
|
if (weight <= 0D && !required) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long stable = sourceStableId(sourceSalt, node.x(), node.z());
|
||||||
|
if (routing.outlets().isEmpty()) {
|
||||||
|
addSourceDiagnostic(node, surface, stable, HydrologyCandidateRejection.NO_LEGAL_OUTLET, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Double.isFinite(routing.potential()[node.index()]) || routing.parent()[node.index()] < 0) {
|
||||||
|
addSourceDiagnostic(node, surface, stable, terrain.confinesKey() != null
|
||||||
|
? HydrologyCandidateRejection.CONFINED_NO_OUTLET
|
||||||
|
: HydrologyCandidateRejection.NO_DRAINAGE_PATH, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!sourceOutletAllowed(node, routing, surface)) {
|
||||||
|
addSourceDiagnostic(node, surface, stable, HydrologyCandidateRejection.NO_LEGAL_OUTLET, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!surface && planner.settings.underground().connectToExistingCaves() && !terrain.caveAvailable()) {
|
||||||
|
addSourceDiagnostic(node, false, stable, HydrologyCandidateRejection.CAVE_CONTAINMENT, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int routeLength = routing.routeLengths()[node.index()];
|
||||||
|
if (routeLength <= 0 || routeLength > planner.settings.routing().maximumRouteLength()) {
|
||||||
|
addSourceDiagnostic(node, surface, stable, HydrologyCandidateRejection.ROUTE_LIMIT, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (routeLength < planner.settings.routing().minimumCourseLength(surface)) {
|
||||||
|
addSourceDiagnostic(node, surface, stable, HydrologyCandidateRejection.COURSE_TOO_SHORT, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Elevation and route length share the same scale: 1000 route blocks count as 100 blocks of height.
|
||||||
|
double score = (required ? 1.0E15D : 0D)
|
||||||
|
+ weight * 1.0E9D
|
||||||
|
+ (terrain.naturalHeight() + routeLength * 0.1D * planner.settings.routing().lengthPreference()) * 1.0E5D
|
||||||
|
+ routing.potential()[node.index()]
|
||||||
|
+ HydrologyHash.unit(stable);
|
||||||
|
candidates.add(new SourceCandidate(node.index(), stable, score, required));
|
||||||
|
hasRequiredCandidate |= required;
|
||||||
|
}
|
||||||
|
int target = expectedCount(sourceSettings.density(), HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
sourceSalt,
|
||||||
|
key.tileX(),
|
||||||
|
key.tileZ()
|
||||||
|
));
|
||||||
|
int requiredMinimum = hasRequiredCandidate
|
||||||
|
? Math.max(1, sourceSettings.minimumPerTile())
|
||||||
|
: 0;
|
||||||
|
target = Math.max(requiredMinimum, target);
|
||||||
|
int maximum = hasRequiredCandidate
|
||||||
|
? Math.max(1, sourceSettings.maximumPerTile())
|
||||||
|
: sourceSettings.maximumPerTile();
|
||||||
|
target = Math.min(maximum, target);
|
||||||
|
int tributaries = surface ? planner.settings.routing().tributaries() : planner.settings.underground().tributaries();
|
||||||
|
int coursesPerOutlet = 1 + tributaries;
|
||||||
|
// Tributaries are budgeted on top of the source density: every outlet may draw its extra courses
|
||||||
|
// beyond the tile's expected count, or no junction would ever fit inside a one-source budget.
|
||||||
|
int tributaryBudget = Math.multiplyExact(routing.outlets().size(), tributaries);
|
||||||
|
target = Math.min(Math.addExact(maximum, tributaryBudget), Math.addExact(target, tributaryBudget));
|
||||||
|
target = effectiveSourceTarget(surface && !enforceGlobalSpacing, target, routing.outlets().size(), coursesPerOutlet);
|
||||||
|
Comparator<SourceCandidate> candidateOrder = Comparator
|
||||||
|
.comparing(SourceCandidate::required)
|
||||||
|
.reversed();
|
||||||
|
candidates.sort(candidateOrder
|
||||||
|
.thenComparing(Comparator.comparingDouble(SourceCandidate::score).reversed())
|
||||||
|
.thenComparingLong(SourceCandidate::stableId));
|
||||||
|
prioritizeInlandSource(candidates, routing);
|
||||||
|
int guaranteed = Math.min(target, requiredMinimum);
|
||||||
|
int maximumCoursesPerOutlet = coursesPerOutlet;
|
||||||
|
SourceAdmissionSelection admission = selectSourceAdmissionsByOutlet(
|
||||||
|
candidates,
|
||||||
|
routing,
|
||||||
|
target,
|
||||||
|
guaranteed,
|
||||||
|
maximumCoursesPerOutlet,
|
||||||
|
(int candidateIndex) -> {
|
||||||
|
if (!enforceGlobalSpacing) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
SourceCandidate candidate = candidates.get(candidateIndex);
|
||||||
|
return globallyAdmittedSource(
|
||||||
|
grid.node(candidate.nodeIndex()),
|
||||||
|
sourceSettings,
|
||||||
|
sourceSalt,
|
||||||
|
surface,
|
||||||
|
routingContexts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
int rejectionsPerTarget = surface
|
||||||
|
? SURFACE_OPTIONAL_SOURCE_REJECTIONS_PER_TARGET
|
||||||
|
: OPTIONAL_SOURCE_REJECTIONS_PER_TARGET;
|
||||||
|
int maximumOptionalRejections = Math.max(
|
||||||
|
rejectionsPerTarget,
|
||||||
|
target * rejectionsPerTarget
|
||||||
|
);
|
||||||
|
int admittedGuaranteed = Math.min(guaranteed, admission.selectedCandidateIndices().size());
|
||||||
|
return new SourceSelection(
|
||||||
|
surface,
|
||||||
|
candidates,
|
||||||
|
admission,
|
||||||
|
admittedGuaranteed,
|
||||||
|
maximumOptionalRejections
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int effectiveSourceTarget(boolean outletBounded, int requestedTarget, int outletCount) {
|
||||||
|
return effectiveSourceTarget(outletBounded, requestedTarget, outletCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int effectiveSourceTarget(boolean outletBounded, int requestedTarget, int outletCount, int coursesPerOutlet) {
|
||||||
|
if (requestedTarget < 0 || outletCount < 0 || coursesPerOutlet < 1) {
|
||||||
|
throw new IllegalArgumentException("Source target bounds cannot be negative.");
|
||||||
|
}
|
||||||
|
long bound = (long) outletCount * coursesPerOutlet;
|
||||||
|
return outletBounded ? (int) Math.min(requestedTarget, bound) : requestedTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceAdmissionSelection selectSourceAdmissionsByOutlet(
|
||||||
|
List<SourceCandidate> candidates,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
int target,
|
||||||
|
int guaranteed,
|
||||||
|
int maximumCoursesPerOutlet,
|
||||||
|
IntPredicate globallyAdmitted
|
||||||
|
) {
|
||||||
|
int candidateCount = candidates.size();
|
||||||
|
ArrayList<Integer> selected = new ArrayList<>(Math.min(candidateCount, target));
|
||||||
|
boolean[] selectedCandidates = new boolean[candidateCount];
|
||||||
|
boolean[] spacingRejectedCandidates = new boolean[candidateCount];
|
||||||
|
boolean[] evaluatedCandidates = new boolean[candidateCount];
|
||||||
|
int[] outletIndices = new int[candidateCount];
|
||||||
|
HashMap<Integer, Integer> selectedByOutlet = new HashMap<>();
|
||||||
|
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++) {
|
||||||
|
outletIndices[candidateIndex] = routing.outletIndex()[candidates.get(candidateIndex).nodeIndex()];
|
||||||
|
}
|
||||||
|
for (int candidateIndex = 0;
|
||||||
|
candidateIndex < candidateCount && selected.size() < target;
|
||||||
|
candidateIndex++) {
|
||||||
|
int outletIndex = outletIndices[candidateIndex];
|
||||||
|
if (selectedByOutlet.getOrDefault(outletIndex, 0) >= maximumCoursesPerOutlet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
evaluatedCandidates[candidateIndex] = true;
|
||||||
|
if (!globallyAdmitted.test(candidateIndex)) {
|
||||||
|
spacingRejectedCandidates[candidateIndex] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selected.add(candidateIndex);
|
||||||
|
selectedCandidates[candidateIndex] = true;
|
||||||
|
selectedByOutlet.merge(outletIndex, 1, Math::addExact);
|
||||||
|
}
|
||||||
|
for (int candidateIndex = 0;
|
||||||
|
candidateIndex < candidateCount && selected.size() < guaranteed;
|
||||||
|
candidateIndex++) {
|
||||||
|
if (selectedCandidates[candidateIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int outletIndex = outletIndices[candidateIndex];
|
||||||
|
if (selectedByOutlet.getOrDefault(outletIndex, 0) >= maximumCoursesPerOutlet) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selected.add(candidateIndex);
|
||||||
|
selectedCandidates[candidateIndex] = true;
|
||||||
|
spacingRejectedCandidates[candidateIndex] = false;
|
||||||
|
selectedByOutlet.merge(outletIndex, 1, Math::addExact);
|
||||||
|
}
|
||||||
|
return new SourceAdmissionSelection(
|
||||||
|
target,
|
||||||
|
selected,
|
||||||
|
selectedCandidates,
|
||||||
|
spacingRejectedCandidates,
|
||||||
|
evaluatedCandidates,
|
||||||
|
globallyAdmitted,
|
||||||
|
outletIndices,
|
||||||
|
maximumCoursesPerOutlet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void prioritizeInlandSource(List<SourceCandidate> candidates, HydrologyRoutingPlan routing) {
|
||||||
|
if (candidates.size() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean required = candidates.getFirst().required();
|
||||||
|
for (int candidateIndex = 0; candidateIndex < candidates.size(); candidateIndex++) {
|
||||||
|
SourceCandidate candidate = candidates.get(candidateIndex);
|
||||||
|
if (candidate.required() != required) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!isInlandSource(candidate, routing)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (candidateIndex > 0) {
|
||||||
|
candidates.addFirst(candidates.remove(candidateIndex));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isInlandSource(SourceCandidate candidate, HydrologyRoutingPlan routing) {
|
||||||
|
if (candidate == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int outletIndex = routing.outletIndex()[candidate.nodeIndex()];
|
||||||
|
return outletIndex >= 0
|
||||||
|
&& routing.outlets().get(outletIndex).outlet().type() == HydrologyFeatureType.INLAND_GROTTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SourceAdmissionSelection selectSourceAdmissions(
|
||||||
|
int candidateCount,
|
||||||
|
int target,
|
||||||
|
int guaranteed,
|
||||||
|
IntPredicate globallyAdmitted
|
||||||
|
) {
|
||||||
|
if (candidateCount < 0 || target < 0 || guaranteed < 0 || guaranteed > target) {
|
||||||
|
throw new IllegalArgumentException("Source admission bounds are invalid.");
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(globallyAdmitted, "globallyAdmitted");
|
||||||
|
ArrayList<Integer> selected = new ArrayList<>(Math.min(candidateCount, target));
|
||||||
|
boolean[] selectedCandidates = new boolean[candidateCount];
|
||||||
|
boolean[] spacingRejectedCandidates = new boolean[candidateCount];
|
||||||
|
boolean[] evaluatedCandidates = new boolean[candidateCount];
|
||||||
|
for (int candidateIndex = 0;
|
||||||
|
candidateIndex < candidateCount && selected.size() < target;
|
||||||
|
candidateIndex++) {
|
||||||
|
evaluatedCandidates[candidateIndex] = true;
|
||||||
|
if (!globallyAdmitted.test(candidateIndex)) {
|
||||||
|
spacingRejectedCandidates[candidateIndex] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selected.add(candidateIndex);
|
||||||
|
selectedCandidates[candidateIndex] = true;
|
||||||
|
}
|
||||||
|
for (int candidateIndex = 0;
|
||||||
|
candidateIndex < candidateCount && selected.size() < guaranteed;
|
||||||
|
candidateIndex++) {
|
||||||
|
if (selectedCandidates[candidateIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selected.add(candidateIndex);
|
||||||
|
selectedCandidates[candidateIndex] = true;
|
||||||
|
spacingRejectedCandidates[candidateIndex] = false;
|
||||||
|
}
|
||||||
|
return new SourceAdmissionSelection(
|
||||||
|
target,
|
||||||
|
selected,
|
||||||
|
selectedCandidates,
|
||||||
|
spacingRejectedCandidates,
|
||||||
|
evaluatedCandidates,
|
||||||
|
globallyAdmitted,
|
||||||
|
new int[candidateCount],
|
||||||
|
Integer.MAX_VALUE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean globallyAdmittedSource(
|
||||||
|
HydrologyGridNode candidate,
|
||||||
|
HydrologyPlannerSettings.Source sourceSettings,
|
||||||
|
long sourceSalt,
|
||||||
|
boolean surface,
|
||||||
|
Map<HydrologyTileKey, SourceRoutingContext> routingContexts
|
||||||
|
) {
|
||||||
|
int minimumSpacing = sourceSettings.minimumSpacing();
|
||||||
|
boolean required = surface
|
||||||
|
? candidate.terrain().surfaceSourceRequired()
|
||||||
|
: candidate.terrain().undergroundSourceRequired();
|
||||||
|
if (minimumSpacing <= 0 || required) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
SourcePriority candidatePriority = sourcePriority(candidate.x(), candidate.z(), candidate.terrain(), sourceSalt, surface);
|
||||||
|
int sampleSpacing = planner.settings.routing().sampleSpacing();
|
||||||
|
int latticeRadius = (int) StrictMath.ceil(minimumSpacing / (double) sampleSpacing);
|
||||||
|
for (int offsetZ = -latticeRadius; offsetZ <= latticeRadius; offsetZ++) {
|
||||||
|
for (int offsetX = -latticeRadius; offsetX <= latticeRadius; offsetX++) {
|
||||||
|
if (offsetX == 0 && offsetZ == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long deltaX = (long) offsetX * sampleSpacing;
|
||||||
|
long deltaZ = (long) offsetZ * sampleSpacing;
|
||||||
|
if (StrictMath.hypot(deltaX, deltaZ) >= minimumSpacing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long neighborX = (long) candidate.x() + deltaX;
|
||||||
|
long neighborZ = (long) candidate.z() + deltaZ;
|
||||||
|
if (neighborX < Integer.MIN_VALUE || neighborX > Integer.MAX_VALUE
|
||||||
|
|| neighborZ < Integer.MIN_VALUE || neighborZ > Integer.MAX_VALUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int x = (int) neighborX;
|
||||||
|
int z = (int) neighborZ;
|
||||||
|
HydrologyTileKey owner = HydrologyTileKey.fromBlock(x, z, planner.settings.routing().tileSize());
|
||||||
|
SourceRoutingContext context = routingContexts.computeIfAbsent(owner, planner::sourceRoutingContext);
|
||||||
|
HydrologyGridNode neighbor = context.grid().nodeAtWorld(x, z);
|
||||||
|
if (neighbor == null) {
|
||||||
|
throw new IllegalStateException("Source coordinate is absent from its owner routing lattice.");
|
||||||
|
}
|
||||||
|
HydrologyTerrainSample terrain = neighbor.terrain();
|
||||||
|
if (!rawSourceEligible(terrain, sourceSettings, surface)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!routeViable(neighbor, context, surface)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SourcePriority neighborPriority = sourcePriority(x, z, terrain, sourceSalt, surface);
|
||||||
|
if (compareSourcePriority(neighborPriority, candidatePriority) > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceRoutingContext compileSourceRoutingContext(HydrologyTileKey key) {
|
||||||
|
HydrologySampledGrid grid = planner.outletPlanner.sample(key);
|
||||||
|
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||||
|
List<OutletCandidate> surfaceOutlets = planner.outletPlanner.resolveOutlets(key, grid, true, diagnostics);
|
||||||
|
HydrologyRoutingPlan surfaceRouting = buildRouting(grid, surfaceOutlets, true);
|
||||||
|
List<OutletCandidate> undergroundOutlets = planner.settings.underground().enabled()
|
||||||
|
? planner.outletPlanner.resolveOutlets(key, grid, false, diagnostics)
|
||||||
|
: surfaceOutlets;
|
||||||
|
HydrologyRoutingPlan undergroundRouting = planner.settings.underground().enabled()
|
||||||
|
? buildRouting(grid, undergroundOutlets, false)
|
||||||
|
: surfaceRouting;
|
||||||
|
return new SourceRoutingContext(
|
||||||
|
grid,
|
||||||
|
surfaceRouting,
|
||||||
|
undergroundRouting,
|
||||||
|
List.copyOf(diagnostics)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean routeViable(HydrologyGridNode node, SourceRoutingContext context, boolean surface) {
|
||||||
|
HydrologyRoutingPlan routing = context.routing(surface);
|
||||||
|
if (routing.outlets().isEmpty()
|
||||||
|
|| !Double.isFinite(routing.potential()[node.index()])
|
||||||
|
|| routing.parent()[node.index()] < 0
|
||||||
|
|| !sourceOutletAllowed(node, routing, surface)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int routeLength = routing.routeLengths()[node.index()];
|
||||||
|
return routeLength >= planner.settings.routing().minimumCourseLength(surface)
|
||||||
|
&& routeLength <= planner.settings.routing().maximumRouteLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean sourceOutletAllowed(HydrologyGridNode node, HydrologyRoutingPlan routing, boolean surface) {
|
||||||
|
int outletIndex = routing.outletIndex()[node.index()];
|
||||||
|
if (outletIndex < 0 || outletIndex >= routing.outlets().size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RiverOutlet outlet = routing.outlets().get(outletIndex).outlet();
|
||||||
|
return !surface
|
||||||
|
|| outlet.type() != HydrologyFeatureType.INLAND_GROTTO
|
||||||
|
|| planner.settings.outlets().surfaceSinkholesEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean rawSourceEligible(
|
||||||
|
HydrologyTerrainSample terrain,
|
||||||
|
HydrologyPlannerSettings.Source sourceSettings,
|
||||||
|
boolean surface
|
||||||
|
) {
|
||||||
|
if (terrain.ocean() || !terrain.transitAllowed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean allowed = surface ? terrain.surfaceSourceAllowed() : terrain.undergroundSourceAllowed();
|
||||||
|
boolean required = surface ? terrain.surfaceSourceRequired() : terrain.undergroundSourceRequired();
|
||||||
|
if (!allowed || surface && terrain.naturalHeight() < sourceSettings.minimumElevation()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!surface && planner.settings.underground().connectToExistingCaves() && !terrain.caveAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double weight = surface ? terrain.surfaceSourceWeight() : terrain.undergroundSourceWeight();
|
||||||
|
return required || weight > 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourcePriority sourcePriority(
|
||||||
|
int x,
|
||||||
|
int z,
|
||||||
|
HydrologyTerrainSample terrain,
|
||||||
|
long sourceSalt,
|
||||||
|
boolean surface
|
||||||
|
) {
|
||||||
|
return new SourcePriority(
|
||||||
|
surface ? terrain.surfaceSourceRequired() : terrain.undergroundSourceRequired(),
|
||||||
|
surface ? terrain.surfaceSourceWeight() : terrain.undergroundSourceWeight(),
|
||||||
|
terrain.naturalHeight(),
|
||||||
|
sourceStableId(sourceSalt, x, z),
|
||||||
|
x,
|
||||||
|
z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long sourceStableId(long sourceSalt, int x, int z) {
|
||||||
|
return HydrologyHash.mix(planner.worldSeed, sourceSalt, x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareSourcePriority(SourcePriority first, SourcePriority second) {
|
||||||
|
int requiredComparison = Boolean.compare(first.required(), second.required());
|
||||||
|
if (requiredComparison != 0) {
|
||||||
|
return requiredComparison;
|
||||||
|
}
|
||||||
|
int weightComparison = Double.compare(first.weight(), second.weight());
|
||||||
|
if (weightComparison != 0) {
|
||||||
|
return weightComparison;
|
||||||
|
}
|
||||||
|
int stableComparison = Long.compareUnsigned(first.stableId(), second.stableId());
|
||||||
|
if (stableComparison != 0) {
|
||||||
|
return stableComparison;
|
||||||
|
}
|
||||||
|
int heightComparison = Integer.compare(first.naturalHeight(), second.naturalHeight());
|
||||||
|
if (heightComparison != 0) {
|
||||||
|
return heightComparison;
|
||||||
|
}
|
||||||
|
int xComparison = Integer.compare(first.x(), second.x());
|
||||||
|
return xComparison != 0 ? xComparison : Integer.compare(first.z(), second.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addSourceDiagnostic(
|
||||||
|
HydrologyGridNode node,
|
||||||
|
boolean surface,
|
||||||
|
long stableId,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
int y = surface ? node.terrain().naturalHeight() : node.terrain().caveFluidY();
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(stableId, DIAGNOSTIC_SALT, rejection.ordinal()),
|
||||||
|
HydrologyCandidateKind.SOURCE,
|
||||||
|
surface ? HydrologyFeatureType.SURFACE_POOL : HydrologyFeatureType.UNDERGROUND_POOL,
|
||||||
|
new HydrologyPoint(node.x(), y, node.z()),
|
||||||
|
rejection, 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
int expectedCount(double density, long stable) {
|
||||||
|
int count = (int) StrictMath.floor(density);
|
||||||
|
double remainder = density - count;
|
||||||
|
return count + (HydrologyHash.unit(stable) < remainder ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int[] routeLengths(int[] parent, int width, HydrologyPlannerSettings.Routing routing) {
|
||||||
|
int[] lengths = new int[parent.length];
|
||||||
|
int[] pending = new int[parent.length];
|
||||||
|
double[] distances = new double[parent.length];
|
||||||
|
Arrays.fill(distances, Double.NaN);
|
||||||
|
double diagonalDistance = StrictMath.hypot(routing.sampleSpacing(), routing.sampleSpacing());
|
||||||
|
for (int sourceIndex = 0; sourceIndex < parent.length; sourceIndex++) {
|
||||||
|
if (!Double.isNaN(distances[sourceIndex])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int current = sourceIndex;
|
||||||
|
int pendingCount = 0;
|
||||||
|
while (current >= 0 && Double.isNaN(distances[current])) {
|
||||||
|
pending[pendingCount++] = current;
|
||||||
|
distances[current] = -1D;
|
||||||
|
current = parent[current];
|
||||||
|
}
|
||||||
|
if (current >= 0 && distances[current] < 0D) {
|
||||||
|
throw new IllegalStateException("Hydrology drainage routing contains a cycle.");
|
||||||
|
}
|
||||||
|
double distance = current < 0 ? 0D : distances[current];
|
||||||
|
while (pendingCount > 0) {
|
||||||
|
int node = pending[--pendingCount];
|
||||||
|
int next = parent[node];
|
||||||
|
if (next >= 0) {
|
||||||
|
boolean cardinal = node / width == next / width || node % width == next % width;
|
||||||
|
distance += cardinal ? routing.sampleSpacing() : diagonalDistance;
|
||||||
|
}
|
||||||
|
distances[node] = distance;
|
||||||
|
lengths[node] = distance > routing.maximumRouteLength()
|
||||||
|
? Integer.MAX_VALUE
|
||||||
|
: (int) StrictMath.ceil(distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lengths;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] countContributions(List<Integer> sources, HydrologyRoutingPlan routing) {
|
||||||
|
int[] counts = new int[routing.parent().length];
|
||||||
|
for (int source : sources) {
|
||||||
|
int current = source;
|
||||||
|
int steps = 0;
|
||||||
|
while (routing.parent()[current] >= 0 && steps++ < planner.settings.routing().maximumRouteNodes()) {
|
||||||
|
counts[current]++;
|
||||||
|
current = routing.parent()[current];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompiledGraph compileGraph(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
int[] surfaceContributions,
|
||||||
|
int[] undergroundContributions,
|
||||||
|
Map<Long, List<HydrologyPoint>> refinedEdges
|
||||||
|
) {
|
||||||
|
HashSet<Integer> includedNodeIndices = new HashSet<>();
|
||||||
|
ArrayList<DrainageEdge> edges = new ArrayList<>();
|
||||||
|
HashMap<Integer, DrainageEdge> edgeByUpstream = new HashMap<>();
|
||||||
|
for (HydrologyGridNode upstream : grid.nodes()) {
|
||||||
|
int downstreamIndex = routing.parent()[upstream.index()];
|
||||||
|
if (downstreamIndex < 0 || !Double.isFinite(routing.potential()[upstream.index()])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean acceptedCourseEdge = surfaceContributions[upstream.index()] > 0
|
||||||
|
|| undergroundContributions[upstream.index()] > 0;
|
||||||
|
if (!acceptedCourseEdge) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyGridNode downstream = grid.node(downstreamIndex);
|
||||||
|
RiverOutlet outlet = routing.outlets().get(routing.outletIndex()[upstream.index()]).outlet();
|
||||||
|
long edgeId = HydrologyHash.mix(planner.worldSeed, EDGE_SALT, upstream.id(), downstream.id(), outlet.id());
|
||||||
|
List<HydrologyPoint> centerline = refinedEdges.get(edgeId);
|
||||||
|
int transverseCandidates = HydrologyRouteGeometry.ROUTE_TRANSVERSE_CANDIDATES;
|
||||||
|
if (centerline == null) {
|
||||||
|
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream);
|
||||||
|
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream);
|
||||||
|
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet);
|
||||||
|
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
|
||||||
|
upstream.id(),
|
||||||
|
downstream.id(),
|
||||||
|
upstreamAnchor.x(),
|
||||||
|
upstreamAnchor.z(),
|
||||||
|
downstreamAnchor.x(),
|
||||||
|
downstreamAnchor.z(),
|
||||||
|
continuation.x(),
|
||||||
|
continuation.z(),
|
||||||
|
transverseCandidates
|
||||||
|
);
|
||||||
|
centerline = planner.refinedEdgeCache.get(
|
||||||
|
refinedEdgeKey,
|
||||||
|
ignored -> planner.routeGeometry.refineEdge(
|
||||||
|
upstream.id(),
|
||||||
|
downstream.id(),
|
||||||
|
upstreamAnchor,
|
||||||
|
downstreamAnchor,
|
||||||
|
continuation,
|
||||||
|
transverseCandidates
|
||||||
|
)
|
||||||
|
);
|
||||||
|
refinedEdges.put(edgeId, centerline);
|
||||||
|
}
|
||||||
|
if (centerline.isEmpty()) {
|
||||||
|
HydrologyPoint upstreamAnchor = planner.routeGeometry.routeAnchor(upstream);
|
||||||
|
HydrologyPoint downstreamAnchor = planner.routeGeometry.routeAnchor(downstream);
|
||||||
|
HydrologyPoint continuation = planner.routeGeometry.edgeContinuation(grid, routing, downstream, outlet);
|
||||||
|
RefinedEdgeKey refinedEdgeKey = new RefinedEdgeKey(
|
||||||
|
upstream.id(),
|
||||||
|
downstream.id(),
|
||||||
|
upstreamAnchor.x(),
|
||||||
|
upstreamAnchor.z(),
|
||||||
|
downstreamAnchor.x(),
|
||||||
|
downstreamAnchor.z(),
|
||||||
|
continuation.x(),
|
||||||
|
continuation.z(),
|
||||||
|
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES
|
||||||
|
);
|
||||||
|
centerline = planner.refinedEdgeCache.get(
|
||||||
|
refinedEdgeKey,
|
||||||
|
ignored -> planner.routeGeometry.refineEdge(
|
||||||
|
upstream.id(),
|
||||||
|
downstream.id(),
|
||||||
|
upstreamAnchor,
|
||||||
|
downstreamAnchor,
|
||||||
|
continuation,
|
||||||
|
HydrologyRouteGeometry.ROUTE_FALLBACK_TRANSVERSE_CANDIDATES
|
||||||
|
)
|
||||||
|
);
|
||||||
|
refinedEdges.put(edgeId, centerline);
|
||||||
|
}
|
||||||
|
if (centerline.isEmpty()) {
|
||||||
|
centerline = planner.routeGeometry.constrainedTerrainFallbackEdge(
|
||||||
|
planner.routeGeometry.routeAnchor(upstream),
|
||||||
|
planner.routeGeometry.routeAnchor(downstream)
|
||||||
|
);
|
||||||
|
refinedEdges.put(edgeId, centerline);
|
||||||
|
}
|
||||||
|
if (centerline.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double cost = routing.potential()[upstream.index()] - routing.potential()[downstreamIndex];
|
||||||
|
DrainageEdge edge = new DrainageEdge(
|
||||||
|
edgeId,
|
||||||
|
upstream.id(),
|
||||||
|
downstream.id(),
|
||||||
|
outlet.id(),
|
||||||
|
cost,
|
||||||
|
surfaceContributions[upstream.index()],
|
||||||
|
undergroundContributions[upstream.index()],
|
||||||
|
centerline
|
||||||
|
);
|
||||||
|
edges.add(edge);
|
||||||
|
edgeByUpstream.put(upstream.index(), edge);
|
||||||
|
includedNodeIndices.add(upstream.index());
|
||||||
|
includedNodeIndices.add(downstream.index());
|
||||||
|
}
|
||||||
|
ArrayList<DrainageNode> nodes = new ArrayList<>(includedNodeIndices.size());
|
||||||
|
for (HydrologyGridNode node : grid.nodes()) {
|
||||||
|
if (!includedNodeIndices.contains(node.index())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int outletIndex = routing.outletIndex()[node.index()];
|
||||||
|
RiverOutlet outlet = routing.outlets().get(outletIndex).outlet();
|
||||||
|
HydrologyPoint anchor = planner.routeGeometry.routeAnchor(node);
|
||||||
|
HydrologyTerrainSample terrain = Objects.requireNonNull(
|
||||||
|
planner.sampleLandBasis(anchor.x(), anchor.z()),
|
||||||
|
"Hydrology route anchor left natural land"
|
||||||
|
);
|
||||||
|
nodes.add(new DrainageNode(
|
||||||
|
node.id(),
|
||||||
|
anchor.x(),
|
||||||
|
anchor.z(),
|
||||||
|
terrain,
|
||||||
|
routing.potential()[node.index()],
|
||||||
|
outlet.id()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ArrayList<RiverOutlet> outlets = new ArrayList<>(routing.outlets().size());
|
||||||
|
for (OutletCandidate outletCandidate : routing.outlets()) {
|
||||||
|
outlets.add(outletCandidate.outlet());
|
||||||
|
}
|
||||||
|
return new CompiledGraph(List.copyOf(nodes), List.copyOf(edges), List.copyOf(outlets), Map.copyOf(edgeByUpstream));
|
||||||
|
}
|
||||||
|
|
||||||
|
CompiledGraph mergeGraphs(CompiledGraph first, CompiledGraph second) {
|
||||||
|
LinkedHashMap<Long, DrainageNode> nodes = new LinkedHashMap<>();
|
||||||
|
for (DrainageNode node : first.nodes()) {
|
||||||
|
nodes.put(node.id(), node);
|
||||||
|
}
|
||||||
|
for (DrainageNode node : second.nodes()) {
|
||||||
|
nodes.putIfAbsent(node.id(), node);
|
||||||
|
}
|
||||||
|
LinkedHashMap<Long, DrainageEdge> edges = new LinkedHashMap<>();
|
||||||
|
for (DrainageEdge edge : first.edges()) {
|
||||||
|
edges.put(edge.id(), edge);
|
||||||
|
}
|
||||||
|
for (DrainageEdge edge : second.edges()) {
|
||||||
|
DrainageNode upstream = nodes.get(edge.upstreamNodeId());
|
||||||
|
DrainageNode downstream = nodes.get(edge.downstreamNodeId());
|
||||||
|
if (upstream != null
|
||||||
|
&& downstream != null
|
||||||
|
&& downstream.potential() < upstream.potential()) {
|
||||||
|
edges.putIfAbsent(edge.id(), edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkedHashMap<Long, RiverOutlet> outlets = new LinkedHashMap<>();
|
||||||
|
for (RiverOutlet outlet : first.outlets()) {
|
||||||
|
outlets.put(outlet.id(), outlet);
|
||||||
|
}
|
||||||
|
for (RiverOutlet outlet : second.outlets()) {
|
||||||
|
outlets.putIfAbsent(outlet.id(), outlet);
|
||||||
|
}
|
||||||
|
HashMap<Integer, DrainageEdge> edgeByUpstream = new HashMap<>(first.edgeByUpstream());
|
||||||
|
for (Map.Entry<Integer, DrainageEdge> entry : second.edgeByUpstream().entrySet()) {
|
||||||
|
if (entry.getValue().equals(edges.get(entry.getValue().id()))) {
|
||||||
|
edgeByUpstream.putIfAbsent(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new CompiledGraph(
|
||||||
|
List.copyOf(nodes.values()),
|
||||||
|
List.copyOf(edges.values()),
|
||||||
|
List.copyOf(outlets.values()),
|
||||||
|
Map.copyOf(edgeByUpstream)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record SourcePriority(
|
||||||
|
boolean required,
|
||||||
|
double weight,
|
||||||
|
int naturalHeight,
|
||||||
|
long stableId,
|
||||||
|
int x,
|
||||||
|
int z
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+937
@@ -0,0 +1,937 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
|
final class HydrologySurfaceCoursePlanner {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologySurfaceCoursePlanner(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final long COURSE_SALT = 0x434f55525345L;
|
||||||
|
static final double SURFACE_MAXIMUM_RENDERED_TURN_DEGREES = 50D;
|
||||||
|
static final double SURFACE_MAXIMUM_ISOLATED_TURN_DEGREES = 35D;
|
||||||
|
static final double SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES = 10D;
|
||||||
|
static final double SURFACE_MAXIMUM_P95_TURN_DEGREES = 35D;
|
||||||
|
static final double SURFACE_MAXIMUM_SINUOSITY = 1.45D;
|
||||||
|
static final double SURFACE_GRID_LOCK_TOLERANCE_DEGREES = 5D;
|
||||||
|
static final double SURFACE_MAXIMUM_GRID_LOCKED_FRACTION = 0.5D;
|
||||||
|
static final double SURFACE_MAXIMUM_GRID_LOCKED_RUN = 64D;
|
||||||
|
static final double SURFACE_MAXIMUM_WIDTH_SCALE = 1.18D;
|
||||||
|
|
||||||
|
void compileSurfaceCourses(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
CompiledGraph graph,
|
||||||
|
List<Integer> sources,
|
||||||
|
List<RiverCourse> courses,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
LinkedHashMap<Long, ArrayList<SurfaceCourseDraft>> draftsByOutlet = new LinkedHashMap<>();
|
||||||
|
for (int source : sources) {
|
||||||
|
HydrologyGridNode sourceNode = grid.node(source);
|
||||||
|
long sourceCourseId = HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
COURSE_SALT,
|
||||||
|
HydrologySourcePlanner.SURFACE_SOURCE_SALT,
|
||||||
|
sourceNode.id()
|
||||||
|
);
|
||||||
|
HydrologyCoursePath path = coursePath(grid, routing, graph, source, true);
|
||||||
|
if (path == null) {
|
||||||
|
planner.undergroundCourses.addCompiledSourceDiagnostic(
|
||||||
|
sourceNode,
|
||||||
|
true,
|
||||||
|
sourceCourseId,
|
||||||
|
HydrologyCandidateRejection.NO_DRAINAGE_PATH,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long courseId = HydrologyHash.mix(sourceCourseId, path.outlet().id());
|
||||||
|
HydrologyPoint trunkPoint = path.points().getFirst();
|
||||||
|
HydrologyTerrainSample trunkTerrain = Objects.requireNonNull(
|
||||||
|
planner.sampleDetailed(trunkPoint.x(), trunkPoint.z()),
|
||||||
|
"Hydrology surface trunk left sampled terrain"
|
||||||
|
);
|
||||||
|
String profileKey = planner.segments.chooseProfile(trunkTerrain, path.outlet().id());
|
||||||
|
draftsByOutlet.computeIfAbsent(path.outlet().id(), (Long ignored) -> new ArrayList<>())
|
||||||
|
.add(new SurfaceCourseDraft(sourceNode, courseId, profileKey, path));
|
||||||
|
}
|
||||||
|
for (ArrayList<SurfaceCourseDraft> outletDrafts : draftsByOutlet.values()) {
|
||||||
|
outletDrafts.sort(Comparator
|
||||||
|
.comparingDouble((SurfaceCourseDraft draft) -> planner.tributaries.edgeLength(
|
||||||
|
draft.path().edges(),
|
||||||
|
draft.path().edges().size()
|
||||||
|
))
|
||||||
|
.reversed()
|
||||||
|
.thenComparingLong(SurfaceCourseDraft::courseId));
|
||||||
|
RiverCourse mainCourse = null;
|
||||||
|
HashMap<Long, SurfaceCourseBuild> mainRejections = new HashMap<>();
|
||||||
|
int tributaries = 0;
|
||||||
|
for (SurfaceCourseDraft draft : outletDrafts) {
|
||||||
|
if (mainCourse != null && tributaries >= planner.settings.routing().tributaries()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mainCourse == null) {
|
||||||
|
SurfaceCourseBuild candidate = buildSurfaceCourse(
|
||||||
|
draft.courseId(),
|
||||||
|
draft.profileKey(),
|
||||||
|
draft.source(),
|
||||||
|
draft.path()
|
||||||
|
);
|
||||||
|
if (candidate.course() == null) {
|
||||||
|
mainRejections.put(draft.courseId(), candidate);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mainCourse = candidate.course();
|
||||||
|
courses.add(mainCourse);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RiverCourse tributary = planner.tributaries.buildSurfaceTributary(draft, mainCourse, diagnostics);
|
||||||
|
if (tributary != null) {
|
||||||
|
courses.add(tributary);
|
||||||
|
tributaries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mainCourse == null) {
|
||||||
|
for (SurfaceCourseDraft draft : outletDrafts) {
|
||||||
|
SurfaceCourseBuild rejected = mainRejections.get(draft.courseId());
|
||||||
|
planner.undergroundCourses.addCompiledSourceDiagnostic(
|
||||||
|
draft.source(),
|
||||||
|
true,
|
||||||
|
draft.courseId(),
|
||||||
|
rejected.rejection(),
|
||||||
|
rejected.rejectionDetail(),
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceCourseBuild buildSurfaceCourse(
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
HydrologyGridNode source,
|
||||||
|
HydrologyCoursePath initialPath
|
||||||
|
) {
|
||||||
|
return buildSurfaceCourseAtPath(courseId, profileKey, source, initialPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceCourseBuild buildSurfaceCourseAtPath(
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
HydrologyGridNode source,
|
||||||
|
HydrologyCoursePath path
|
||||||
|
) {
|
||||||
|
if (path.points().size() < 2) {
|
||||||
|
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, 0);
|
||||||
|
}
|
||||||
|
RiverOutlet outlet = path.outlet();
|
||||||
|
boolean surfaceSinkhole = path.reachesOutlet() && planner.segments.surfaceSinkhole(outlet);
|
||||||
|
SurfaceTerminal terminal;
|
||||||
|
if (!path.reachesOutlet() || outlet.type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||||
|
terminal = SurfaceTerminal.SINKHOLE;
|
||||||
|
} else if (outlet.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||||
|
terminal = SurfaceTerminal.COASTAL_GROTTO;
|
||||||
|
} else {
|
||||||
|
terminal = SurfaceTerminal.OCEAN_MOUTH;
|
||||||
|
}
|
||||||
|
int outletHead = planner.segments.outletHead(outlet);
|
||||||
|
int terminalHead = surfaceSinkhole ? Math.addExact(outletHead, 1) : outletHead;
|
||||||
|
SurfaceCourseResult result = planner.surfaceCourseBuilder.build(
|
||||||
|
planner.worldSeed,
|
||||||
|
courseId,
|
||||||
|
profileKey,
|
||||||
|
path.points(),
|
||||||
|
terminal,
|
||||||
|
terminalHead
|
||||||
|
);
|
||||||
|
if (!result.accepted()) {
|
||||||
|
return SurfaceCourseBuild.rejected(result.rejection(), result.rejectionDetail());
|
||||||
|
}
|
||||||
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
|
||||||
|
if (path.reachesOutlet()) {
|
||||||
|
HydrologyCoursePath terminalPath = new HydrologyCoursePath(
|
||||||
|
List.of(result.pathEnd()),
|
||||||
|
path.pairEdges(),
|
||||||
|
path.edges(),
|
||||||
|
outlet,
|
||||||
|
true,
|
||||||
|
path.organicSurfaceRequired(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
planner.segments.appendOutletSegments(
|
||||||
|
RiverCourseType.SURFACE,
|
||||||
|
courseId,
|
||||||
|
terminalPath,
|
||||||
|
result.lastHead(),
|
||||||
|
new int[] {result.lastWidth()},
|
||||||
|
new int[] {result.lastDepth()},
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (path.organicSurfaceRequired()) {
|
||||||
|
int shapeRejection = surfaceShapeRejection(path.points());
|
||||||
|
if (shapeRejection != 0) {
|
||||||
|
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, shapeRejection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int discharge = planner.segments.maximumSurfaceDischarge(path.edges());
|
||||||
|
return SurfaceCourseBuild.accepted(new RiverCourse(
|
||||||
|
courseId,
|
||||||
|
RiverCourseType.SURFACE,
|
||||||
|
OptionalLong.of(source.id()),
|
||||||
|
OptionalLong.of(outlet.id()),
|
||||||
|
profileKey,
|
||||||
|
discharge,
|
||||||
|
path.edges(),
|
||||||
|
segments
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 0 when the shape is organic; otherwise 1 too short, 2 too sinuous, 3 grid-locked, 4 sharp turns. */
|
||||||
|
int surfaceShapeRejection(List<HydrologyPoint> routedCenterline) {
|
||||||
|
return surfaceShapeRejection(routedCenterline, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As above; a tributary reach keeps the hard turn cap and the overall turn budget but may bend
|
||||||
|
* once where it leaves its own valley for the stem's, which a main river is not allowed to do.
|
||||||
|
*/
|
||||||
|
int surfaceShapeRejection(List<HydrologyPoint> routedCenterline, boolean tributary) {
|
||||||
|
List<HydrologyPoint> centerline = planner.routePaths.withoutDuplicateRoutePoints(routedCenterline);
|
||||||
|
if (centerline.size() < 3) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
double routedLength = centerlineLength(centerline);
|
||||||
|
HydrologyPoint start = centerline.getFirst();
|
||||||
|
HydrologyPoint end = centerline.getLast();
|
||||||
|
double directLength = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
|
||||||
|
if (directLength <= 0D
|
||||||
|
|| routedLength / directLength > SURFACE_MAXIMUM_SINUOSITY) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> sampled = resampleRouteCenterline(
|
||||||
|
centerline,
|
||||||
|
Math.max(16D, planner.settings.routing().refinementSpacing() * 4D)
|
||||||
|
);
|
||||||
|
double lockedLength = 0D;
|
||||||
|
double currentLockedRun = 0D;
|
||||||
|
double longestLockedRun = 0D;
|
||||||
|
for (int pointIndex = 1; pointIndex < sampled.size(); pointIndex++) {
|
||||||
|
HydrologyPoint previous = sampled.get(pointIndex - 1);
|
||||||
|
HydrologyPoint point = sampled.get(pointIndex);
|
||||||
|
double deltaX = point.x() - previous.x();
|
||||||
|
double deltaZ = point.z() - previous.z();
|
||||||
|
double length = StrictMath.hypot(deltaX, deltaZ);
|
||||||
|
if (length <= 0D) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double angle = StrictMath.toDegrees(StrictMath.atan2(deltaZ, deltaX));
|
||||||
|
double remainder = Math.floorMod(
|
||||||
|
(int) StrictMath.round(angle * 1_000_000D),
|
||||||
|
45_000_000
|
||||||
|
) / 1_000_000D;
|
||||||
|
double gridDistance = Math.min(remainder, 45D - remainder);
|
||||||
|
if (gridDistance <= SURFACE_GRID_LOCK_TOLERANCE_DEGREES) {
|
||||||
|
lockedLength += length;
|
||||||
|
currentLockedRun += length;
|
||||||
|
longestLockedRun = Math.max(longestLockedRun, currentLockedRun);
|
||||||
|
} else {
|
||||||
|
currentLockedRun = 0D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (routedLength >= 96D
|
||||||
|
&& (lockedLength / routedLength > SURFACE_MAXIMUM_GRID_LOCKED_FRACTION
|
||||||
|
|| longestLockedRun > SURFACE_MAXIMUM_GRID_LOCKED_RUN)) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
ArrayList<Double> turnAngles = new ArrayList<>();
|
||||||
|
for (int pointIndex = 1; pointIndex < sampled.size() - 1; pointIndex++) {
|
||||||
|
double turn = HydrologyRoutePath.routeTurnDegrees(
|
||||||
|
sampled.get(pointIndex - 1),
|
||||||
|
sampled.get(pointIndex),
|
||||||
|
sampled.get(pointIndex + 1)
|
||||||
|
);
|
||||||
|
if (turn > SURFACE_MAXIMUM_RENDERED_TURN_DEGREES) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
turnAngles.add(turn);
|
||||||
|
}
|
||||||
|
if (turnAngles.isEmpty()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (int turnIndex = 0; turnIndex < turnAngles.size() && !tributary; turnIndex++) {
|
||||||
|
double turn = turnAngles.get(turnIndex);
|
||||||
|
double previous = turnIndex == 0 ? 0D : turnAngles.get(turnIndex - 1);
|
||||||
|
double next = turnIndex + 1 == turnAngles.size() ? 0D : turnAngles.get(turnIndex + 1);
|
||||||
|
if (turn > SURFACE_MAXIMUM_ISOLATED_TURN_DEGREES
|
||||||
|
&& previous <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES
|
||||||
|
&& next <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
turnAngles.sort(Double::compare);
|
||||||
|
int percentileIndex = (int) StrictMath.ceil(0.95D * turnAngles.size()) - 1;
|
||||||
|
double percentile = turnAngles.get(
|
||||||
|
Math.max(0, Math.min(percentileIndex, turnAngles.size() - 1))
|
||||||
|
);
|
||||||
|
if (percentile > (tributary ? SURFACE_MAXIMUM_RENDERED_TURN_DEGREES : SURFACE_MAXIMUM_P95_TURN_DEGREES)) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double centerlineLength(List<HydrologyPoint> points) {
|
||||||
|
double length = 0D;
|
||||||
|
for (int pointIndex = 1; pointIndex < points.size(); pointIndex++) {
|
||||||
|
HydrologyPoint previous = points.get(pointIndex - 1);
|
||||||
|
HydrologyPoint current = points.get(pointIndex);
|
||||||
|
length += StrictMath.hypot(current.x() - previous.x(), current.z() - previous.z());
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> resampleRouteCenterline(
|
||||||
|
List<HydrologyPoint> points,
|
||||||
|
double spacing
|
||||||
|
) {
|
||||||
|
double[] cumulative = new double[points.size()];
|
||||||
|
for (int pointIndex = 1; pointIndex < points.size(); pointIndex++) {
|
||||||
|
HydrologyPoint previous = points.get(pointIndex - 1);
|
||||||
|
HydrologyPoint point = points.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 <= spacing) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
int samples = Math.max(2, (int) StrictMath.floor(totalLength / spacing));
|
||||||
|
ArrayList<HydrologyPoint> sampled = new ArrayList<>(samples + 2);
|
||||||
|
int segmentIndex = 0;
|
||||||
|
for (double distance = 0D; distance < totalLength; distance += spacing) {
|
||||||
|
while (segmentIndex < points.size() - 2
|
||||||
|
&& cumulative[segmentIndex + 1] < distance) {
|
||||||
|
segmentIndex++;
|
||||||
|
}
|
||||||
|
HydrologyPoint start = points.get(segmentIndex);
|
||||||
|
HydrologyPoint end = points.get(segmentIndex + 1);
|
||||||
|
double segmentLength = cumulative[segmentIndex + 1] - cumulative[segmentIndex];
|
||||||
|
double progress = segmentLength <= 0D
|
||||||
|
? 0D
|
||||||
|
: (distance - cumulative[segmentIndex]) / segmentLength;
|
||||||
|
HydrologyPoint point = new HydrologyPoint(
|
||||||
|
(int) StrictMath.round(start.x() + (end.x() - start.x()) * progress),
|
||||||
|
(int) StrictMath.round(start.y() + (end.y() - start.y()) * progress),
|
||||||
|
(int) StrictMath.round(start.z() + (end.z() - start.z()) * progress)
|
||||||
|
);
|
||||||
|
if (sampled.isEmpty()
|
||||||
|
|| point.x() != sampled.getLast().x()
|
||||||
|
|| point.z() != sampled.getLast().z()) {
|
||||||
|
sampled.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HydrologyPoint last = points.getLast();
|
||||||
|
HydrologyPoint sampledLast = sampled.isEmpty() ? null : sampled.getLast();
|
||||||
|
double remaining = sampledLast == null
|
||||||
|
? Double.POSITIVE_INFINITY
|
||||||
|
: StrictMath.hypot(last.x() - sampledLast.x(), last.z() - sampledLast.z());
|
||||||
|
if (remaining >= spacing * 0.5D) {
|
||||||
|
sampled.add(last);
|
||||||
|
}
|
||||||
|
return List.copyOf(sampled);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyCoursePath coursePath(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
CompiledGraph graph,
|
||||||
|
int sourceIndex,
|
||||||
|
boolean surface
|
||||||
|
) {
|
||||||
|
ArrayList<DrainageEdge> edges = new ArrayList<>();
|
||||||
|
ArrayList<HydrologyPoint> guide = new ArrayList<>();
|
||||||
|
ArrayList<DrainageEdge> guideEdges = new ArrayList<>();
|
||||||
|
int current = sourceIndex;
|
||||||
|
int steps = 0;
|
||||||
|
while (routing.parent()[current] >= 0) {
|
||||||
|
DrainageEdge edge = graph.edgeByUpstream().get(current);
|
||||||
|
if (edge == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
edges.add(edge);
|
||||||
|
int downstream = routing.parent()[current];
|
||||||
|
if (surface) {
|
||||||
|
for (HydrologyPoint point : edge.centerline()) {
|
||||||
|
if (guide.isEmpty()) {
|
||||||
|
guide.add(point);
|
||||||
|
} else if (point.x() != guide.getLast().x() || point.z() != guide.getLast().z()) {
|
||||||
|
guide.add(point);
|
||||||
|
guideEdges.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = downstream;
|
||||||
|
if (++steps > planner.settings.routing().maximumRouteNodes()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int outletIndex = routing.outletIndex()[sourceIndex];
|
||||||
|
if (outletIndex < 0 || edges.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
RiverOutlet outlet = routing.outlets().get(outletIndex).outlet();
|
||||||
|
if (!surface) {
|
||||||
|
return planner.undergroundCourses.undergroundCoursePath(edges, outlet);
|
||||||
|
}
|
||||||
|
if (guide.size() < 2 || guideEdges.size() != guide.size() - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> routedGuide = new ArrayList<>(guide);
|
||||||
|
ArrayList<DrainageEdge> routedGuideEdges = new ArrayList<>(guideEdges);
|
||||||
|
HydrologyPoint landward = outlet.landwardPoint();
|
||||||
|
HydrologyPoint guideEnd = routedGuide.getLast();
|
||||||
|
boolean terminateAtSinkhole = planner.segments.surfaceSinkhole(outlet)
|
||||||
|
&& (guideEnd.x() != landward.x() || guideEnd.z() != landward.z());
|
||||||
|
if (terminateAtSinkhole) {
|
||||||
|
routedGuide.set(routedGuide.size() - 1, landward);
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> surfaceGuide = planner.routePaths.withoutDuplicateRoutePoints(routedGuide);
|
||||||
|
long geometryId = HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
COURSE_SALT,
|
||||||
|
HydrologySourcePlanner.SURFACE_SOURCE_SALT,
|
||||||
|
edges.getFirst().upstreamNodeId(),
|
||||||
|
outlet.id()
|
||||||
|
);
|
||||||
|
Objects.requireNonNull(grid, "Surface course routing grid is required");
|
||||||
|
HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get();
|
||||||
|
DraftProfile profile = planner.currentDraftProfile();
|
||||||
|
profile.routeCalls++;
|
||||||
|
List<HydrologyPoint> points;
|
||||||
|
if (samples == null) {
|
||||||
|
profile.routeSolves++;
|
||||||
|
long solveStarted = System.nanoTime();
|
||||||
|
points = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
|
||||||
|
profile.routeSolveNanos += System.nanoTime() - solveStarted;
|
||||||
|
} else {
|
||||||
|
HashMap<SurfaceRouteKey, List<HydrologyPoint>> routes = samples.surfaceRoutes.computeIfAbsent(
|
||||||
|
grid, ignored -> new HashMap<>()
|
||||||
|
);
|
||||||
|
SurfaceRouteKey routeKey = new SurfaceRouteKey(surfaceGuide, geometryId);
|
||||||
|
points = routes.computeIfAbsent(routeKey, ignored -> {
|
||||||
|
profile.routeSolves++;
|
||||||
|
long solveStarted = System.nanoTime();
|
||||||
|
List<HydrologyPoint> solved = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
|
||||||
|
profile.routeSolveNanos += System.nanoTime() - solveStarted;
|
||||||
|
return solved;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (points.size() < 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<DrainageEdge> pairEdges = mapCoursePairs(points, routedGuide, routedGuideEdges);
|
||||||
|
if (pairEdges.size() != points.size() - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new HydrologyCoursePath(
|
||||||
|
points,
|
||||||
|
pairEdges,
|
||||||
|
List.copyOf(edges),
|
||||||
|
outlet,
|
||||||
|
true,
|
||||||
|
routing.organicSurfaceRequired(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyPoint> surfaceCourseCenterline(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<HydrologyPoint> guide,
|
||||||
|
long geometryId
|
||||||
|
) {
|
||||||
|
if (guide.size() < 2) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
int refinement = planner.settings.routing().refinementSpacing();
|
||||||
|
List<SurfaceCurvePoint> base = smoothSurfaceGuide(resampleSurfaceGuide(guide, refinement));
|
||||||
|
List<SurfaceCurvePoint> nominal = meanderSurfaceGuide(base, geometryId, refinement);
|
||||||
|
if (nominal.size() < 2) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
ArrayList<List<RouteCandidate>> layers = new ArrayList<>(nominal.size());
|
||||||
|
int transverseCandidates = HydrologyRouteGeometry.SURFACE_COURSE_TRANSVERSE_CANDIDATES;
|
||||||
|
for (int pointIndex = 0; pointIndex < nominal.size(); pointIndex++) {
|
||||||
|
SurfaceCurvePoint point = nominal.get(pointIndex);
|
||||||
|
RouteDirection tangent = surfaceCurveTangent(nominal, pointIndex);
|
||||||
|
if (pointIndex == 0 || pointIndex == nominal.size() - 1) {
|
||||||
|
HydrologyPoint endpoint = pointIndex == 0 ? guide.getFirst() : guide.getLast();
|
||||||
|
layers.add(List.of(planner.routeGeometry.routeEndpoint(endpoint, tangent)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double progress = pointIndex / (double) (nominal.size() - 1);
|
||||||
|
SurfaceCurvePoint basePoint = base.get(pointIndex);
|
||||||
|
RoutePosition position = new RoutePosition(
|
||||||
|
point.x(),
|
||||||
|
point.z(),
|
||||||
|
basePoint.x(),
|
||||||
|
basePoint.z(),
|
||||||
|
tangent
|
||||||
|
);
|
||||||
|
long candidateId = HydrologyHash.mix(geometryId, pointIndex);
|
||||||
|
List<RouteCandidate> candidates = planner.routeGeometry.unresolvedSurfaceRouteCandidates(
|
||||||
|
grid,
|
||||||
|
geometryId,
|
||||||
|
candidateId,
|
||||||
|
position,
|
||||||
|
progress,
|
||||||
|
refinement,
|
||||||
|
transverseCandidates
|
||||||
|
);
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
layers.add(candidates);
|
||||||
|
}
|
||||||
|
ArrayList<List<RouteCandidate>> availableLayers = new ArrayList<>(layers);
|
||||||
|
double maximumTurn = Math.min(
|
||||||
|
HydrologyRouteGeometry.SURFACE_COURSE_MAXIMUM_TURN_DEGREES,
|
||||||
|
planner.settings.geometry().meanders().maximumTurnDegrees()
|
||||||
|
);
|
||||||
|
while (true) {
|
||||||
|
RouteCandidate[] route = planner.routeGeometry.selectCurvatureAwareTerrainRoute(
|
||||||
|
availableLayers,
|
||||||
|
refinement,
|
||||||
|
maximumTurn,
|
||||||
|
HydrologyRouteGeometry.SURFACE_COURSE_TURN_COST
|
||||||
|
);
|
||||||
|
if (route.length == 0) {
|
||||||
|
if (addExhaustedSurfaceRouteFallbacks(
|
||||||
|
grid,
|
||||||
|
availableLayers,
|
||||||
|
nominal,
|
||||||
|
base,
|
||||||
|
geometryId
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
int pitRemoval = planner.routePaths.removeTerrainPits(availableLayers, route);
|
||||||
|
if (pitRemoval < 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
if (pitRemoval > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(route.length);
|
||||||
|
for (RouteCandidate candidate : route) {
|
||||||
|
HydrologyPoint point = candidate.point();
|
||||||
|
if (points.isEmpty()
|
||||||
|
|| point.x() != points.getLast().x()
|
||||||
|
|| point.z() != points.getLast().z()) {
|
||||||
|
points.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (points.size() < 2 || !planner.routePaths.traversableRoute(points)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return planner.routePaths.smoothTerrainRoute(points);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean addExhaustedSurfaceRouteFallbacks(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<List<RouteCandidate>> layers,
|
||||||
|
List<SurfaceCurvePoint> nominal,
|
||||||
|
List<SurfaceCurvePoint> base,
|
||||||
|
long geometryId
|
||||||
|
) {
|
||||||
|
boolean added = false;
|
||||||
|
for (int layerIndex = 1; layerIndex < layers.size() - 1; layerIndex++) {
|
||||||
|
List<RouteCandidate> layer = layers.get(layerIndex);
|
||||||
|
if (!exhaustedSurfaceRouteLayer(layer)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SurfaceCurvePoint nominalPoint = nominal.get(layerIndex);
|
||||||
|
SurfaceCurvePoint basePoint = base.get(layerIndex);
|
||||||
|
RouteDirection tangent = surfaceCurveTangent(nominal, layerIndex);
|
||||||
|
int fallbackX = (int) StrictMath.round(basePoint.x());
|
||||||
|
int fallbackZ = (int) StrictMath.round(basePoint.z());
|
||||||
|
long packed = RiverFootprint.pack(fallbackX, fallbackZ);
|
||||||
|
boolean present = false;
|
||||||
|
for (RouteCandidate candidate : layer) {
|
||||||
|
if (RiverFootprint.pack(candidate.point().x(), candidate.point().z()) == packed) {
|
||||||
|
present = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (present) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RouteTerrainEstimate estimate = planner.routeGeometry.routeTerrainEstimate(grid, fallbackX, fallbackZ);
|
||||||
|
double offset = (basePoint.x() - nominalPoint.x()) * -tangent.z()
|
||||||
|
+ (basePoint.z() - nominalPoint.z()) * tangent.x();
|
||||||
|
double localScore = estimate.terrainScore()
|
||||||
|
+ StrictMath.abs(offset) * 0.18D
|
||||||
|
+ HydrologyHash.unit(HydrologyHash.mix(
|
||||||
|
planner.worldSeed,
|
||||||
|
geometryId,
|
||||||
|
HydrologyHash.mix(geometryId, layerIndex),
|
||||||
|
fallbackX,
|
||||||
|
fallbackZ
|
||||||
|
)) * 1.0E-6D;
|
||||||
|
ArrayList<RouteCandidate> expanded = new ArrayList<>(layer);
|
||||||
|
expanded.add(new RouteCandidate(
|
||||||
|
new HydrologyPoint(fallbackX, estimate.height(), fallbackZ),
|
||||||
|
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) {
|
||||||
|
List<HydrologyPoint> sampled = resampleRouteCenterline(
|
||||||
|
route,
|
||||||
|
Math.max(16D, planner.settings.routing().refinementSpacing() * 4D)
|
||||||
|
);
|
||||||
|
if (sampled.size() < 3) {
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
ArrayList<Double> turns = new ArrayList<>(sampled.size() - 2);
|
||||||
|
double maximum = 0D;
|
||||||
|
for (int pointIndex = 1; pointIndex < sampled.size() - 1; pointIndex++) {
|
||||||
|
double turn = HydrologyRoutePath.routeTurnDegrees(
|
||||||
|
sampled.get(pointIndex - 1),
|
||||||
|
sampled.get(pointIndex),
|
||||||
|
sampled.get(pointIndex + 1)
|
||||||
|
);
|
||||||
|
turns.add(turn);
|
||||||
|
maximum = Math.max(maximum, turn);
|
||||||
|
}
|
||||||
|
turns.sort(Double::compare);
|
||||||
|
int percentileIndex = (int) StrictMath.ceil(0.95D * turns.size()) - 1;
|
||||||
|
double percentile = turns.get(Math.max(0, Math.min(percentileIndex, turns.size() - 1)));
|
||||||
|
return percentile + maximum * 0.05D;
|
||||||
|
}
|
||||||
|
|
||||||
|
double surfaceRouteCandidateBankPenalty(RouteCandidate candidate) {
|
||||||
|
int minimumBankDistance = surfaceBankDistance(planner.settings.surface().minimumWidth());
|
||||||
|
int maximumBankDistance = surfaceBankDistance(planner.settings.surface().maximumWidth());
|
||||||
|
int maximumBankRise = Math.addExact(
|
||||||
|
planner.settings.surface().maximumIncision(),
|
||||||
|
(int) StrictMath.ceil(planner.settings.surface().banks().minimumBlendWidth() * 0.5D)
|
||||||
|
);
|
||||||
|
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 surfaceRouteBankPenalty(
|
||||||
|
point,
|
||||||
|
candidate.tangent(),
|
||||||
|
minimumBankDistance,
|
||||||
|
maximumBankDistance,
|
||||||
|
head,
|
||||||
|
maximumBankRise
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double surfaceRouteBankPenalty(
|
||||||
|
HydrologyPoint point,
|
||||||
|
RouteDirection tangent,
|
||||||
|
int minimumDistance,
|
||||||
|
int maximumDistance,
|
||||||
|
int head,
|
||||||
|
int maximumBankRise
|
||||||
|
) {
|
||||||
|
double penalty = 0D;
|
||||||
|
for (int distance = minimumDistance; 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
|
||||||
|
) {
|
||||||
|
HydrologyTerrainSample bank = surfaceBankTerrain(point, tangent, signedDistance);
|
||||||
|
if (bank == null) {
|
||||||
|
return 1.0E12D;
|
||||||
|
}
|
||||||
|
int rise = bank.naturalHeight() - 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyTerrainSample surfaceBankTerrain(
|
||||||
|
HydrologyPoint point,
|
||||||
|
RouteDirection tangent,
|
||||||
|
int signedDistance
|
||||||
|
) {
|
||||||
|
HydrologyPoint bankPoint = surfaceBankPoint(point, tangent, signedDistance);
|
||||||
|
int bankX = bankPoint.x();
|
||||||
|
int bankZ = bankPoint.z();
|
||||||
|
return planner.sampleLandBasisWithoutSlope(bankX, bankZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyPoint surfaceBankPoint(HydrologyPoint point, RouteDirection tangent, int signedDistance) {
|
||||||
|
return new HydrologyPoint(
|
||||||
|
(int) StrictMath.round(point.x() - tangent.z() * signedDistance),
|
||||||
|
point.y(),
|
||||||
|
(int) StrictMath.round(point.z() + tangent.x() * signedDistance)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record SurfaceCurvePoint(double x, double z, double distance) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record SurfaceCourseBuild(
|
||||||
|
RiverCourse course,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
int rejectionDetail
|
||||||
|
) {
|
||||||
|
SurfaceCourseBuild {
|
||||||
|
if ((course == null) == (rejection == null)) {
|
||||||
|
throw new IllegalArgumentException("Surface course builds require exactly one result.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceCourseBuild accepted(RiverCourse course) {
|
||||||
|
return new SurfaceCourseBuild(Objects.requireNonNull(course, "course"), null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceCourseBuild rejected(HydrologyCandidateRejection rejection, int detail) {
|
||||||
|
return new SurfaceCourseBuild(null, Objects.requireNonNull(rejection, "rejection"), detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,674 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
|
||||||
|
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class HydrologyTributaryPlanner {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologyTributaryPlanner(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
static final int TRIBUTARY_JUNCTION_ATTEMPTS = 4;
|
||||||
|
static final int TRIBUTARY_BACKWATER_LIMIT = 3;
|
||||||
|
static final int TRIBUTARY_JUNCTION_FLAT_STATIONS = 6;
|
||||||
|
static final int TRIBUTARY_JUNCTION_PAIR_INDEX = 0x4a4f494e;
|
||||||
|
|
||||||
|
List<RiverCourse> normalizeOutletContinuations(List<RiverCourse> courses) {
|
||||||
|
HashMap<OutletCourseKey, RiverCourse> canonicalCourses = new HashMap<>();
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
if (!hasInlandOutletContinuation(course) || course.outletId().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
OutletCourseKey key = new OutletCourseKey(course.outletId().getAsLong());
|
||||||
|
canonicalCourses.merge(
|
||||||
|
key,
|
||||||
|
course,
|
||||||
|
this::preferredOutletContinuation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ArrayList<RiverCourse> normalized = new ArrayList<>(courses.size());
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
if (course.outletId().isEmpty()) {
|
||||||
|
normalized.add(course);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
OutletCourseKey key = new OutletCourseKey(course.outletId().getAsLong());
|
||||||
|
RiverCourse canonicalCourse = canonicalCourses.get(key);
|
||||||
|
if (canonicalCourse == null || canonicalCourse.id() == course.id()) {
|
||||||
|
normalized.add(course);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<HydraulicSegment> segments = course.segments();
|
||||||
|
int retainedSize = course.surfaceSinkholeContinuation()
|
||||||
|
? segments.size() - 2
|
||||||
|
: segments.getLast().type() == HydrologyFeatureType.INLAND_GROTTO
|
||||||
|
? segments.size() - 1
|
||||||
|
: segments.size();
|
||||||
|
retainedSize = trimCanonicalOutletOverlap(course, canonicalCourse, retainedSize);
|
||||||
|
if (retainedSize < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.add(new RiverCourse(
|
||||||
|
course.id(),
|
||||||
|
course.type(),
|
||||||
|
course.sourceNodeId(),
|
||||||
|
course.outletId(),
|
||||||
|
course.profileKey(),
|
||||||
|
course.discharge(),
|
||||||
|
course.drainageEdges(),
|
||||||
|
segments.subList(0, retainedSize)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
int trimCanonicalOutletOverlap(
|
||||||
|
RiverCourse course,
|
||||||
|
RiverCourse canonicalCourse,
|
||||||
|
int retainedSize
|
||||||
|
) {
|
||||||
|
if (!canonicalCourse.surfaceSinkholeContinuation()
|
||||||
|
|| canonicalCourse.segments().isEmpty()
|
||||||
|
|| retainedSize < 1) {
|
||||||
|
return retainedSize;
|
||||||
|
}
|
||||||
|
HydraulicSegment canonicalGrotto = canonicalCourse.segments().getLast();
|
||||||
|
if (canonicalGrotto.type() != HydrologyFeatureType.INLAND_GROTTO) {
|
||||||
|
return retainedSize;
|
||||||
|
}
|
||||||
|
int exclusionRadius = Math.addExact(
|
||||||
|
planner.settings.outlets().inlandGrotto().horizontalRadius(),
|
||||||
|
Math.addExact((int) StrictMath.ceil(planner.settings.underground().maximumWidth() / 2D), 2)
|
||||||
|
);
|
||||||
|
long exclusionRadiusSquared = (long) exclusionRadius * exclusionRadius;
|
||||||
|
while (retainedSize > 0 && segmentOverlapsOutlet(
|
||||||
|
course.segments().get(retainedSize - 1),
|
||||||
|
canonicalGrotto,
|
||||||
|
exclusionRadiusSquared
|
||||||
|
)) {
|
||||||
|
retainedSize--;
|
||||||
|
}
|
||||||
|
return retainedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean segmentOverlapsOutlet(
|
||||||
|
HydraulicSegment segment,
|
||||||
|
HydraulicSegment outlet,
|
||||||
|
long exclusionRadiusSquared
|
||||||
|
) {
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
for (HydrologyPoint outletPoint : outlet.centerline()) {
|
||||||
|
if (point.distanceSquared2D(outletPoint) <= exclusionRadiusSquared) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RiverCourse> normalizeSharedTrunks(List<RiverCourse> courses, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||||
|
HashMap<CourseEdgeKey, RiverCourse> edgeOwners = new HashMap<>();
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
if (course.sourceNodeId().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (DrainageEdge edge : course.drainageEdges()) {
|
||||||
|
CourseEdgeKey key = new CourseEdgeKey(course.type(), edge.id());
|
||||||
|
edgeOwners.merge(
|
||||||
|
key,
|
||||||
|
course,
|
||||||
|
this::preferredTrunkCourse
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArrayList<RiverCourse> normalized = new ArrayList<>(courses.size());
|
||||||
|
for (RiverCourse course : courses) {
|
||||||
|
if (course.sourceNodeId().isEmpty() || course.drainageEdges().isEmpty()) {
|
||||||
|
normalized.add(course);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int ownedEdgeCount = 0;
|
||||||
|
for (DrainageEdge edge : course.drainageEdges()) {
|
||||||
|
RiverCourse owner = edgeOwners.get(new CourseEdgeKey(course.type(), edge.id()));
|
||||||
|
if (owner == null || owner.id() != course.id()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ownedEdgeCount++;
|
||||||
|
}
|
||||||
|
if (ownedEdgeCount == course.drainageEdges().size()) {
|
||||||
|
normalized.add(course);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ownedEdgeCount < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
HydrologyPoint boundary = course.drainageEdges()
|
||||||
|
.get(ownedEdgeCount - 1)
|
||||||
|
.centerline()
|
||||||
|
.getLast();
|
||||||
|
if (course.type() == RiverCourseType.SURFACE) {
|
||||||
|
// Surface tributaries are cut at their junction before they are built, so a surface
|
||||||
|
// course still sharing drainage with a longer trunk has no junction to keep it.
|
||||||
|
addTributaryDiagnostic(course.id(), course.segments().getFirst().start(),
|
||||||
|
HydrologyCandidateRejection.NO_DRAINAGE_PATH, ownedEdgeCount, diagnostics);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int retainedSegmentCount = retainedSegmentCount(course.segments(), boundary);
|
||||||
|
if (retainedSegmentCount < 1) {
|
||||||
|
normalized.add(course);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.add(new RiverCourse(
|
||||||
|
course.id(),
|
||||||
|
course.type(),
|
||||||
|
course.sourceNodeId(),
|
||||||
|
course.outletId(),
|
||||||
|
course.profileKey(),
|
||||||
|
course.discharge(),
|
||||||
|
course.drainageEdges().subList(0, ownedEdgeCount),
|
||||||
|
course.segments().subList(0, retainedSegmentCount)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return List.copyOf(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a later draft to the stem's outlet as a tributary. The draft's path is cut where it first
|
||||||
|
* comes within a stem width of the stem or first enters drainage the stem already owns, and it is
|
||||||
|
* shaped with the stem's water level at that station as its terminal, so it arrives at or above the
|
||||||
|
* stem and steps down into it. When the tributary would arrive below the stem's water there, the
|
||||||
|
* junction slides downstream along the stem to the first station whose water is low enough and the
|
||||||
|
* course is shaped again. The tributary owns only the drainage edges upstream of the junction.
|
||||||
|
*/
|
||||||
|
RiverCourse buildSurfaceTributary(
|
||||||
|
SurfaceCourseDraft draft,
|
||||||
|
RiverCourse stem,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
HydrologyCoursePath path = draft.path();
|
||||||
|
HydrologyPoint sourcePoint = path.points().getFirst();
|
||||||
|
HashSet<Long> stemEdges = new HashSet<>();
|
||||||
|
for (DrainageEdge edge : stem.drainageEdges()) {
|
||||||
|
stemEdges.add(edge.id());
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> stemStations = new ArrayList<>();
|
||||||
|
ArrayList<Integer> stemWidths = new ArrayList<>();
|
||||||
|
for (HydraulicSegment segment : stem.segments()) {
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
stemStations.add(point);
|
||||||
|
stemWidths.add(segment.width());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int minimumStemIndex = 0;
|
||||||
|
HydrologyCandidateRejection rejection = HydrologyCandidateRejection.NO_DRAINAGE_PATH;
|
||||||
|
int rejectionDetail = 0;
|
||||||
|
for (int attempt = 0; attempt < TRIBUTARY_JUNCTION_ATTEMPTS; attempt++) {
|
||||||
|
int[] junction = tributaryJunction(path, stemStations, stemWidths, stemEdges, minimumStemIndex);
|
||||||
|
if (junction == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int joinIndex = junction[0];
|
||||||
|
int stemIndex = junction[1];
|
||||||
|
HydrologyPoint joinStation = stemStations.get(stemIndex);
|
||||||
|
if (joinIndex < 1) {
|
||||||
|
rejection = HydrologyCandidateRejection.COURSE_TOO_SHORT;
|
||||||
|
rejectionDetail = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
TributaryBuild build = buildSurfaceTributaryAt(draft, joinIndex, stemIndex, stemStations, stemWidths, stemEdges);
|
||||||
|
if (build.course() != null) {
|
||||||
|
return build.course();
|
||||||
|
}
|
||||||
|
rejection = build.rejection();
|
||||||
|
rejectionDetail = build.rejectionDetail();
|
||||||
|
if (rejection != HydrologyCandidateRejection.SURFACE_HEAD_RANGE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// The tributary arrives rejectionDetail blocks below the stem's water: slide the junction
|
||||||
|
// downstream to the first stem station whose water is that much lower.
|
||||||
|
int target = joinStation.y() - rejectionDetail;
|
||||||
|
int nextStemIndex = -1;
|
||||||
|
for (int index = stemIndex + 1; index < stemStations.size(); index++) {
|
||||||
|
if (stemStations.get(index).y() <= target) {
|
||||||
|
nextStemIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextStemIndex < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
minimumStemIndex = nextStemIndex;
|
||||||
|
}
|
||||||
|
addTributaryDiagnostic(draft.courseId(), sourcePoint, rejection, rejectionDetail, diagnostics);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first draft point, in path order, that lies within a stem width of a stem station at or past
|
||||||
|
* {@code minimumStemIndex} or that enters drainage the stem owns, as {draft point index, stem station
|
||||||
|
* index}; null when the draft never meets the stem there. Among the stations the point can reach the
|
||||||
|
* lowest water wins, then the flattest stretch, so the tributary's mouth sits at the river's level
|
||||||
|
* where the two channels touch and never above the stem's shore.
|
||||||
|
*/
|
||||||
|
static int[] tributaryJunction(
|
||||||
|
HydrologyCoursePath path,
|
||||||
|
List<HydrologyPoint> stemStations,
|
||||||
|
List<Integer> stemWidths,
|
||||||
|
Set<Long> stemEdges,
|
||||||
|
int minimumStemIndex
|
||||||
|
) {
|
||||||
|
for (int index = 0; index < path.points().size(); index++) {
|
||||||
|
HydrologyPoint point = path.points().get(index);
|
||||||
|
boolean sharedEdge = index > 0 && stemEdges.contains(path.pairEdges().get(index - 1).id());
|
||||||
|
int best = -1;
|
||||||
|
int bestFlatness = -1;
|
||||||
|
double bestDistance = Double.POSITIVE_INFINITY;
|
||||||
|
for (int stemIndex = minimumStemIndex; stemIndex < stemStations.size(); stemIndex++) {
|
||||||
|
double distance = point.distanceSquared2D(stemStations.get(stemIndex));
|
||||||
|
double reach = stemWidths.get(stemIndex) + 1D;
|
||||||
|
boolean reachable = distance <= reach * reach;
|
||||||
|
if (!reachable && !(sharedEdge && best < 0 && distance < bestDistance)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int flatness = reachable ? stemFlatness(stemStations, stemIndex) : -1;
|
||||||
|
int level = stemStations.get(stemIndex).y();
|
||||||
|
int bestLevel = best < 0 ? Integer.MAX_VALUE : stemStations.get(best).y();
|
||||||
|
if (level < bestLevel
|
||||||
|
|| level == bestLevel && (flatness > bestFlatness || flatness == bestFlatness && distance < bestDistance)) {
|
||||||
|
best = stemIndex;
|
||||||
|
bestFlatness = flatness;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best >= 0) {
|
||||||
|
return new int[] {index, best};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many of the stem stations around {@code stemIndex} share its water level. */
|
||||||
|
static int stemFlatness(List<HydrologyPoint> stemStations, int stemIndex) {
|
||||||
|
int level = stemStations.get(stemIndex).y();
|
||||||
|
int flat = 0;
|
||||||
|
for (int offset = -TRIBUTARY_JUNCTION_FLAT_STATIONS; offset <= TRIBUTARY_JUNCTION_FLAT_STATIONS; offset++) {
|
||||||
|
int index = stemIndex + offset;
|
||||||
|
if (index >= 0 && index < stemStations.size() && stemStations.get(index).y() == level) {
|
||||||
|
flat++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raises every trailing station below {@code level} to it, so the tail is a flat pool at the stem's water. */
|
||||||
|
static void backwater(List<HydraulicSegment> segments, int level) {
|
||||||
|
for (int index = segments.size() - 1; index >= 0; index--) {
|
||||||
|
HydraulicSegment segment = segments.get(index);
|
||||||
|
if (segment.upstreamHeadY() >= level && segment.downstreamHeadY() >= level) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ArrayList<HydrologyPoint> centerline = new ArrayList<>(segment.centerline().size());
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
centerline.add(point.y() < level ? new HydrologyPoint(point.x(), level, point.z()) : point);
|
||||||
|
}
|
||||||
|
int upstream = Math.max(segment.upstreamHeadY(), level);
|
||||||
|
int downstream = Math.max(segment.downstreamHeadY(), level);
|
||||||
|
segments.set(index, new HydraulicSegment(
|
||||||
|
segment.id(),
|
||||||
|
segment.courseId(),
|
||||||
|
upstream == downstream ? HydrologyFeatureType.SURFACE_POOL : segment.type(),
|
||||||
|
upstream,
|
||||||
|
downstream,
|
||||||
|
segment.width(),
|
||||||
|
segment.depth(),
|
||||||
|
segment.fallingFluid(),
|
||||||
|
segment.receivingPool(),
|
||||||
|
List.copyOf(centerline)
|
||||||
|
));
|
||||||
|
if (segment.upstreamHeadY() >= level) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lowers the trailing stations of a tributary towards {@code level}, one block per cascade run of
|
||||||
|
* centerline distance from the junction and never deeper than the channel may cut, so the reach
|
||||||
|
* grades down into the stem instead of hanging above it. Where the ground holds a station up, the
|
||||||
|
* stations upstream of it are held with it, so the graded reach still descends into the stem.
|
||||||
|
*/
|
||||||
|
void levelApproach(List<HydraulicSegment> segments, int level) {
|
||||||
|
int run = Math.max(1, planner.settings.surface().banks().cascadeRun());
|
||||||
|
int maximumIncision = planner.settings.surface().maximumIncision();
|
||||||
|
double distance = 0D;
|
||||||
|
HydrologyPoint previous = null;
|
||||||
|
int downstreamHead = Integer.MIN_VALUE;
|
||||||
|
for (int index = segments.size() - 1; index >= 0; index--) {
|
||||||
|
HydraulicSegment segment = segments.get(index);
|
||||||
|
ArrayList<HydrologyPoint> centerline = new ArrayList<>(segment.centerline());
|
||||||
|
boolean changed = false;
|
||||||
|
boolean settled = false;
|
||||||
|
for (int pointIndex = centerline.size() - 1; pointIndex >= 0; pointIndex--) {
|
||||||
|
HydrologyPoint point = centerline.get(pointIndex);
|
||||||
|
if (previous != null) {
|
||||||
|
distance += StrictMath.hypot(point.x() - previous.x(), point.z() - previous.z());
|
||||||
|
}
|
||||||
|
previous = point;
|
||||||
|
int allowed = level + (int) StrictMath.floor(distance / run);
|
||||||
|
if (point.y() <= allowed) {
|
||||||
|
settled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int floor = planner.sampleBasisWithoutSlope(point.x(), point.z()).naturalHeight() - maximumIncision;
|
||||||
|
int lowered = Math.min(point.y(), Math.max(Math.max(allowed, floor), downstreamHead));
|
||||||
|
if (lowered < point.y()) {
|
||||||
|
centerline.set(pointIndex, new HydrologyPoint(point.x(), lowered, point.z()));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
downstreamHead = lowered;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
// A level reach may carry a bump above its heads (a pool's centerline is not graded);
|
||||||
|
// once its tail is cut down the reach becomes a graded transition, which must never rise
|
||||||
|
// downstream, so every station is held at or below the one before it.
|
||||||
|
int ceiling = centerline.getFirst().y();
|
||||||
|
for (int pointIndex = 1; pointIndex < centerline.size(); pointIndex++) {
|
||||||
|
HydrologyPoint point = centerline.get(pointIndex);
|
||||||
|
if (point.y() > ceiling) {
|
||||||
|
centerline.set(pointIndex, new HydrologyPoint(point.x(), ceiling, point.z()));
|
||||||
|
} else {
|
||||||
|
ceiling = point.y();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int upstream = centerline.getFirst().y();
|
||||||
|
int downstream = centerline.getLast().y();
|
||||||
|
HydrologyFeatureType type = segment.type();
|
||||||
|
if (upstream > downstream && !type.isDrop()) {
|
||||||
|
type = upstream - downstream >= planner.settings.hydraulics().waterfallMinimumDrop()
|
||||||
|
? HydrologyFeatureType.WATERFALL
|
||||||
|
: upstream - downstream == 1 ? HydrologyFeatureType.RIFFLE : HydrologyFeatureType.CASCADE;
|
||||||
|
} else if (upstream == downstream && type.isDrop()) {
|
||||||
|
type = HydrologyFeatureType.SURFACE_POOL;
|
||||||
|
}
|
||||||
|
segments.set(index, new HydraulicSegment(
|
||||||
|
segment.id(),
|
||||||
|
segment.courseId(),
|
||||||
|
type,
|
||||||
|
upstream,
|
||||||
|
downstream,
|
||||||
|
segment.width(),
|
||||||
|
segment.depth(),
|
||||||
|
segment.fallingFluid(),
|
||||||
|
segment.receivingPool(),
|
||||||
|
List.copyOf(centerline)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (settled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lastWidth(SurfaceCourseResult result) {
|
||||||
|
return Math.max(1, result.lastWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
record TributaryBuild(RiverCourse course, HydrologyCandidateRejection rejection, int rejectionDetail) {
|
||||||
|
private static TributaryBuild rejected(HydrologyCandidateRejection rejection, int detail) {
|
||||||
|
return new TributaryBuild(null, rejection, detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TributaryBuild buildSurfaceTributaryAt(
|
||||||
|
SurfaceCourseDraft draft,
|
||||||
|
int joinIndex,
|
||||||
|
int stemIndex,
|
||||||
|
List<HydrologyPoint> stemStations,
|
||||||
|
List<Integer> stemWidths,
|
||||||
|
Set<Long> stemEdges
|
||||||
|
) {
|
||||||
|
HydrologyCoursePath path = draft.path();
|
||||||
|
HydrologyPoint joinStation = stemStations.get(stemIndex);
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>(path.points().subList(0, joinIndex + 1));
|
||||||
|
HydrologyPoint last = points.getLast();
|
||||||
|
if (last.x() != joinStation.x() || last.z() != joinStation.z()) {
|
||||||
|
points.add(new HydrologyPoint(joinStation.x(), last.y(), joinStation.z()));
|
||||||
|
}
|
||||||
|
LinkedHashSet<DrainageEdge> ownedEdges = new LinkedHashSet<>();
|
||||||
|
for (DrainageEdge edge : path.pairEdges().subList(0, joinIndex)) {
|
||||||
|
if (!stemEdges.contains(edge.id())) {
|
||||||
|
ownedEdges.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownedEdges.isEmpty()) {
|
||||||
|
return TributaryBuild.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT, joinIndex);
|
||||||
|
}
|
||||||
|
int stemHead = joinStation.y();
|
||||||
|
SurfaceCourseResult result = planner.tributaryCourseBuilder.build(
|
||||||
|
planner.worldSeed,
|
||||||
|
draft.courseId(),
|
||||||
|
draft.profileKey(),
|
||||||
|
List.copyOf(points),
|
||||||
|
SurfaceTerminal.TRIBUTARY,
|
||||||
|
stemHead
|
||||||
|
);
|
||||||
|
if (!result.accepted()) {
|
||||||
|
return TributaryBuild.rejected(result.rejection(), result.rejectionDetail());
|
||||||
|
}
|
||||||
|
if (path.organicSurfaceRequired()) {
|
||||||
|
// The reach is judged without its approach into the stem: the turn a tributary makes to
|
||||||
|
// enter its river is junction geometry, not a bend in the river itself.
|
||||||
|
double approachRadius = 3D * lastWidth(result) + 8D;
|
||||||
|
int judged = joinIndex;
|
||||||
|
while (judged > 0 && path.points().get(judged).distanceSquared2D(joinStation) <= approachRadius * approachRadius) {
|
||||||
|
judged--;
|
||||||
|
}
|
||||||
|
int shapeRejection = judged >= 2 ? planner.surfaceCourses.surfaceShapeRejection(path.points().subList(0, judged + 1), true) : 0;
|
||||||
|
if (shapeRejection != 0) {
|
||||||
|
return TributaryBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, shapeRejection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
|
||||||
|
// Where the two channels touch, the tributary must sit at the lowest stem water it reaches.
|
||||||
|
double touch = stemWidths.get(stemIndex) + lastWidth(result) + 1D;
|
||||||
|
for (int index = 0; index < stemStations.size(); index++) {
|
||||||
|
HydrologyPoint station = stemStations.get(index);
|
||||||
|
if (station.distanceSquared2D(joinStation) <= touch * touch) {
|
||||||
|
stemHead = Math.min(stemHead, station.y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (segments.getLast().downstreamHeadY() < stemHead) {
|
||||||
|
int shortfall = stemHead - segments.getLast().downstreamHeadY();
|
||||||
|
if (shortfall > TRIBUTARY_BACKWATER_LIMIT) {
|
||||||
|
return TributaryBuild.rejected(HydrologyCandidateRejection.SURFACE_HEAD_RANGE, shortfall);
|
||||||
|
}
|
||||||
|
// The stem's water stands a little above the tributary's floor at the junction: the tail of
|
||||||
|
// the tributary backs up to the stem's level, a short still reach the bank lip contains.
|
||||||
|
backwater(segments, stemHead);
|
||||||
|
} else if (segments.getLast().downstreamHeadY() > stemHead) {
|
||||||
|
// The tributary hangs above its river: its last stretch is cut down to the stem's water,
|
||||||
|
// a block per cascade run, so the channel meets the river at its level and not above
|
||||||
|
// the stem's shore.
|
||||||
|
levelApproach(segments, stemHead);
|
||||||
|
}
|
||||||
|
HydraulicSegment lastSegment = segments.getLast();
|
||||||
|
HydrologyPoint end = lastSegment.end();
|
||||||
|
if (end.x() != joinStation.x() || end.z() != joinStation.z() || lastSegment.downstreamHeadY() != stemHead) {
|
||||||
|
// The hop onto the stem is graded like any in-course drop, so the water steps down block
|
||||||
|
// by block into the river instead of standing above the stem's shore.
|
||||||
|
int drop = lastSegment.downstreamHeadY() - stemHead;
|
||||||
|
HydrologyFeatureType type = drop == 0
|
||||||
|
? HydrologyFeatureType.SURFACE_POOL
|
||||||
|
: drop >= planner.settings.hydraulics().waterfallMinimumDrop()
|
||||||
|
? HydrologyFeatureType.WATERFALL
|
||||||
|
: drop == 1 ? HydrologyFeatureType.RIFFLE : HydrologyFeatureType.CASCADE;
|
||||||
|
boolean graded = planner.segments.addHydraulicSegments(
|
||||||
|
draft.courseId(),
|
||||||
|
TRIBUTARY_JUNCTION_PAIR_INDEX,
|
||||||
|
type,
|
||||||
|
lastSegment.downstreamHeadY(),
|
||||||
|
stemHead,
|
||||||
|
lastSegment.width(),
|
||||||
|
lastSegment.depth(),
|
||||||
|
List.of(end, new HydrologyPoint(joinStation.x(), stemHead, joinStation.z())),
|
||||||
|
true,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
if (!graded) {
|
||||||
|
return TributaryBuild.rejected(HydrologyCandidateRejection.SURFACE_DROP_UNSUPPORTED, drop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<DrainageEdge> edges = List.copyOf(ownedEdges);
|
||||||
|
return new TributaryBuild(new RiverCourse(
|
||||||
|
draft.courseId(),
|
||||||
|
RiverCourseType.SURFACE,
|
||||||
|
OptionalLong.of(draft.source().id()),
|
||||||
|
OptionalLong.of(path.outlet().id()),
|
||||||
|
draft.profileKey(),
|
||||||
|
planner.segments.maximumSurfaceDischarge(edges),
|
||||||
|
edges,
|
||||||
|
segments
|
||||||
|
), null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addTributaryDiagnostic(
|
||||||
|
long courseId,
|
||||||
|
HydrologyPoint sourcePoint,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
int detail,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(courseId, HydrologySourcePlanner.DIAGNOSTIC_SALT, rejection.ordinal(), 0x5452494255544152L),
|
||||||
|
HydrologyCandidateKind.TRIBUTARY,
|
||||||
|
HydrologyFeatureType.SURFACE_POOL,
|
||||||
|
sourcePoint,
|
||||||
|
rejection,
|
||||||
|
detail
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse preferredTrunkCourse(RiverCourse first, RiverCourse second) {
|
||||||
|
double firstLength = edgeLength(first.drainageEdges(), first.drainageEdges().size());
|
||||||
|
double secondLength = edgeLength(second.drainageEdges(), second.drainageEdges().size());
|
||||||
|
if (firstLength != secondLength) {
|
||||||
|
return firstLength > secondLength ? first : second;
|
||||||
|
}
|
||||||
|
if (first.discharge() != second.discharge()) {
|
||||||
|
return first.discharge() > second.discharge() ? first : second;
|
||||||
|
}
|
||||||
|
return Long.compareUnsigned(first.id(), second.id()) <= 0 ? first : second;
|
||||||
|
}
|
||||||
|
|
||||||
|
double edgeLength(List<DrainageEdge> edges, int edgeCount) {
|
||||||
|
double length = 0D;
|
||||||
|
for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) {
|
||||||
|
List<HydrologyPoint> centerline = edges.get(edgeIndex).centerline();
|
||||||
|
for (int pointIndex = 1; pointIndex < centerline.size(); pointIndex++) {
|
||||||
|
HydrologyPoint previous = centerline.get(pointIndex - 1);
|
||||||
|
HydrologyPoint current = centerline.get(pointIndex);
|
||||||
|
length += StrictMath.hypot(current.x() - previous.x(), current.z() - previous.z());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
int retainedSegmentCount(List<HydraulicSegment> segments, HydrologyPoint boundary) {
|
||||||
|
for (int segmentIndex = 0; segmentIndex < segments.size(); segmentIndex++) {
|
||||||
|
HydrologyPoint end = segments.get(segmentIndex).centerline().getLast();
|
||||||
|
if (end.x() == boundary.x() && end.z() == boundary.z()) {
|
||||||
|
return segmentIndex + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse preferredOutletContinuation(RiverCourse first, RiverCourse second) {
|
||||||
|
boolean firstSurfaceSinkhole = first.surfaceSinkholeContinuation();
|
||||||
|
boolean secondSurfaceSinkhole = second.surfaceSinkholeContinuation();
|
||||||
|
if (firstSurfaceSinkhole != secondSurfaceSinkhole) {
|
||||||
|
return firstSurfaceSinkhole ? first : second;
|
||||||
|
}
|
||||||
|
return Long.compareUnsigned(first.id(), second.id()) <= 0 ? first : second;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasInlandOutletContinuation(RiverCourse course) {
|
||||||
|
return course.surfaceSinkholeContinuation()
|
||||||
|
|| course.segments().getLast().type() == HydrologyFeatureType.INLAND_GROTTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a later underground draft to the stem's outlet as a tributary: the path is cut at the first
|
||||||
|
* point the stem's passage already runs through, and the passage is solved with the stem's fluid
|
||||||
|
* level there as its outlet level, so it joins the stem at or above its water.
|
||||||
|
*/
|
||||||
|
RiverCourse buildUndergroundTributary(
|
||||||
|
UndergroundCourseDraft draft,
|
||||||
|
RiverCourse stem,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
HydrologyCoursePath path = draft.path();
|
||||||
|
HashSet<Long> stemEdges = new HashSet<>();
|
||||||
|
for (DrainageEdge edge : stem.drainageEdges()) {
|
||||||
|
stemEdges.add(edge.id());
|
||||||
|
}
|
||||||
|
HashMap<Long, Integer> stemHeads = new HashMap<>();
|
||||||
|
for (HydraulicSegment segment : stem.segments()) {
|
||||||
|
for (HydrologyPoint point : segment.centerline()) {
|
||||||
|
stemHeads.putIfAbsent(RiverFootprint.pack(point.x(), point.z()), point.y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int joinIndex = -1;
|
||||||
|
for (int index = 0; index < path.points().size(); index++) {
|
||||||
|
HydrologyPoint point = path.points().get(index);
|
||||||
|
boolean sharedEdge = index > 0 && stemEdges.contains(path.pairEdges().get(index - 1).id());
|
||||||
|
if (sharedEdge || stemHeads.containsKey(RiverFootprint.pack(point.x(), point.z()))) {
|
||||||
|
joinIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (joinIndex < 1 || !stemHeads.containsKey(RiverFootprint.pack(
|
||||||
|
path.points().get(joinIndex).x(), path.points().get(joinIndex).z()))) {
|
||||||
|
planner.undergroundCourses.addCompiledSourceDiagnostic(draft.source(), false, draft.courseId(),
|
||||||
|
joinIndex < 0 ? HydrologyCandidateRejection.NO_DRAINAGE_PATH : HydrologyCandidateRejection.COURSE_TOO_SHORT,
|
||||||
|
diagnostics);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<HydrologyPoint> points = path.points().subList(0, joinIndex + 1);
|
||||||
|
List<DrainageEdge> pairEdges = path.pairEdges().subList(0, joinIndex);
|
||||||
|
LinkedHashSet<DrainageEdge> ownedEdges = new LinkedHashSet<>();
|
||||||
|
for (DrainageEdge edge : pairEdges) {
|
||||||
|
if (!stemEdges.contains(edge.id())) {
|
||||||
|
ownedEdges.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ownedEdges.isEmpty()
|
||||||
|
|| planner.surfaceCourses.centerlineLength(points) < planner.settings.routing().minimumUndergroundCourseLength() / 2D) {
|
||||||
|
planner.undergroundCourses.addCompiledSourceDiagnostic(draft.source(), false, draft.courseId(),
|
||||||
|
HydrologyCandidateRejection.COURSE_TOO_SHORT, joinIndex, diagnostics);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
HydrologyPoint junction = points.getLast();
|
||||||
|
HydrologyCoursePath junctionPath = new HydrologyCoursePath(
|
||||||
|
List.copyOf(points),
|
||||||
|
List.copyOf(pairEdges),
|
||||||
|
List.copyOf(ownedEdges),
|
||||||
|
path.outlet(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
junction
|
||||||
|
);
|
||||||
|
int junctionHead = stemHeads.get(RiverFootprint.pack(junction.x(), junction.z()));
|
||||||
|
return planner.undergroundCourses.buildUndergroundCourse(draft, junctionPath, junctionHead, diagnostics);
|
||||||
|
}
|
||||||
|
}
|
||||||
+546
@@ -0,0 +1,546 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
|
final class HydrologyUndergroundCoursePlanner {
|
||||||
|
private final HydrologyPlanner planner;
|
||||||
|
|
||||||
|
HydrologyUndergroundCoursePlanner(HydrologyPlanner planner) {
|
||||||
|
this.planner = planner;
|
||||||
|
}
|
||||||
|
|
||||||
|
void compileUndergroundCourses(
|
||||||
|
HydrologyTileKey key,
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan routing,
|
||||||
|
CompiledGraph graph,
|
||||||
|
List<Integer> sources,
|
||||||
|
List<RiverCourse> courses,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
LinkedHashMap<Long, ArrayList<UndergroundCourseDraft>> draftsByOutlet = new LinkedHashMap<>();
|
||||||
|
for (int sourceIndex : sources) {
|
||||||
|
HydrologyGridNode source = grid.node(sourceIndex);
|
||||||
|
long courseId = HydrologyHash.mix(planner.worldSeed, HydrologySurfaceCoursePlanner.COURSE_SALT, HydrologySourcePlanner.UNDERGROUND_SOURCE_SALT, source.id());
|
||||||
|
HydrologyCoursePath path = planner.surfaceCourses.coursePath(null, routing, graph, sourceIndex, false);
|
||||||
|
if (path == null) {
|
||||||
|
addCompiledSourceDiagnostic(
|
||||||
|
source,
|
||||||
|
false,
|
||||||
|
courseId,
|
||||||
|
HydrologyCandidateRejection.NO_DRAINAGE_PATH,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
draftsByOutlet.computeIfAbsent(path.outlet().id(), (Long ignored) -> new ArrayList<>())
|
||||||
|
.add(new UndergroundCourseDraft(source, courseId, path));
|
||||||
|
}
|
||||||
|
for (ArrayList<UndergroundCourseDraft> outletDrafts : draftsByOutlet.values()) {
|
||||||
|
outletDrafts.sort(Comparator
|
||||||
|
.comparingDouble((UndergroundCourseDraft draft) -> planner.tributaries.edgeLength(
|
||||||
|
draft.path().edges(),
|
||||||
|
draft.path().edges().size()
|
||||||
|
))
|
||||||
|
.reversed()
|
||||||
|
.thenComparingLong(UndergroundCourseDraft::courseId));
|
||||||
|
RiverCourse mainCourse = null;
|
||||||
|
int tributaries = 0;
|
||||||
|
for (UndergroundCourseDraft draft : outletDrafts) {
|
||||||
|
if (mainCourse != null && tributaries >= planner.settings.underground().tributaries()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (mainCourse == null) {
|
||||||
|
RiverCourse course = buildUndergroundCourse(draft, draft.path(), planner.segments.outletHead(draft.path().outlet()), diagnostics);
|
||||||
|
if (course != null) {
|
||||||
|
mainCourse = course;
|
||||||
|
courses.add(course);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
RiverCourse tributary = planner.tributaries.buildUndergroundTributary(draft, mainCourse, diagnostics);
|
||||||
|
if (tributary != null) {
|
||||||
|
courses.add(tributary);
|
||||||
|
tributaries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse buildUndergroundCourse(
|
||||||
|
UndergroundCourseDraft draft,
|
||||||
|
HydrologyCoursePath path,
|
||||||
|
int outletHead,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
HydrologyGridNode source = draft.source();
|
||||||
|
long courseId = draft.courseId();
|
||||||
|
HydrologyPoint sourcePoint = path.points().getFirst();
|
||||||
|
HydrologyPoint trunkPoint = path.points().getLast();
|
||||||
|
HydrologyTerrainSample trunkTerrain = Objects.requireNonNull(
|
||||||
|
planner.sampleDetailed(trunkPoint.x(), trunkPoint.z()),
|
||||||
|
"Hydrology underground trunk left sampled terrain"
|
||||||
|
);
|
||||||
|
String profileKey = planner.segments.chooseProfile(trunkTerrain, path.outlet().id());
|
||||||
|
int styledHead = planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
||||||
|
profileKey,
|
||||||
|
sourcePoint.x(),
|
||||||
|
sourcePoint.z(),
|
||||||
|
0L,
|
||||||
|
planner.settings.underground().minimumFluidY(),
|
||||||
|
planner.settings.underground().maximumFluidY()
|
||||||
|
);
|
||||||
|
if (outletHead > planner.settings.underground().maximumFluidY()) {
|
||||||
|
addCompiledSourceDiagnostic(
|
||||||
|
source,
|
||||||
|
false,
|
||||||
|
courseId,
|
||||||
|
HydrologyCandidateRejection.OUTLET_LEVEL,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int initialHead = Math.max(styledHead, outletHead);
|
||||||
|
RiverCourse course = buildUndergroundCourse(courseId, profileKey, source, path, initialHead, outletHead);
|
||||||
|
if (course == null) {
|
||||||
|
addCompiledSourceDiagnostic(
|
||||||
|
source,
|
||||||
|
false,
|
||||||
|
courseId,
|
||||||
|
HydrologyCandidateRejection.CAVE_CONTAINMENT,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addCompiledSourceDiagnostic(
|
||||||
|
HydrologyGridNode source,
|
||||||
|
boolean surface,
|
||||||
|
long courseId,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
addCompiledSourceDiagnostic(source, surface, courseId, rejection, 0, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addCompiledSourceDiagnostic(
|
||||||
|
HydrologyGridNode source,
|
||||||
|
boolean surface,
|
||||||
|
long courseId,
|
||||||
|
HydrologyCandidateRejection rejection,
|
||||||
|
int detail,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
int y = surface ? source.terrain().naturalHeight() : source.terrain().caveFluidY();
|
||||||
|
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||||
|
HydrologyHash.mix(courseId, HydrologySourcePlanner.DIAGNOSTIC_SALT, rejection.ordinal()),
|
||||||
|
HydrologyCandidateKind.SOURCE,
|
||||||
|
surface ? HydrologyFeatureType.SURFACE_POOL : HydrologyFeatureType.UNDERGROUND_POOL,
|
||||||
|
new HydrologyPoint(source.x(), y, source.z()),
|
||||||
|
rejection,
|
||||||
|
detail
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
RiverCourse buildUndergroundCourse(
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
HydrologyGridNode source,
|
||||||
|
HydrologyCoursePath path,
|
||||||
|
int initialHead,
|
||||||
|
int outletHead
|
||||||
|
) {
|
||||||
|
int pairCount = path.points().size() - 1;
|
||||||
|
if (pairCount < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int[] heads = new int[path.points().size()];
|
||||||
|
int[] preferredHeads = new int[path.points().size()];
|
||||||
|
int[] widths = new int[pairCount];
|
||||||
|
int[] depths = new int[pairCount];
|
||||||
|
preferredHeads[0] = initialHead;
|
||||||
|
for (int index = 0; index < pairCount; index++) {
|
||||||
|
HydrologyPoint point = path.points().get(index);
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleBasisWithoutSlope(point.x(), point.z());
|
||||||
|
int discharge = Math.max(1, path.pairEdges().get(index).contributingUndergroundSources());
|
||||||
|
widths[index] = planner.segments.scaledDimension(
|
||||||
|
planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.UNDERGROUND_WIDTH,
|
||||||
|
profileKey,
|
||||||
|
point.x(),
|
||||||
|
point.z(),
|
||||||
|
0L,
|
||||||
|
planner.settings.underground().minimumWidth(),
|
||||||
|
planner.settings.underground().maximumWidth()
|
||||||
|
),
|
||||||
|
planner.settings.underground().minimumWidth(),
|
||||||
|
planner.settings.underground().maximumWidth(),
|
||||||
|
discharge,
|
||||||
|
terrain.widthMultiplier()
|
||||||
|
);
|
||||||
|
depths[index] = planner.segments.scaledDimension(
|
||||||
|
planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.UNDERGROUND_DEPTH,
|
||||||
|
profileKey,
|
||||||
|
point.x(),
|
||||||
|
point.z(),
|
||||||
|
0L,
|
||||||
|
planner.settings.underground().minimumDepth(),
|
||||||
|
planner.settings.underground().maximumDepth()
|
||||||
|
),
|
||||||
|
planner.settings.underground().minimumDepth(),
|
||||||
|
planner.settings.underground().maximumDepth(),
|
||||||
|
discharge,
|
||||||
|
terrain.depthMultiplier()
|
||||||
|
);
|
||||||
|
HydrologyPoint downstreamPoint = path.points().get(index + 1);
|
||||||
|
int caveHead = planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.UNDERGROUND_FLUID_LEVEL,
|
||||||
|
profileKey,
|
||||||
|
downstreamPoint.x(),
|
||||||
|
downstreamPoint.z(),
|
||||||
|
0L,
|
||||||
|
planner.settings.underground().minimumFluidY(),
|
||||||
|
planner.settings.underground().maximumFluidY()
|
||||||
|
);
|
||||||
|
preferredHeads[index + 1] = caveHead;
|
||||||
|
}
|
||||||
|
int[] minimumHeads = undergroundMinimumHeads(path, depths, outletHead);
|
||||||
|
int[] optimisticMaximumHeads = undergroundOptimisticMaximumHeads(path);
|
||||||
|
if (!undergroundHeadsFeasible(minimumHeads, optimisticMaximumHeads, outletHead)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int[] maximumHeads = undergroundMaximumHeads(courseId, profileKey, path, widths);
|
||||||
|
if (!solveUndergroundHeads(preferredHeads, minimumHeads, maximumHeads, outletHead, heads)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
levelUndergroundMouth(path, heads, outletHead);
|
||||||
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(pairCount + 1);
|
||||||
|
for (int index = 0; index < pairCount; index++) {
|
||||||
|
int drop = heads[index] - heads[index + 1];
|
||||||
|
HydrologyFeatureType type = drop > 0
|
||||||
|
? HydrologyFeatureType.UNDERGROUND_DROP
|
||||||
|
: HydrologyFeatureType.UNDERGROUND_POOL;
|
||||||
|
if (!planner.segments.addHydraulicSegments(
|
||||||
|
courseId,
|
||||||
|
index,
|
||||||
|
type,
|
||||||
|
heads[index],
|
||||||
|
heads[index + 1],
|
||||||
|
widths[index],
|
||||||
|
depths[index],
|
||||||
|
List.of(
|
||||||
|
HydrologyPlanner.withY(path.points().get(index), heads[index]),
|
||||||
|
HydrologyPlanner.withY(path.points().get(index + 1), heads[index + 1])
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
segments
|
||||||
|
)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
planner.segments.appendOutletSegments(
|
||||||
|
RiverCourseType.UNDERGROUND,
|
||||||
|
courseId,
|
||||||
|
path,
|
||||||
|
heads[heads.length - 1],
|
||||||
|
widths,
|
||||||
|
depths,
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
int discharge = planner.segments.maximumUndergroundDischarge(path.edges());
|
||||||
|
return new RiverCourse(
|
||||||
|
courseId,
|
||||||
|
RiverCourseType.UNDERGROUND,
|
||||||
|
OptionalLong.of(source.id()),
|
||||||
|
OptionalLong.of(path.outlet().id()),
|
||||||
|
profileKey,
|
||||||
|
discharge,
|
||||||
|
path.edges(),
|
||||||
|
segments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] undergroundMaximumHeads(
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
HydrologyCoursePath path,
|
||||||
|
int[] widths
|
||||||
|
) {
|
||||||
|
int[] segmentCaps = new int[widths.length];
|
||||||
|
for (int index = 0; index < widths.length; index++) {
|
||||||
|
int radius = Math.max(1, widths[index] / 2);
|
||||||
|
segmentCaps[index] = undergroundSegmentCap(
|
||||||
|
courseId,
|
||||||
|
profileKey,
|
||||||
|
path.points().get(index),
|
||||||
|
path.points().get(index + 1),
|
||||||
|
radius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nodeMaximumHeads(segmentCaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] undergroundOptimisticMaximumHeads(HydrologyCoursePath path) {
|
||||||
|
int segmentCount = path.points().size() - 1;
|
||||||
|
int[] segmentCaps = new int[segmentCount];
|
||||||
|
Arrays.fill(segmentCaps, Integer.MAX_VALUE);
|
||||||
|
int minimumHeadroom = planner.settings.underground().minimumHeadroom();
|
||||||
|
for (int index = 0; index < segmentCount; index++) {
|
||||||
|
HydrologyPoint start = path.points().get(index);
|
||||||
|
HydrologyPoint end = path.points().get(index + 1);
|
||||||
|
segmentCaps[index] = Math.min(
|
||||||
|
optimisticUndergroundSegmentCap(start, minimumHeadroom),
|
||||||
|
optimisticUndergroundSegmentCap(end, minimumHeadroom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nodeMaximumHeads(segmentCaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
int optimisticUndergroundSegmentCap(HydrologyPoint point, int minimumHeadroom) {
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
|
||||||
|
return terrain == null || terrain.ocean()
|
||||||
|
? Integer.MAX_VALUE
|
||||||
|
: terrain.naturalHeight() - minimumHeadroom - planner.settings.underground().minimumRockCover();
|
||||||
|
}
|
||||||
|
|
||||||
|
int undergroundSegmentCap(
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
HydrologyPoint start,
|
||||||
|
HydrologyPoint end,
|
||||||
|
int radius
|
||||||
|
) {
|
||||||
|
int minimumHeadroom = planner.settings.underground().minimumHeadroom();
|
||||||
|
int maximumHeadroom = planner.settings.underground().maximumHeadroom();
|
||||||
|
UndergroundSegmentCapKey key = new UndergroundSegmentCapKey(
|
||||||
|
courseId,
|
||||||
|
profileKey,
|
||||||
|
start.x(),
|
||||||
|
start.z(),
|
||||||
|
end.x(),
|
||||||
|
end.z(),
|
||||||
|
radius,
|
||||||
|
minimumHeadroom,
|
||||||
|
maximumHeadroom
|
||||||
|
);
|
||||||
|
HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get();
|
||||||
|
if (samples != null) {
|
||||||
|
Integer cached = samples.undergroundSegmentCaps.get(key);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int segmentCap = Integer.MAX_VALUE;
|
||||||
|
List<HydrologyPoint> centerline = planner.segments.line(start, end, 1);
|
||||||
|
for (HydrologyPoint point : centerline) {
|
||||||
|
int headroom = planner.sampleGeometry(
|
||||||
|
HydrologyGeometrySampler.Field.UNDERGROUND_HEADROOM,
|
||||||
|
profileKey,
|
||||||
|
point.x(),
|
||||||
|
point.z(),
|
||||||
|
courseId,
|
||||||
|
minimumHeadroom,
|
||||||
|
maximumHeadroom
|
||||||
|
);
|
||||||
|
int minimumLandHeight = minimumLandHeightWithinRadius(point.x(), point.z(), radius);
|
||||||
|
if (minimumLandHeight != Integer.MAX_VALUE) {
|
||||||
|
segmentCap = Math.min(segmentCap, minimumLandHeight - headroom - planner.settings.underground().minimumRockCover());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (samples != null) {
|
||||||
|
samples.undergroundSegmentCaps.put(key, segmentCap);
|
||||||
|
}
|
||||||
|
return segmentCap;
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] nodeMaximumHeads(int[] segmentCaps) {
|
||||||
|
int[] maximumHeads = new int[segmentCaps.length + 1];
|
||||||
|
maximumHeads[0] = segmentCaps[0];
|
||||||
|
for (int index = 1; index < maximumHeads.length - 1; index++) {
|
||||||
|
maximumHeads[index] = Math.min(segmentCaps[index - 1], segmentCaps[index]);
|
||||||
|
}
|
||||||
|
maximumHeads[maximumHeads.length - 1] = segmentCaps[segmentCaps.length - 1];
|
||||||
|
return maximumHeads;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minimumLandHeightWithinRadius(int x, int z, int radius) {
|
||||||
|
HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get();
|
||||||
|
MinimumLandHeightKey key = new MinimumLandHeightKey(x, z, radius);
|
||||||
|
if (samples != null) {
|
||||||
|
Integer cached = samples.minimumLandHeights.get(key);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyGridOffset> offsets = samples == null
|
||||||
|
? radialOffsets(radius)
|
||||||
|
: samples.radialOffsets.computeIfAbsent(radius, this::radialOffsets);
|
||||||
|
int minimumHeight = Integer.MAX_VALUE;
|
||||||
|
for (HydrologyGridOffset offset : offsets) {
|
||||||
|
HydrologyTerrainSample terrain = planner.sampleLandBasis(x + offset.x(), z + offset.z());
|
||||||
|
if (terrain == null || terrain.ocean()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
minimumHeight = Math.min(minimumHeight, terrain.naturalHeight());
|
||||||
|
}
|
||||||
|
if (samples != null) {
|
||||||
|
samples.minimumLandHeights.put(key, minimumHeight);
|
||||||
|
}
|
||||||
|
return minimumHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HydrologyGridOffset> radialOffsets(int radius) {
|
||||||
|
int blockRadius = (int) StrictMath.ceil(radius + 0.25D);
|
||||||
|
ArrayList<HydrologyGridOffset> offsets = new ArrayList<>();
|
||||||
|
for (int deltaZ = -blockRadius; deltaZ <= blockRadius; deltaZ++) {
|
||||||
|
for (int deltaX = -blockRadius; deltaX <= blockRadius; deltaX++) {
|
||||||
|
if (StrictMath.hypot(deltaX, deltaZ) <= radius + 0.25D) {
|
||||||
|
offsets.add(new HydrologyGridOffset(deltaX, deltaZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(offsets);
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] undergroundMinimumHeads(HydrologyCoursePath path, int[] depths, int outletHead) {
|
||||||
|
int[] minimumHeads = new int[path.points().size()];
|
||||||
|
for (int index = 0; index < minimumHeads.length; index++) {
|
||||||
|
int depth = index == 0
|
||||||
|
? depths[0]
|
||||||
|
: index == depths.length ? depths[depths.length - 1] : Math.max(depths[index - 1], depths[index]);
|
||||||
|
depth = Math.max(depth, planner.settings.geometry().drops().maximumBasinDepth());
|
||||||
|
long worldFloorHead = (long) planner.minimumY + depth + planner.settings.underground().minimumFloorCover();
|
||||||
|
int boundedWorldFloor = worldFloorHead < Integer.MIN_VALUE
|
||||||
|
? Integer.MIN_VALUE
|
||||||
|
: worldFloorHead > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) worldFloorHead;
|
||||||
|
int configuredMinimum = index == minimumHeads.length - 1
|
||||||
|
? outletHead
|
||||||
|
: planner.settings.underground().minimumFluidY();
|
||||||
|
minimumHeads[index] = Math.max(outletHead, Math.max(configuredMinimum, boundedWorldFloor));
|
||||||
|
}
|
||||||
|
return minimumHeads;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean undergroundHeadsFeasible(
|
||||||
|
int[] minimumHeads,
|
||||||
|
int[] maximumHeads,
|
||||||
|
int outletHead
|
||||||
|
) {
|
||||||
|
if (minimumHeads.length == 0 || minimumHeads.length != maximumHeads.length) {
|
||||||
|
throw new IllegalArgumentException("Underground head arrays must have the same positive length.");
|
||||||
|
}
|
||||||
|
int index = minimumHeads.length - 1;
|
||||||
|
int requiredHead = Math.max(minimumHeads[index], outletHead);
|
||||||
|
if (requiredHead > maximumHeads[index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (index--; index >= 0; index--) {
|
||||||
|
requiredHead = Math.max(minimumHeads[index], requiredHead);
|
||||||
|
if (requiredHead > maximumHeads[index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean solveUndergroundHeads(
|
||||||
|
int[] preferredHeads,
|
||||||
|
int[] minimumHeads,
|
||||||
|
int[] maximumHeads,
|
||||||
|
int outletHead,
|
||||||
|
int[] solvedHeads
|
||||||
|
) {
|
||||||
|
int length = preferredHeads.length;
|
||||||
|
if (length == 0
|
||||||
|
|| minimumHeads.length != length
|
||||||
|
|| maximumHeads.length != length
|
||||||
|
|| solvedHeads.length != length) {
|
||||||
|
throw new IllegalArgumentException("Underground head arrays must have the same positive length.");
|
||||||
|
}
|
||||||
|
int[] requiredHeads = new int[length];
|
||||||
|
requiredHeads[length - 1] = Math.max(minimumHeads[length - 1], outletHead);
|
||||||
|
if (requiredHeads[length - 1] > maximumHeads[length - 1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int index = length - 2; index >= 0; index--) {
|
||||||
|
requiredHeads[index] = Math.max(minimumHeads[index], requiredHeads[index + 1]);
|
||||||
|
if (requiredHeads[index] > maximumHeads[index]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
solvedHeads[0] = HydrologyPlanner.clamp(preferredHeads[0], requiredHeads[0], maximumHeads[0]);
|
||||||
|
for (int index = 1; index < length; index++) {
|
||||||
|
int maximum = Math.min(maximumHeads[index], solvedHeads[index - 1]);
|
||||||
|
if (requiredHeads[index] > maximum) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
solvedHeads[index] = HydrologyPlanner.clamp(preferredHeads[index], requiredHeads[index], maximum);
|
||||||
|
}
|
||||||
|
solvedHeads[length - 1] = outletHead;
|
||||||
|
return outletHead >= minimumHeads[length - 1]
|
||||||
|
&& outletHead <= maximumHeads[length - 1]
|
||||||
|
&& (length == 1 || solvedHeads[length - 2] >= outletHead);
|
||||||
|
}
|
||||||
|
|
||||||
|
HydrologyCoursePath undergroundCoursePath(List<DrainageEdge> edges, RiverOutlet outlet) {
|
||||||
|
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||||
|
ArrayList<DrainageEdge> pairEdges = new ArrayList<>();
|
||||||
|
for (DrainageEdge edge : edges) {
|
||||||
|
for (int pointIndex = 0; pointIndex < edge.centerline().size(); pointIndex++) {
|
||||||
|
HydrologyPoint point = edge.centerline().get(pointIndex);
|
||||||
|
if (points.isEmpty()) {
|
||||||
|
points.add(point);
|
||||||
|
} else if (pointIndex > 0) {
|
||||||
|
points.add(point);
|
||||||
|
pairEdges.add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return points.size() >= 2 && pairEdges.size() == points.size() - 1
|
||||||
|
? new HydrologyCoursePath(
|
||||||
|
List.copyOf(points),
|
||||||
|
List.copyOf(pairEdges),
|
||||||
|
List.copyOf(edges),
|
||||||
|
outlet,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void levelUndergroundMouth(HydrologyCoursePath path, int[] heads, int outletHead) {
|
||||||
|
heads[heads.length - 1] = outletHead;
|
||||||
|
int levelingDistance = planner.settings.outlets().mouthLevelingDistance();
|
||||||
|
if (levelingDistance == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double remaining = 0D;
|
||||||
|
int initialHead = heads[0];
|
||||||
|
for (int index = path.points().size() - 2; index >= 0; index--) {
|
||||||
|
HydrologyPoint current = path.points().get(index);
|
||||||
|
HydrologyPoint downstream = path.points().get(index + 1);
|
||||||
|
remaining += StrictMath.hypot(downstream.x() - current.x(), downstream.z() - current.z());
|
||||||
|
if (remaining > levelingDistance) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
double ratio = remaining / levelingDistance;
|
||||||
|
int cap = outletHead + (int) StrictMath.ceil((initialHead - outletHead) * ratio);
|
||||||
|
heads[index] = Math.min(heads[index], cap);
|
||||||
|
}
|
||||||
|
for (int index = 1; index < heads.length; index++) {
|
||||||
|
heads[index] = Math.min(heads[index - 1], heads[index]);
|
||||||
|
}
|
||||||
|
heads[heads.length - 1] = outletHead;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record MinimumLandHeightKey(int x, int z, int radius) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record OutletCandidate(int landIndex, int oceanIndex, RiverOutlet outlet) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record OutletCourseKey(long outletId) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record RefinedEdgeKey(
|
||||||
|
long upstreamId,
|
||||||
|
long downstreamId,
|
||||||
|
int upstreamX,
|
||||||
|
int upstreamZ,
|
||||||
|
int downstreamX,
|
||||||
|
int downstreamZ,
|
||||||
|
int continuationX,
|
||||||
|
int continuationZ,
|
||||||
|
int transverseCandidates
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record RouteCandidate(
|
||||||
|
HydrologyPoint point,
|
||||||
|
double offset,
|
||||||
|
double localScore,
|
||||||
|
double terrainScore,
|
||||||
|
RouteDirection tangent,
|
||||||
|
boolean terrainResolved
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record RouteDirection(double x, double z) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record RoutePosition(double x, double z, double baseX, double baseZ, RouteDirection tangent) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record RouteTerrainEstimate(int height, double terrainScore) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
record RoutingContextLoad(Thread owner, CompletableFuture<SourceRoutingContext> future) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record RoutingQueueEntry(
|
||||||
|
int nodeIndex,
|
||||||
|
double potential,
|
||||||
|
int outletIndex
|
||||||
|
)
|
||||||
|
implements Comparable<RoutingQueueEntry> {
|
||||||
|
@Override
|
||||||
|
public int compareTo(RoutingQueueEntry other) {
|
||||||
|
int potentialComparison = Double.compare(potential, other.potential);
|
||||||
|
if (potentialComparison != 0) {
|
||||||
|
return potentialComparison;
|
||||||
|
}
|
||||||
|
int nodeComparison = Integer.compare(nodeIndex, other.nodeIndex);
|
||||||
|
return nodeComparison != 0 ? nodeComparison : Integer.compare(outletIndex, other.outletIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.IntPredicate;
|
||||||
|
|
||||||
|
final class SourceAdmissionSelection {
|
||||||
|
final int targetCount;
|
||||||
|
final List<Integer> selectedCandidateIndices;
|
||||||
|
final boolean[] selectedCandidates;
|
||||||
|
final boolean[] spacingRejectedCandidates;
|
||||||
|
final boolean[] evaluatedCandidates;
|
||||||
|
final IntPredicate globallyAdmitted;
|
||||||
|
final int[] outletIndices;
|
||||||
|
final int maximumCoursesPerOutlet;
|
||||||
|
|
||||||
|
SourceAdmissionSelection(
|
||||||
|
int targetCount,
|
||||||
|
List<Integer> selectedCandidateIndices,
|
||||||
|
boolean[] selectedCandidates,
|
||||||
|
boolean[] spacingRejectedCandidates,
|
||||||
|
boolean[] evaluatedCandidates,
|
||||||
|
IntPredicate globallyAdmitted,
|
||||||
|
int[] outletIndices,
|
||||||
|
int maximumCoursesPerOutlet
|
||||||
|
) {
|
||||||
|
if (outletIndices.length != selectedCandidates.length || maximumCoursesPerOutlet < 1) {
|
||||||
|
throw new IllegalArgumentException("Source outlet admission bounds are invalid.");
|
||||||
|
}
|
||||||
|
this.targetCount = targetCount;
|
||||||
|
this.selectedCandidateIndices = List.copyOf(selectedCandidateIndices);
|
||||||
|
this.selectedCandidates = selectedCandidates;
|
||||||
|
this.spacingRejectedCandidates = spacingRejectedCandidates;
|
||||||
|
this.evaluatedCandidates = evaluatedCandidates;
|
||||||
|
this.globallyAdmitted = globallyAdmitted;
|
||||||
|
this.outletIndices = outletIndices;
|
||||||
|
this.maximumCoursesPerOutlet = maximumCoursesPerOutlet;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> selectedCandidateIndices() {
|
||||||
|
return selectedCandidateIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetCount() {
|
||||||
|
return targetCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean selected(int candidateIndex) {
|
||||||
|
return selectedCandidates[candidateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean rejectedBySpacing(int candidateIndex) {
|
||||||
|
return spacingRejectedCandidates[candidateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean admitted(int candidateIndex) {
|
||||||
|
if (!evaluatedCandidates[candidateIndex]) {
|
||||||
|
evaluatedCandidates[candidateIndex] = true;
|
||||||
|
spacingRejectedCandidates[candidateIndex] = !globallyAdmitted.test(candidateIndex);
|
||||||
|
}
|
||||||
|
return !spacingRejectedCandidates[candidateIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean outletQuotaAvailable(int candidateIndex, List<Integer> selectedIndices) {
|
||||||
|
int outletIndex = outletIndices[candidateIndex];
|
||||||
|
int selectedForOutlet = 0;
|
||||||
|
for (int selectedIndex : selectedIndices) {
|
||||||
|
if (outletIndices[selectedIndex] == outletIndex) {
|
||||||
|
selectedForOutlet++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedForOutlet < maximumCoursesPerOutlet;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record SourceCandidate(
|
||||||
|
int nodeIndex,
|
||||||
|
long stableId,
|
||||||
|
double score,
|
||||||
|
boolean required
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record SourceRoutingContext(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
HydrologyRoutingPlan surfaceRouting,
|
||||||
|
HydrologyRoutingPlan undergroundRouting,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
HydrologyRoutingPlan routing(boolean surface) {
|
||||||
|
return surface ? surfaceRouting : undergroundRouting;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
final class SourceSelection {
|
||||||
|
final boolean surface;
|
||||||
|
final List<SourceCandidate> candidates;
|
||||||
|
final SourceAdmissionSelection admission;
|
||||||
|
final int guaranteedMinimum;
|
||||||
|
final int maximumOptionalRejections;
|
||||||
|
final int targetCount;
|
||||||
|
final ArrayList<Integer> selectedCandidateIndices;
|
||||||
|
final boolean[] selectedCandidates;
|
||||||
|
final boolean[] attemptedCandidates;
|
||||||
|
int rejectedOptionalCandidates;
|
||||||
|
|
||||||
|
SourceSelection(
|
||||||
|
boolean surface,
|
||||||
|
List<SourceCandidate> candidates,
|
||||||
|
SourceAdmissionSelection admission,
|
||||||
|
int guaranteedMinimum,
|
||||||
|
int maximumOptionalRejections
|
||||||
|
) {
|
||||||
|
if (guaranteedMinimum < 0 || guaranteedMinimum > admission.selectedCandidateIndices().size()) {
|
||||||
|
throw new IllegalArgumentException("Guaranteed source minimum is outside the admission bounds.");
|
||||||
|
}
|
||||||
|
if (maximumOptionalRejections < 0) {
|
||||||
|
throw new IllegalArgumentException("Maximum optional source rejections cannot be negative.");
|
||||||
|
}
|
||||||
|
this.surface = surface;
|
||||||
|
this.candidates = List.copyOf(candidates);
|
||||||
|
this.admission = admission;
|
||||||
|
this.guaranteedMinimum = guaranteedMinimum;
|
||||||
|
this.maximumOptionalRejections = maximumOptionalRejections;
|
||||||
|
this.selectedCandidateIndices = new ArrayList<>(admission.selectedCandidateIndices());
|
||||||
|
this.targetCount = admission.targetCount();
|
||||||
|
this.selectedCandidates = new boolean[candidates.size()];
|
||||||
|
this.attemptedCandidates = new boolean[candidates.size()];
|
||||||
|
for (int candidateIndex : selectedCandidateIndices) {
|
||||||
|
selectedCandidates[candidateIndex] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SourceSelection empty(boolean surface) {
|
||||||
|
return new SourceSelection(
|
||||||
|
surface,
|
||||||
|
List.of(),
|
||||||
|
new SourceAdmissionSelection(
|
||||||
|
0,
|
||||||
|
List.of(),
|
||||||
|
new boolean[0],
|
||||||
|
new boolean[0],
|
||||||
|
new boolean[0],
|
||||||
|
(int candidateIndex) -> false,
|
||||||
|
new int[0],
|
||||||
|
Integer.MAX_VALUE
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> selectedNodeIndices() {
|
||||||
|
ArrayList<Integer> selectedNodes = new ArrayList<>(selectedCandidateIndices.size());
|
||||||
|
for (int candidateIndex : selectedCandidateIndices) {
|
||||||
|
selectedNodes.add(candidates.get(candidateIndex).nodeIndex());
|
||||||
|
}
|
||||||
|
return List.copyOf(selectedNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
int candidateCount() {
|
||||||
|
return candidates.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean needsSurfaceFallback() {
|
||||||
|
return surface && targetCount > 0 && selectedCandidateIndices.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasAcceptedSelection() {
|
||||||
|
return !selectedCandidateIndices.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean advanceAfterPublication(List<RiverCourse> acceptedCourses, HydrologySampledGrid grid) {
|
||||||
|
Set<Long> acceptedSourceNodeIds = acceptedSourceNodeIds(acceptedCourses);
|
||||||
|
int acceptedRequired = 0;
|
||||||
|
boolean changed = false;
|
||||||
|
int selectedPosition = 0;
|
||||||
|
while (selectedPosition < selectedCandidateIndices.size()) {
|
||||||
|
int candidateIndex = selectedCandidateIndices.get(selectedPosition);
|
||||||
|
SourceCandidate candidate = candidates.get(candidateIndex);
|
||||||
|
long sourceNodeId = grid.node(candidate.nodeIndex()).id();
|
||||||
|
if (acceptedSourceNodeIds.contains(sourceNodeId)) {
|
||||||
|
if (candidate.required()) {
|
||||||
|
attemptedCandidates[candidateIndex] = true;
|
||||||
|
acceptedRequired++;
|
||||||
|
}
|
||||||
|
selectedPosition++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attemptedCandidates[candidateIndex] = true;
|
||||||
|
if (!candidate.required()) {
|
||||||
|
rejectedOptionalCandidates++;
|
||||||
|
}
|
||||||
|
selectedCandidateIndices.remove(selectedPosition);
|
||||||
|
selectedCandidates[candidateIndex] = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
int requiredVacancies = Math.max(0, guaranteedMinimum - acceptedRequired);
|
||||||
|
for (int vacancy = 0; vacancy < requiredVacancies; vacancy++) {
|
||||||
|
int replacement = nextRequiredCandidate();
|
||||||
|
if (replacement < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
selectedCandidateIndices.add(replacement);
|
||||||
|
selectedCandidates[replacement] = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
while (selectedCandidateIndices.size() < targetCount) {
|
||||||
|
int replacement = nextAdmittedCandidate();
|
||||||
|
if (replacement < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
selectedCandidateIndices.add(replacement);
|
||||||
|
selectedCandidates[replacement] = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> acceptedSourceNodeIds(List<RiverCourse> acceptedCourses) {
|
||||||
|
RiverCourseType expectedType = surface ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND;
|
||||||
|
HashSet<Long> accepted = new HashSet<>();
|
||||||
|
for (RiverCourse course : acceptedCourses) {
|
||||||
|
if (course.type() == expectedType && course.sourceNodeId().isPresent()) {
|
||||||
|
accepted.add(course.sourceNodeId().getAsLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextRequiredCandidate() {
|
||||||
|
for (int candidateIndex = 0; candidateIndex < candidates.size(); candidateIndex++) {
|
||||||
|
SourceCandidate candidate = candidates.get(candidateIndex);
|
||||||
|
if (candidate.required()
|
||||||
|
&& !selectedCandidates[candidateIndex]
|
||||||
|
&& !attemptedCandidates[candidateIndex]
|
||||||
|
&& admission.outletQuotaAvailable(candidateIndex, selectedCandidateIndices)) {
|
||||||
|
return candidateIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextAdmittedCandidate() {
|
||||||
|
if (rejectedOptionalCandidates >= maximumOptionalRejections) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
for (int candidateIndex = 0; candidateIndex < candidates.size(); candidateIndex++) {
|
||||||
|
if (!selectedCandidates[candidateIndex]
|
||||||
|
&& !attemptedCandidates[candidateIndex]
|
||||||
|
&& admission.outletQuotaAvailable(candidateIndex, selectedCandidateIndices)
|
||||||
|
&& admission.admitted(candidateIndex)) {
|
||||||
|
return candidateIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFinalAdmissionDiagnostics(
|
||||||
|
HydrologySampledGrid grid,
|
||||||
|
List<HydrologyDiagnosticCandidate> diagnostics
|
||||||
|
) {
|
||||||
|
for (int candidateIndex = 0; candidateIndex < candidates.size(); candidateIndex++) {
|
||||||
|
if (selectedCandidates[candidateIndex] || attemptedCandidates[candidateIndex]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SourceCandidate candidate = candidates.get(candidateIndex);
|
||||||
|
HydrologySourcePlanner.addSourceDiagnostic(
|
||||||
|
grid.node(candidate.nodeIndex()),
|
||||||
|
surface,
|
||||||
|
candidate.stableId(),
|
||||||
|
admission.rejectedBySpacing(candidateIndex)
|
||||||
|
? HydrologyCandidateRejection.SOURCE_SPACING
|
||||||
|
: HydrologyCandidateRejection.SOURCE_QUOTA,
|
||||||
|
diagnostics
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record SurfaceCourseDraft(
|
||||||
|
HydrologyGridNode source,
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
HydrologyCoursePath path
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
record SurfaceRouteKey(List<HydrologyPoint> guide, long geometryId) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record UndergroundCourseDraft(HydrologyGridNode source, long courseId, HydrologyCoursePath path) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
record UndergroundSegmentCapKey(
|
||||||
|
long courseId,
|
||||||
|
String profileKey,
|
||||||
|
int startX,
|
||||||
|
int startZ,
|
||||||
|
int endX,
|
||||||
|
int endZ,
|
||||||
|
int radius,
|
||||||
|
int minimumHeadroom,
|
||||||
|
int maximumHeadroom
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class HydrologyPlanHashTest {
|
||||||
|
private static final String EXPECTED_PLAN_DIGEST = "de765d4be6c95954f8df344ee2d1dd72c97e8640fc28c92c3ec4717cff0d88e4";
|
||||||
|
|
||||||
|
private static final List<HydrologyTileKey> TILES = List.of(
|
||||||
|
new HydrologyTileKey(0, 0),
|
||||||
|
new HydrologyTileKey(-1, -1),
|
||||||
|
new HydrologyTileKey(0, -1),
|
||||||
|
new HydrologyTileKey(1, 0));
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fullTilePlansAreByteIdentical() {
|
||||||
|
StringBuilder canonical = new StringBuilder();
|
||||||
|
int courses = 0;
|
||||||
|
int columns = 0;
|
||||||
|
for (long seed : new long[]{77L, 19L}) {
|
||||||
|
HydrologyPlannerSettings settings = planSettings();
|
||||||
|
HydrologyPlanner planner = new HydrologyPlanner(seed, settings, rollingCoast(112));
|
||||||
|
for (HydrologyTileKey key : TILES) {
|
||||||
|
HydrologyTile tile = planner.plan(key);
|
||||||
|
courses += tile.courses().size();
|
||||||
|
columns += tile.footprint().columns().size();
|
||||||
|
canonical.append(render(tile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue("planned courses: " + courses, courses > 0);
|
||||||
|
assertTrue("planned columns: " + columns, columns > 0);
|
||||||
|
assertEquals(EXPECTED_PLAN_DIGEST, digest(canonical.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String render(HydrologyTile tile) {
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
out.append("tile ").append(tile.key()).append('\n');
|
||||||
|
out.append("seed ").append(tile.worldSeed()).append('\n');
|
||||||
|
out.append("fingerprint ").append(tile.settingsFingerprint()).append('\n');
|
||||||
|
out.append("size ").append(tile.tileSize()).append('\n');
|
||||||
|
for (DrainageNode node : tile.nodes()) {
|
||||||
|
out.append("node ").append(node).append('\n');
|
||||||
|
}
|
||||||
|
for (DrainageEdge edge : tile.edges()) {
|
||||||
|
out.append("edge ").append(edge).append('\n');
|
||||||
|
}
|
||||||
|
for (RiverOutlet outlet : tile.outlets()) {
|
||||||
|
out.append("outlet ").append(outlet).append('\n');
|
||||||
|
}
|
||||||
|
for (RiverCourse course : tile.courses()) {
|
||||||
|
out.append("course ").append(course).append('\n');
|
||||||
|
}
|
||||||
|
for (HydrologyCavePlan plan : tile.cavePlans()) {
|
||||||
|
out.append("cave ").append(plan).append('\n');
|
||||||
|
}
|
||||||
|
for (HydrologyDiagnosticCandidate diagnostic : tile.diagnosticCandidates()) {
|
||||||
|
out.append("diagnostic ").append(diagnostic).append('\n');
|
||||||
|
}
|
||||||
|
for (HydrologyFeatureRef feature : tile.features()) {
|
||||||
|
out.append("feature ").append(feature).append('\n');
|
||||||
|
}
|
||||||
|
List<Map.Entry<Long, HydrologyColumnSample>> columns =
|
||||||
|
new ArrayList<>(tile.footprint().columns().entrySet());
|
||||||
|
columns.sort(Comparator.comparingLong(Map.Entry::getKey));
|
||||||
|
for (Map.Entry<Long, HydrologyColumnSample> column : columns) {
|
||||||
|
out.append("column ").append(column.getKey()).append(' ').append(column.getValue()).append('\n');
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String digest(String value) {
|
||||||
|
MessageDigest sha256;
|
||||||
|
try {
|
||||||
|
sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
byte[] hash = sha256.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder hex = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte b : hash) {
|
||||||
|
hex.append(Character.forDigit((b >> 4) & 0xF, 16));
|
||||||
|
hex.append(Character.forDigit(b & 0xF, 16));
|
||||||
|
}
|
||||||
|
return hex.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyTerrainSampler rollingCoast(int coastX) {
|
||||||
|
return (int x, int z) -> {
|
||||||
|
if (x >= coastX) {
|
||||||
|
return oceanTerrain();
|
||||||
|
}
|
||||||
|
int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D);
|
||||||
|
return landTerrain(height, x >= 0 && x <= 24);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyTerrainSample landTerrain(int height, boolean source) {
|
||||||
|
return new HydrologyTerrainSample(
|
||||||
|
height,
|
||||||
|
1D,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
72,
|
||||||
|
74,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
source,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
0D,
|
||||||
|
source ? 1D : 0D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
"parent",
|
||||||
|
"surface",
|
||||||
|
"mouth",
|
||||||
|
"shore",
|
||||||
|
"dry",
|
||||||
|
"flooded",
|
||||||
|
List.of("beta", "alpha"),
|
||||||
|
List.of(),
|
||||||
|
Double.NaN,
|
||||||
|
null,
|
||||||
|
Double.NaN,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyTerrainSample oceanTerrain() {
|
||||||
|
return new HydrologyTerrainSample(
|
||||||
|
54,
|
||||||
|
0D,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
30,
|
||||||
|
32,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
0D,
|
||||||
|
0D,
|
||||||
|
0D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
"parent",
|
||||||
|
"surface",
|
||||||
|
"mouth",
|
||||||
|
"shore",
|
||||||
|
"dry",
|
||||||
|
"flooded",
|
||||||
|
List.of("beta", "alpha"),
|
||||||
|
List.of(),
|
||||||
|
Double.NaN,
|
||||||
|
null,
|
||||||
|
Double.NaN,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyPlannerSettings planSettings() {
|
||||||
|
HydrologyPlannerSettings.Source surfaceSources = new HydrologyPlannerSettings.Source(
|
||||||
|
true, 4D, 80, 0, 6, 24);
|
||||||
|
HydrologyPlannerSettings.Source undergroundSources = new HydrologyPlannerSettings.Source(
|
||||||
|
true, 2D, Integer.MIN_VALUE, 0, 4, 32);
|
||||||
|
HydrologyPlannerSettings.ChannelShape stableChannel =
|
||||||
|
HydrologyPlannerSettings.ChannelShape.of(2D, 0D, 0D, 11);
|
||||||
|
HydrologyPlannerSettings.Geometry geometry = new HydrologyPlannerSettings.Geometry(
|
||||||
|
new HydrologyPlannerSettings.Meanders(224, 72, 0D, 0D, 0D, 0, 75D),
|
||||||
|
stableChannel,
|
||||||
|
stableChannel,
|
||||||
|
stableChannel,
|
||||||
|
HydrologyPlannerSettings.Geometry.defaults().drops());
|
||||||
|
return new HydrologyPlannerSettings(
|
||||||
|
63,
|
||||||
|
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||||
|
new HydrologyPlannerSettings.Surface(
|
||||||
|
true,
|
||||||
|
surfaceSources,
|
||||||
|
4,
|
||||||
|
18,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
10,
|
||||||
|
1.5D,
|
||||||
|
HydrologyPlannerSettings.Banks.defaults()),
|
||||||
|
new HydrologyPlannerSettings.Hydraulics(4),
|
||||||
|
HydrologyPlannerSettings.Underground.of(
|
||||||
|
true, undergroundSources, 68, 82, 4, 12, 2, 4, 5, 9, true, 0),
|
||||||
|
HydrologyPlannerSettings.Outlets.of(
|
||||||
|
true,
|
||||||
|
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||||
|
new HydrologyPlannerSettings.Grotto(false, 4, 3, 3, 4096),
|
||||||
|
false,
|
||||||
|
12,
|
||||||
|
32,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
4),
|
||||||
|
geometry,
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
0D,
|
||||||
|
HydrologyPlannerSettings.SeaCaves.disabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -188,7 +188,7 @@ public class HydrologyPlannerGlobalInvariantTest {
|
|||||||
globallyAdmitted[candidateIndex] = random.nextBoolean();
|
globallyAdmitted[candidateIndex] = random.nextBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
HydrologyPlanner.SourceAdmissionSelection lazy = HydrologyPlanner.selectSourceAdmissions(
|
SourceAdmissionSelection lazy = HydrologySourcePlanner.selectSourceAdmissions(
|
||||||
candidateCount,
|
candidateCount,
|
||||||
target,
|
target,
|
||||||
guaranteed,
|
guaranteed,
|
||||||
@@ -208,7 +208,7 @@ public class HydrologyPlannerGlobalInvariantTest {
|
|||||||
public void lazyAdmissionStopsGlobalEvaluationWhenQuotaIsFilled() {
|
public void lazyAdmissionStopsGlobalEvaluationWhenQuotaIsFilled() {
|
||||||
AtomicInteger evaluations = new AtomicInteger();
|
AtomicInteger evaluations = new AtomicInteger();
|
||||||
|
|
||||||
HydrologyPlanner.SourceAdmissionSelection selection = HydrologyPlanner.selectSourceAdmissions(
|
SourceAdmissionSelection selection = HydrologySourcePlanner.selectSourceAdmissions(
|
||||||
256,
|
256,
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
|
|||||||
+6
-6
@@ -331,12 +331,12 @@ public class HydrologyPlannerSettingsTest {
|
|||||||
);
|
);
|
||||||
HydrologyTerrainSample terrainWithoutUndergroundRiverCave = HydrologyTerrainSample.openLand(100, 1D, "parent");
|
HydrologyTerrainSample terrainWithoutUndergroundRiverCave = HydrologyTerrainSample.openLand(100, 1D, "parent");
|
||||||
|
|
||||||
assertFalse(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, 0));
|
assertFalse(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, 0));
|
||||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, -64));
|
assertTrue(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, -64));
|
||||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 4, 3, 0));
|
assertTrue(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 4, 3, 0));
|
||||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 80, 3, 0));
|
assertTrue(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 80, 3, 0));
|
||||||
assertFalse(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 96, 3, 0));
|
assertFalse(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 96, 3, 0));
|
||||||
assertFalse(HydrologyPlanner.deepSiteFits(
|
assertFalse(HydrologyFeatureSitePlanner.deepSiteFits(
|
||||||
HydrologyTerrainSample.ocean(50, "ocean"),
|
HydrologyTerrainSample.ocean(50, "ocean"),
|
||||||
deepFluid,
|
deepFluid,
|
||||||
40,
|
40,
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ public class HydrologyPlannerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static AutoCloseable earlyAdmission(HydrologyPlanner planner, HydrologyTileKey key) throws Exception {
|
private static AutoCloseable earlyAdmission(HydrologyPlanner planner, HydrologyTileKey key) throws Exception {
|
||||||
Class<?> contextType = Class.forName(HydrologyPlanner.class.getName() + "$CrossTileResolutionContext");
|
Class<?> contextType = CrossTileResolutionContext.class;
|
||||||
Constructor<?> contextConstructor = contextType.getDeclaredConstructor(HydrologyTileKey.class, long.class, int.class);
|
Constructor<?> contextConstructor = contextType.getDeclaredConstructor(HydrologyTileKey.class, long.class, int.class);
|
||||||
contextConstructor.setAccessible(true);
|
contextConstructor.setAccessible(true);
|
||||||
Object context = contextConstructor.newInstance(key, 64L, 4096);
|
Object context = contextConstructor.newInstance(key, 64L, 4096);
|
||||||
@@ -631,8 +631,8 @@ public class HydrologyPlannerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void biomeIncisionMultiplierCannotExceedTheConfiguredSurfaceMaximum() {
|
public void biomeIncisionMultiplierCannotExceedTheConfiguredSurfaceMaximum() {
|
||||||
assertEquals(6, HydrologyPlanner.permittedSurfaceIncision(6, 2D));
|
assertEquals(6, HydrologySourcePlanner.permittedSurfaceIncision(6, 2D));
|
||||||
assertEquals(3, HydrologyPlanner.permittedSurfaceIncision(6, 0.5D));
|
assertEquals(3, HydrologySourcePlanner.permittedSurfaceIncision(6, 0.5D));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -709,12 +709,12 @@ public class HydrologyPlannerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void fallbackSourceTargetCannotExceedAvailableOutlets() {
|
public void fallbackSourceTargetCannotExceedAvailableOutlets() {
|
||||||
assertEquals(1, HydrologyPlanner.effectiveSourceTarget(true, 8, 1));
|
assertEquals(1, HydrologySourcePlanner.effectiveSourceTarget(true, 8, 1));
|
||||||
assertEquals(0, HydrologyPlanner.effectiveSourceTarget(true, 8, 0));
|
assertEquals(0, HydrologySourcePlanner.effectiveSourceTarget(true, 8, 0));
|
||||||
assertEquals(8, HydrologyPlanner.effectiveSourceTarget(false, 8, 1));
|
assertEquals(8, HydrologySourcePlanner.effectiveSourceTarget(false, 8, 1));
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> HydrologyPlanner.effectiveSourceTarget(true, -1, 1)
|
() -> HydrologySourcePlanner.effectiveSourceTarget(true, -1, 1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1507,7 +1507,7 @@ public class HydrologyPlannerTest {
|
|||||||
int outlet = random.nextInt(11) - 3;
|
int outlet = random.nextInt(11) - 3;
|
||||||
|
|
||||||
boolean expected = bruteForceUndergroundHeads(minimum, maximum, outlet, 0, Integer.MAX_VALUE);
|
boolean expected = bruteForceUndergroundHeads(minimum, maximum, outlet, 0, Integer.MAX_VALUE);
|
||||||
boolean actual = HydrologyPlanner.solveUndergroundHeads(
|
boolean actual = HydrologyUndergroundCoursePlanner.solveUndergroundHeads(
|
||||||
preferred,
|
preferred,
|
||||||
minimum,
|
minimum,
|
||||||
maximum,
|
maximum,
|
||||||
@@ -3424,31 +3424,31 @@ public class HydrologyPlannerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double anchorScore(HydrologyPlanner planner, HydrologyTerrainSample terrain, int x, int z) throws Exception {
|
private double anchorScore(HydrologyPlanner planner, HydrologyTerrainSample terrain, int x, int z) throws Exception {
|
||||||
Class<?> nodeType = Class.forName(HydrologyPlanner.class.getName() + "$GridNode");
|
Class<?> nodeType = HydrologyGridNode.class;
|
||||||
Constructor<?> constructor = nodeType.getDeclaredConstructor(int.class, int.class, int.class,
|
Constructor<?> constructor = nodeType.getDeclaredConstructor(int.class, int.class, int.class,
|
||||||
int.class, int.class, long.class, HydrologyTerrainSample.class);
|
int.class, int.class, long.class, HydrologyTerrainSample.class);
|
||||||
constructor.setAccessible(true);
|
constructor.setAccessible(true);
|
||||||
Object node = constructor.newInstance(0, 0, 0, 0, 0, 719L, terrain);
|
Object node = constructor.newInstance(0, 0, 0, 0, 0, 719L, terrain);
|
||||||
Method method = HydrologyPlanner.class.getDeclaredMethod("anchorScore", nodeType,
|
Method method = HydrologyRouteGeometry.class.getDeclaredMethod("anchorScore", nodeType,
|
||||||
int.class, int.class, int.class, int.class, double.class);
|
int.class, int.class, int.class, int.class, double.class);
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
return (double) method.invoke(planner, node, x, z, 0, 0, 0.3D);
|
return (double) method.invoke(planner.routeGeometry, node, x, z, 0, 0, 0.3D);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<?> routeCandidates(HydrologyPlanner planner) throws Exception {
|
private List<?> routeCandidates(HydrologyPlanner planner) throws Exception {
|
||||||
Class<?> directionType = Class.forName(HydrologyPlanner.class.getName() + "$Direction");
|
Class<?> directionType = RouteDirection.class;
|
||||||
Constructor<?> directionConstructor = directionType.getDeclaredConstructor(double.class, double.class);
|
Constructor<?> directionConstructor = directionType.getDeclaredConstructor(double.class, double.class);
|
||||||
directionConstructor.setAccessible(true);
|
directionConstructor.setAccessible(true);
|
||||||
Object direction = directionConstructor.newInstance(1D, 0D);
|
Object direction = directionConstructor.newInstance(1D, 0D);
|
||||||
Class<?> positionType = Class.forName(HydrologyPlanner.class.getName() + "$RoutePosition");
|
Class<?> positionType = RoutePosition.class;
|
||||||
Constructor<?> positionConstructor = positionType.getDeclaredConstructor(double.class, double.class,
|
Constructor<?> positionConstructor = positionType.getDeclaredConstructor(double.class, double.class,
|
||||||
double.class, double.class, directionType);
|
double.class, double.class, directionType);
|
||||||
positionConstructor.setAccessible(true);
|
positionConstructor.setAccessible(true);
|
||||||
Object position = positionConstructor.newInstance(0D, 0D, 100D, 0D, direction);
|
Object position = positionConstructor.newInstance(0D, 0D, 100D, 0D, direction);
|
||||||
Method method = HydrologyPlanner.class.getDeclaredMethod("routeCandidates", long.class, long.class,
|
Method method = HydrologyRouteGeometry.class.getDeclaredMethod("routeCandidates", long.class, long.class,
|
||||||
positionType, double.class, int.class, int.class, String.class);
|
positionType, double.class, int.class, int.class, String.class);
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
return (List<?>) method.invoke(planner, 1L, 2L, position, 0.5D, 4, 1, null);
|
return (List<?>) method.invoke(planner.routeGeometry, 1L, 2L, position, 0.5D, 4, 1, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CountingNaturalSampler implements HydrologyNaturalTerrainSampler {
|
private static final class CountingNaturalSampler implements HydrologyNaturalTerrainSampler {
|
||||||
|
|||||||
+2
-2
@@ -108,7 +108,7 @@ public class HydrologyPlannerTributaryTest {
|
|||||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
||||||
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
|
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
|
||||||
|
|
||||||
planner.levelApproach(segments, 100);
|
planner.tributaries.levelApproach(segments, 100);
|
||||||
|
|
||||||
List<HydrologyPoint> graded = new ArrayList<>();
|
List<HydrologyPoint> graded = new ArrayList<>();
|
||||||
int previousHead = Integer.MAX_VALUE;
|
int previousHead = Integer.MAX_VALUE;
|
||||||
@@ -146,7 +146,7 @@ public class HydrologyPlannerTributaryTest {
|
|||||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
||||||
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
|
1L, 7L, HydrologyFeatureType.SURFACE_POOL, 120, 120, 4, 2, false, false, List.copyOf(centerline))));
|
||||||
|
|
||||||
planner.levelApproach(segments, 112);
|
planner.tributaries.levelApproach(segments, 112);
|
||||||
|
|
||||||
List<HydrologyPoint> graded = segments.getFirst().centerline();
|
List<HydrologyPoint> graded = segments.getFirst().centerline();
|
||||||
assertEquals(13, graded.size());
|
assertEquals(13, graded.size());
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ public class HydrologyRouteLengthsTest {
|
|||||||
int[] parent = forest(width, random);
|
int[] parent = forest(width, random);
|
||||||
HydrologyPlannerSettings.Routing routing = routing(spacing, maximumLength);
|
HydrologyPlannerSettings.Routing routing = routing(spacing, maximumLength);
|
||||||
assertArrayEquals(walks(parent, width, routing),
|
assertArrayEquals(walks(parent, width, routing),
|
||||||
HydrologyPlanner.routeLengths(parent, width, routing));
|
HydrologySourcePlanner.routeLengths(parent, width, routing));
|
||||||
reverse(parent);
|
reverse(parent);
|
||||||
assertArrayEquals(walks(parent, width, routing),
|
assertArrayEquals(walks(parent, width, routing),
|
||||||
HydrologyPlanner.routeLengths(parent, width, routing));
|
HydrologySourcePlanner.routeLengths(parent, width, routing));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ public class HydrologyRouteLengthsTest {
|
|||||||
public void exactLengthLimitAndMissingOutletsKeepTheirBounds() {
|
public void exactLengthLimitAndMissingOutletsKeepTheirBounds() {
|
||||||
int[] parent = {1, 2, 3, 4, -1, -1, -1, -1, -1};
|
int[] parent = {1, 2, 3, 4, -1, -1, -1, -1, -1};
|
||||||
assertArrayEquals(new int[]{Integer.MAX_VALUE, 12, 8, 4, 0, 0, 0, 0, 0},
|
assertArrayEquals(new int[]{Integer.MAX_VALUE, 12, 8, 4, 0, 0, 0, 0, 0},
|
||||||
HydrologyPlanner.routeLengths(parent, 9, routing(4, 12)));
|
HydrologySourcePlanner.routeLengths(parent, 9, routing(4, 12)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -48,11 +48,11 @@ public class HydrologyRouteLengthsTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parent[parent.length - 1] = -1;
|
parent[parent.length - 1] = -1;
|
||||||
int[] lengths = HydrologyPlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
|
int[] lengths = HydrologySourcePlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
|
||||||
assertEquals((parent.length - 1) * 4, lengths[0]);
|
assertEquals((parent.length - 1) * 4, lengths[0]);
|
||||||
assertEquals(0, lengths[lengths.length - 1]);
|
assertEquals(0, lengths[lengths.length - 1]);
|
||||||
reverse(parent);
|
reverse(parent);
|
||||||
lengths = HydrologyPlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
|
lengths = HydrologySourcePlanner.routeLengths(parent, width, routing(4, Integer.MAX_VALUE));
|
||||||
assertEquals((parent.length - 1) * 4, lengths[lengths.length - 1]);
|
assertEquals((parent.length - 1) * 4, lengths[lengths.length - 1]);
|
||||||
assertEquals(0, lengths[0]);
|
assertEquals(0, lengths[0]);
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ public class HydrologyRouteLengthsTest {
|
|||||||
@Test
|
@Test
|
||||||
public void cyclesFailBeforeTheyCanStallPlanning() {
|
public void cyclesFailBeforeTheyCanStallPlanning() {
|
||||||
assertThrows(IllegalStateException.class,
|
assertThrows(IllegalStateException.class,
|
||||||
() -> HydrologyPlanner.routeLengths(new int[]{1, 0}, 2, routing(4, 128)));
|
() -> HydrologySourcePlanner.routeLengths(new int[]{1, 0}, 2, routing(4, 128)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[] forest(int width, Random random) {
|
private static int[] forest(int width, Random random) {
|
||||||
|
|||||||
+5
-5
@@ -194,22 +194,22 @@ public class HydrologyRouteTurnCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class Solver {
|
private static final class Solver {
|
||||||
private final HydrologyPlanner planner = mock(HydrologyPlanner.class, CALLS_REAL_METHODS);
|
private final HydrologyRouteGeometry geometry = mock(HydrologyRouteGeometry.class, CALLS_REAL_METHODS);
|
||||||
private final Constructor<?> candidateConstructor;
|
private final Constructor<?> candidateConstructor;
|
||||||
private final Object tangent;
|
private final Object tangent;
|
||||||
private final Method solve;
|
private final Method solve;
|
||||||
private final Method indices;
|
private final Method indices;
|
||||||
|
|
||||||
private Solver() throws Exception {
|
private Solver() throws Exception {
|
||||||
Class<?> direction = Class.forName(HydrologyPlanner.class.getName() + "$Direction");
|
Class<?> direction = RouteDirection.class;
|
||||||
Constructor<?> directionConstructor = direction.getDeclaredConstructor(double.class, double.class);
|
Constructor<?> directionConstructor = direction.getDeclaredConstructor(double.class, double.class);
|
||||||
directionConstructor.setAccessible(true);
|
directionConstructor.setAccessible(true);
|
||||||
tangent = directionConstructor.newInstance(1D, 0D);
|
tangent = directionConstructor.newInstance(1D, 0D);
|
||||||
Class<?> candidate = Class.forName(HydrologyPlanner.class.getName() + "$RouteCandidate");
|
Class<?> candidate = RouteCandidate.class;
|
||||||
candidateConstructor = candidate.getDeclaredConstructor(HydrologyPoint.class, double.class, double.class,
|
candidateConstructor = candidate.getDeclaredConstructor(HydrologyPoint.class, double.class, double.class,
|
||||||
double.class, direction, boolean.class);
|
double.class, direction, boolean.class);
|
||||||
candidateConstructor.setAccessible(true);
|
candidateConstructor.setAccessible(true);
|
||||||
solve = HydrologyPlanner.class.getDeclaredMethod("selectCurvatureAwareTerrainRoute", List.class,
|
solve = HydrologyRouteGeometry.class.getDeclaredMethod("selectCurvatureAwareTerrainRoute", List.class,
|
||||||
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class);
|
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class);
|
||||||
solve.setAccessible(true);
|
solve.setAccessible(true);
|
||||||
indices = solve.getReturnType().getDeclaredMethod("indices");
|
indices = solve.getReturnType().getDeclaredMethod("indices");
|
||||||
@@ -226,7 +226,7 @@ public class HydrologyRouteTurnCacheTest {
|
|||||||
}
|
}
|
||||||
layers.add(candidates);
|
layers.add(candidates);
|
||||||
}
|
}
|
||||||
return (int[]) indices.invoke(solve.invoke(planner, layers, 12D, maximumTurn, 12D,
|
return (int[]) indices.invoke(solve.invoke(geometry, layers, 12D, maximumTurn, 12D,
|
||||||
fixture.penalties(), fixture.transitions(), cache));
|
fixture.penalties(), fixture.transitions(), cache));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user