mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 16:44:06 +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();
|
||||
}
|
||||
|
||||
HydrologyPlanner.SourceAdmissionSelection lazy = HydrologyPlanner.selectSourceAdmissions(
|
||||
SourceAdmissionSelection lazy = HydrologySourcePlanner.selectSourceAdmissions(
|
||||
candidateCount,
|
||||
target,
|
||||
guaranteed,
|
||||
@@ -208,7 +208,7 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
public void lazyAdmissionStopsGlobalEvaluationWhenQuotaIsFilled() {
|
||||
AtomicInteger evaluations = new AtomicInteger();
|
||||
|
||||
HydrologyPlanner.SourceAdmissionSelection selection = HydrologyPlanner.selectSourceAdmissions(
|
||||
SourceAdmissionSelection selection = HydrologySourcePlanner.selectSourceAdmissions(
|
||||
256,
|
||||
1,
|
||||
0,
|
||||
|
||||
+6
-6
@@ -331,12 +331,12 @@ public class HydrologyPlannerSettingsTest {
|
||||
);
|
||||
HydrologyTerrainSample terrainWithoutUndergroundRiverCave = HydrologyTerrainSample.openLand(100, 1D, "parent");
|
||||
|
||||
assertFalse(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, 0));
|
||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, -64));
|
||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 4, 3, 0));
|
||||
assertTrue(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 80, 3, 0));
|
||||
assertFalse(HydrologyPlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 96, 3, 0));
|
||||
assertFalse(HydrologyPlanner.deepSiteFits(
|
||||
assertFalse(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, 0));
|
||||
assertTrue(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 3, 3, -64));
|
||||
assertTrue(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 4, 3, 0));
|
||||
assertTrue(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 80, 3, 0));
|
||||
assertFalse(HydrologyFeatureSitePlanner.deepSiteFits(terrainWithoutUndergroundRiverCave, deepFluid, 96, 3, 0));
|
||||
assertFalse(HydrologyFeatureSitePlanner.deepSiteFits(
|
||||
HydrologyTerrainSample.ocean(50, "ocean"),
|
||||
deepFluid,
|
||||
40,
|
||||
|
||||
@@ -536,7 +536,7 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
|
||||
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);
|
||||
contextConstructor.setAccessible(true);
|
||||
Object context = contextConstructor.newInstance(key, 64L, 4096);
|
||||
@@ -631,8 +631,8 @@ public class HydrologyPlannerTest {
|
||||
|
||||
@Test
|
||||
public void biomeIncisionMultiplierCannotExceedTheConfiguredSurfaceMaximum() {
|
||||
assertEquals(6, HydrologyPlanner.permittedSurfaceIncision(6, 2D));
|
||||
assertEquals(3, HydrologyPlanner.permittedSurfaceIncision(6, 0.5D));
|
||||
assertEquals(6, HydrologySourcePlanner.permittedSurfaceIncision(6, 2D));
|
||||
assertEquals(3, HydrologySourcePlanner.permittedSurfaceIncision(6, 0.5D));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -709,12 +709,12 @@ public class HydrologyPlannerTest {
|
||||
|
||||
@Test
|
||||
public void fallbackSourceTargetCannotExceedAvailableOutlets() {
|
||||
assertEquals(1, HydrologyPlanner.effectiveSourceTarget(true, 8, 1));
|
||||
assertEquals(0, HydrologyPlanner.effectiveSourceTarget(true, 8, 0));
|
||||
assertEquals(8, HydrologyPlanner.effectiveSourceTarget(false, 8, 1));
|
||||
assertEquals(1, HydrologySourcePlanner.effectiveSourceTarget(true, 8, 1));
|
||||
assertEquals(0, HydrologySourcePlanner.effectiveSourceTarget(true, 8, 0));
|
||||
assertEquals(8, HydrologySourcePlanner.effectiveSourceTarget(false, 8, 1));
|
||||
assertThrows(
|
||||
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;
|
||||
|
||||
boolean expected = bruteForceUndergroundHeads(minimum, maximum, outlet, 0, Integer.MAX_VALUE);
|
||||
boolean actual = HydrologyPlanner.solveUndergroundHeads(
|
||||
boolean actual = HydrologyUndergroundCoursePlanner.solveUndergroundHeads(
|
||||
preferred,
|
||||
minimum,
|
||||
maximum,
|
||||
@@ -3424,31 +3424,31 @@ public class HydrologyPlannerTest {
|
||||
}
|
||||
|
||||
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,
|
||||
int.class, int.class, long.class, HydrologyTerrainSample.class);
|
||||
constructor.setAccessible(true);
|
||||
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);
|
||||
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 {
|
||||
Class<?> directionType = Class.forName(HydrologyPlanner.class.getName() + "$Direction");
|
||||
Class<?> directionType = RouteDirection.class;
|
||||
Constructor<?> directionConstructor = directionType.getDeclaredConstructor(double.class, double.class);
|
||||
directionConstructor.setAccessible(true);
|
||||
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,
|
||||
double.class, double.class, directionType);
|
||||
positionConstructor.setAccessible(true);
|
||||
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);
|
||||
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 {
|
||||
|
||||
+2
-2
@@ -108,7 +108,7 @@ public class HydrologyPlannerTributaryTest {
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
||||
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<>();
|
||||
int previousHead = Integer.MAX_VALUE;
|
||||
@@ -146,7 +146,7 @@ public class HydrologyPlannerTributaryTest {
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(List.of(new HydraulicSegment(
|
||||
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();
|
||||
assertEquals(13, graded.size());
|
||||
|
||||
@@ -19,10 +19,10 @@ public class HydrologyRouteLengthsTest {
|
||||
int[] parent = forest(width, random);
|
||||
HydrologyPlannerSettings.Routing routing = routing(spacing, maximumLength);
|
||||
assertArrayEquals(walks(parent, width, routing),
|
||||
HydrologyPlanner.routeLengths(parent, width, routing));
|
||||
HydrologySourcePlanner.routeLengths(parent, width, routing));
|
||||
reverse(parent);
|
||||
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() {
|
||||
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},
|
||||
HydrologyPlanner.routeLengths(parent, 9, routing(4, 12)));
|
||||
HydrologySourcePlanner.routeLengths(parent, 9, routing(4, 12)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,11 +48,11 @@ public class HydrologyRouteLengthsTest {
|
||||
}
|
||||
}
|
||||
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(0, lengths[lengths.length - 1]);
|
||||
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(0, lengths[0]);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public class HydrologyRouteLengthsTest {
|
||||
@Test
|
||||
public void cyclesFailBeforeTheyCanStallPlanning() {
|
||||
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) {
|
||||
|
||||
+5
-5
@@ -194,22 +194,22 @@ public class HydrologyRouteTurnCacheTest {
|
||||
}
|
||||
|
||||
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 Object tangent;
|
||||
private final Method solve;
|
||||
private final Method indices;
|
||||
|
||||
private Solver() throws Exception {
|
||||
Class<?> direction = Class.forName(HydrologyPlanner.class.getName() + "$Direction");
|
||||
Class<?> direction = RouteDirection.class;
|
||||
Constructor<?> directionConstructor = direction.getDeclaredConstructor(double.class, double.class);
|
||||
directionConstructor.setAccessible(true);
|
||||
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,
|
||||
double.class, direction, boolean.class);
|
||||
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);
|
||||
solve.setAccessible(true);
|
||||
indices = solve.getReturnType().getDeclaredMethod("indices");
|
||||
@@ -226,7 +226,7 @@ public class HydrologyRouteTurnCacheTest {
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user