mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Basin hydrology
- The route-and-reject hydrology system and its probes are removed - Hydrology is configured under dimension hydrology and per biome, region and dimension hydrologyPolicy - riverPolicy and the old hydrology config types are gone - Grid kernels cover priority flood, D8 routing, flow accumulation, basin extraction, breaching and stream burning - A basin spills at its pour point and an interior bump can never be the spill - Region plans cover 4096-block regions on a 64-block grid for major rivers and lakes - Window plans cover 512-block windows on an 8-block grid for reaches and ponds - Plans are cached in memory, persisted per world under iris/hydrology and keyed by pack, seed, world height and settings - Only the main runtime of an engine persists plans and stores swap on hotload and default runtime change - Demand rasters outrank prefetch and stopping pregen withdraws its queued prefetch - Ponds come from natural basins, seeded cups and chained cups with strictly falling levels - A pond level never exceeds its rim plus maxFill or the end grade of any inflow - Pond outlet loops are opened at their lowest pond - Reaches split at junctions, pond outlets and 256-block grid lines - Reaches meander, fit the valley, grade to a running minimum and smooth with Chaikin passes - Drops of two blocks or more get a lip and one-block corner steps become dry rim ledges - Cascades and waterfalls write falling fluid from the mantle at level eight - Inflows into region lakes end at the lake level - River mouths and water beside the sea sit at the sea surface - The per-chunk raster gives terrain height, water head, role, tier, fluid, cave ceiling and shore distance - WaterRole codes are stable and exposed to packs through the WATER_ROLE engine stream - WATER_HEAD replaces the old river engine streams and an unknown stream name fails the pack load - Banks take SMOOTH, LINEAR, CONCAVE, TERRACED or CLIFF profiles - Containment berms grow as tall as the water needs - Columns within two blocks of water stay solid and keep no Terrain3D overhang - Seam dams hold water where new chunks meet chunks generated earlier - The transition blend skips hydrology-owned columns - Caves stay at least two blocks below river and pond beds - The painter is the only writer of still water - Objects cannot open water, falls or bank ground and solid objects may stand in water - On-water objects need fluid at their anchor column - Floating child islands stay out of channels - Sea inlets run inland from cliff coasts at sea level and end in a chamber pool - Inlet passages are sealed against carvers, subterrain features and the post pass - Cave pools settle into carved lows, keep one fluid and stay out of inlet space - Pool and inlet cells use their own cave actions in mantle data - Water vegetation lists sit on tiers, ponds and hydrologyPolicy with SEA_FLOOR, SEA_SURFACE and SHORE_LINE parts - Pack validation compiles hydrology settings and rejects empty or water-holding palettes - Chunk generation fails closed when its 3x3 water rasters are not planned - /iris find covers stream, river, major river, cascade, waterfall, mouth, pond, lake, dam, sea inlet and cave pool - Generation history records the new feature types and drops old hydrology entries - The studio map draws water from rasters up close and from plans when zoomed out - Pregen prepares the current region and one region ahead - hydrologyProbe, riverTransectProbe, hydrologyBlockProbe, seaInletProbe and cavePoolProbe report containment, rises, seams, tick conversion and missing surface - Test forks run with a 2 GB heap, a 60-minute timeout and the Mockito agent loaded explicitly - Generation with hydrology off is unchanged
This commit is contained in:
1 parent
2d211cf106
commit
db0f98d103
710 files changed
+41791
-91883
No files matched your search
+4
-3
@@ -163,11 +163,11 @@ dependencies {
|
||||
slim(libs.jaxen)
|
||||
|
||||
testFixturesImplementation('junit:junit:4.13.2')
|
||||
testFixturesImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testFixturesImplementation(libs.mockito.core)
|
||||
testFixturesCompileOnly(libs.paper.api)
|
||||
|
||||
testImplementation('junit:junit:4.13.2')
|
||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.paper.api)
|
||||
testImplementation(libs.log4j.core)
|
||||
testRuntimeOnly(libs.paper.api)
|
||||
@@ -177,7 +177,8 @@ dependencies {
|
||||
}
|
||||
|
||||
tasks.named('test').configure {
|
||||
maxHeapSize = '1g'
|
||||
timeout = java.time.Duration.ofMinutes(60)
|
||||
maxHeapSize = '2g'
|
||||
jvmArgs('-XX:+ExitOnOutOfMemoryError')
|
||||
systemProperty('iris.generation.counters', 'true')
|
||||
}
|
||||
|
||||
+7
-23
@@ -1,25 +1,14 @@
|
||||
package art.arcane.iris.command.specialhandlers;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.command.DirectorParameterHandler;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
Engine activeEngine = engine();
|
||||
if (activeEngine == null) {
|
||||
return new KList<>(HydrologyFeatureQuery.suggestions(List.of()));
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
return new KList<>(runtime == null
|
||||
? HydrologyFeatureQuery.suggestions(List.of())
|
||||
: runtime.featureQueryKeys());
|
||||
return new KList<>(HydrologyFeatureType.keys());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -29,16 +18,11 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
||||
|
||||
@Override
|
||||
public String parse(String input, boolean force) throws DirectorParsingException {
|
||||
String normalized = input == null ? "" : input.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
throw new DirectorParsingException("Hydrology feature type must not be blank.");
|
||||
try {
|
||||
return HydrologyFeatureType.parse(input).key();
|
||||
} catch (IllegalArgumentException error) {
|
||||
throw new DirectorParsingException(error.getMessage());
|
||||
}
|
||||
for (String possibility : getPossibilities(normalized)) {
|
||||
if (possibility.equalsIgnoreCase(normalized)) {
|
||||
return possibility;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,6 +32,6 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
|
||||
|
||||
@Override
|
||||
public String getRandomDefault() {
|
||||
return "surface";
|
||||
return HydrologyFeatureType.RIVER.key();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import art.arcane.iris.generation.decoration.IrisDepositVariant;
|
||||
import art.arcane.iris.generation.decoration.IrisOreGenerator;
|
||||
import art.arcane.iris.generation.decoration.IrisOreBands;
|
||||
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
|
||||
import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrologyPolicy;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
@@ -161,8 +161,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
private int lockLayersMax = 7;
|
||||
@Description("Profile-driven 3D cave configuration")
|
||||
private IrisCaveProfile caveProfile = new IrisCaveProfile();
|
||||
@Description("Biome-level river placement, routing, profile, biome, and geometry policy. Omit to inherit region and dimension settings.")
|
||||
private IrisRiverPolicy riverPolicy = null;
|
||||
@Description("Water settings for this biome: rainfall, pond density, where streams and ponds may form, and the fluid that fills them. A value left unset comes from the region, then from the dimension.")
|
||||
private IrisHydrologyPolicy hydrologyPolicy = null;
|
||||
@MinNumber(1)
|
||||
@MaxNumber(512)
|
||||
@Description("The rarity of this biome (integer)")
|
||||
|
||||
@@ -5,7 +5,7 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.EngineMetrics;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
|
||||
@@ -25,6 +25,7 @@ public class ChunkContext {
|
||||
private FloatingBiomeOverlay floatingBiomes;
|
||||
private CaveTerrainSnapshot caveTerrain;
|
||||
private TransitionTerrainColumn[] transitionTerrain;
|
||||
private volatile ChunkWaterRaster waterRaster;
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final IrisComplex complex;
|
||||
@@ -89,10 +90,6 @@ public class ChunkContext {
|
||||
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
||||
boolean capturePrefillMetric = metrics != null;
|
||||
long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L;
|
||||
IrisHydrologyRuntime hydrologyRuntime = complex.getHydrologyRuntime();
|
||||
if (hydrologyRuntime != null && (resolvedPlan.height || resolvedPlan.biome)) {
|
||||
hydrologyRuntime.prepareChunkColumns(x, z);
|
||||
}
|
||||
if (shouldPrefillAsync(resolvedPlan, complex.isPlatformMainThread())) {
|
||||
prefillAsync(resolvedPlan);
|
||||
} else {
|
||||
@@ -115,6 +112,9 @@ public class ChunkContext {
|
||||
}
|
||||
|
||||
private void prefillSurface(PrefillPlan prefillPlan) {
|
||||
if (prefillPlan.height || prefillPlan.biome) {
|
||||
waterRaster();
|
||||
}
|
||||
if (prefillPlan.height) {
|
||||
height.fillRounded(roundedHeight);
|
||||
}
|
||||
@@ -209,6 +209,18 @@ public class ChunkContext {
|
||||
return complex;
|
||||
}
|
||||
|
||||
public ChunkWaterRaster waterRaster() {
|
||||
ChunkWaterRaster known = waterRaster;
|
||||
if (known != null) {
|
||||
return known;
|
||||
}
|
||||
ChunkWaterRaster raster = complex.getHydrologyRuntime().raster(x >> 4, z >> 4);
|
||||
if (!raster.provisional()) {
|
||||
waterRaster = raster;
|
||||
}
|
||||
return raster;
|
||||
}
|
||||
|
||||
public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() {
|
||||
return floatingIslandBoundarySampler;
|
||||
}
|
||||
|
||||
@@ -542,7 +542,7 @@ final class DecoratorCore {
|
||||
return stack;
|
||||
}
|
||||
|
||||
private static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
|
||||
static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
|
||||
return decorator.isForcePlace()
|
||||
|| (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface))
|
||||
&& (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(data), surface));
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.block.IrisBlockData;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class HydrologyVegetation {
|
||||
private static final long SALT = 0x4859445256454745L;
|
||||
private static final long GOLDEN = 0x9e3779b97f4a7c15L;
|
||||
private static final int[][] SIDES = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final long seed;
|
||||
private final Engine engine;
|
||||
private final RNG floorRng;
|
||||
private final RNG surfaceRng;
|
||||
private final RNG shoreRng;
|
||||
|
||||
public HydrologyVegetation(long seed, Engine engine) {
|
||||
this.seed = seed ^ SALT;
|
||||
this.engine = engine;
|
||||
this.floorRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SEA_FLOOR));
|
||||
this.surfaceRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SEA_SURFACE));
|
||||
this.shoreRng = new RNG(DecoratorCore.partSeed(this.seed, IrisDecorationPart.SHORE_LINE));
|
||||
}
|
||||
|
||||
public void decorate(Hunk<NativeBlockState> output, int localX, int localZ, int x, int z, WaterRole role, Water water, IrisData data) {
|
||||
Placement placement = placement(role);
|
||||
if (placement == Placement.NONE) {
|
||||
return;
|
||||
}
|
||||
Decorators decorators = water.decorators(x, z);
|
||||
if (decorators.empty()) {
|
||||
return;
|
||||
}
|
||||
int ground = water.ground(x, z);
|
||||
if (ground < 0 || ground + 1 >= output.getHeight()) {
|
||||
return;
|
||||
}
|
||||
Column column = new Column(output, localX, localZ, x, z, ground, water, data);
|
||||
if (placement == Placement.SHORE) {
|
||||
decorateShore(column, decorators.shore);
|
||||
return;
|
||||
}
|
||||
int head = water.head(x, z);
|
||||
if (head <= ground || head + 1 >= output.getHeight()) {
|
||||
return;
|
||||
}
|
||||
decorateFloor(column, head, decorators.floor);
|
||||
if (placement == Placement.STILL && decorators.surface.length > 0 && still(water, x, z, head)) {
|
||||
decorateSurface(column, head, decorators.surface);
|
||||
}
|
||||
}
|
||||
|
||||
private static Placement placement(WaterRole role) {
|
||||
return switch (role) {
|
||||
case CHANNEL, POND -> Placement.STILL;
|
||||
case CASCADE, WATERFALL -> Placement.FLOOR;
|
||||
case BANK, SHORE, POND_SHORE -> Placement.SHORE;
|
||||
case NONE, DAM, MOUTH, INLET -> Placement.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private void decorateFloor(Column column, int head, IrisDecorator[] bucket) {
|
||||
int room = head - column.ground - 1;
|
||||
if (room < 1 || bucket.length == 0) {
|
||||
return;
|
||||
}
|
||||
IrisDecorator decorator = pick(bucket, floorRng, column, IrisDecorationPart.SEA_FLOOR);
|
||||
NativeBlockState support = column.output.get(column.localX, column.ground, column.localZ);
|
||||
if (decorator == null || !B.isSolid(support) || !column.accepts(decorator, support)) {
|
||||
return;
|
||||
}
|
||||
column.place(decorator, floorRng, column.ground + 1, room, Slot.FLOOR, null);
|
||||
}
|
||||
|
||||
private void decorateSurface(Column column, int head, IrisDecorator[] bucket) {
|
||||
NativeBlockState support = column.output.get(column.localX, head, column.localZ);
|
||||
if (!isWaterSource(support)) {
|
||||
return;
|
||||
}
|
||||
IrisDecorator decorator = pick(bucket, surfaceRng, column, IrisDecorationPart.SEA_SURFACE);
|
||||
if (decorator == null || !column.accepts(decorator, support)) {
|
||||
return;
|
||||
}
|
||||
column.place(decorator, surfaceRng, head + 1, column.output.getHeight() - head - 1, Slot.SURFACE, null);
|
||||
}
|
||||
|
||||
private void decorateShore(Column column, IrisDecorator[] bucket) {
|
||||
NativeBlockState support = column.output.get(column.localX, column.ground, column.localZ);
|
||||
if (bucket.length == 0 || support == null || !support.isSolid()) {
|
||||
return;
|
||||
}
|
||||
IrisDecorator decorator = pick(bucket, shoreRng, column, IrisDecorationPart.SHORE_LINE);
|
||||
if (decorator == null || !column.accepts(decorator, support)) {
|
||||
return;
|
||||
}
|
||||
column.place(decorator, shoreRng, column.ground + 1, column.output.getHeight() - column.ground - 1, Slot.SHORE, support);
|
||||
}
|
||||
|
||||
private IrisDecorator pick(IrisDecorator[] bucket, RNG partRng, Column column, IrisDecorationPart part) {
|
||||
IrisDecorator picked = null;
|
||||
long choice = seed + part.ordinal();
|
||||
int count = 0;
|
||||
for (IrisDecorator decorator : bucket) {
|
||||
if (!decorator.passesChanceGate(partRng, column.x, column.z, column.data)) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
choice = mix(choice ^ mix(column.x * 0x632be59bd9b4e019L + column.z * 0x8cb92ba72f3d8dd7L + count));
|
||||
if (count == 1 || Long.remainderUnsigned(choice, count) == 0L) {
|
||||
picked = decorator;
|
||||
}
|
||||
}
|
||||
if (picked == null || picked.isForcePlace() || picked.getSlopeCondition().isDefault()
|
||||
|| picked.getSlopeCondition().isValid(column.water.slope(column.x, column.z))) {
|
||||
return picked;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static boolean still(Water water, int x, int z, int head) {
|
||||
for (int[] side : SIDES) {
|
||||
int neighbourX = x + side[0];
|
||||
int neighbourZ = z + side[1];
|
||||
int neighbourHead = water.head(neighbourX, neighbourZ);
|
||||
if (neighbourHead != head && water.ground(neighbourX, neighbourZ) < neighbourHead) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean isWaterSource(NativeBlockState state) {
|
||||
if (state == null || !state.isWater()) {
|
||||
return false;
|
||||
}
|
||||
String level = IrisProceduralBlocks.propertyValue(state, "level");
|
||||
return level == null || level.equals("0");
|
||||
}
|
||||
|
||||
static boolean holdsWater(NativeBlockState state) {
|
||||
return B.isFluid(state) || B.isWaterLogged(state) || IrisSurfaceDecorator.isAquaticPlacement(state);
|
||||
}
|
||||
|
||||
static boolean plannedWater(Water water, int x, int z, int y) {
|
||||
int ground = water.ground(x, z);
|
||||
int top = water.head(x, z);
|
||||
if (y <= ground || top <= ground) {
|
||||
return false;
|
||||
}
|
||||
for (int[] side : SIDES) {
|
||||
int neighbourHead = water.head(x + side[0], z + side[1]);
|
||||
if (neighbourHead > top && water.ground(x + side[0], z + side[1]) < neighbourHead) {
|
||||
top = neighbourHead;
|
||||
}
|
||||
}
|
||||
return y <= top;
|
||||
}
|
||||
|
||||
private static boolean twoBlock(NativeBlockState state) {
|
||||
String half = IrisProceduralBlocks.propertyValue(state, "half");
|
||||
return "lower".equals(half) || "upper".equals(half);
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
long mixed = (value + GOLDEN ^ (value + GOLDEN >>> 30)) * 0xbf58476d1ce4e5b9L;
|
||||
mixed = (mixed ^ (mixed >>> 27)) * 0x94d049bb133111ebL;
|
||||
return mixed ^ (mixed >>> 31);
|
||||
}
|
||||
|
||||
public interface Water {
|
||||
int ground(int x, int z);
|
||||
|
||||
int head(int x, int z);
|
||||
|
||||
double slope(int x, int z);
|
||||
|
||||
Decorators decorators(int x, int z);
|
||||
}
|
||||
|
||||
private enum Placement {
|
||||
NONE,
|
||||
SHORE,
|
||||
FLOOR,
|
||||
STILL
|
||||
}
|
||||
|
||||
private enum Slot {
|
||||
FLOOR,
|
||||
SURFACE,
|
||||
SHORE
|
||||
}
|
||||
|
||||
private final class Column {
|
||||
private final Hunk<NativeBlockState> output;
|
||||
private final int localX;
|
||||
private final int localZ;
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final int ground;
|
||||
private final Water water;
|
||||
private final IrisData data;
|
||||
|
||||
private Column(Hunk<NativeBlockState> output, int localX, int localZ, int x, int z, int ground, Water water, IrisData data) {
|
||||
this.output = output;
|
||||
this.localX = localX;
|
||||
this.localZ = localZ;
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.ground = ground;
|
||||
this.water = water;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private boolean accepts(IrisDecorator decorator, NativeBlockState support) {
|
||||
return decorator.isForcePlace() || DecoratorCore.allowsSurface(decorator, support, data);
|
||||
}
|
||||
|
||||
private int stack(IrisDecorator decorator, RNG rng, int room) {
|
||||
return decorator.isStacking() ? Math.min(room, DecoratorCore.computeStack(decorator, rng, x, z, data, room)) : 1;
|
||||
}
|
||||
|
||||
private void place(IrisDecorator decorator, RNG rng, int baseY, int room, Slot slot, NativeBlockState support) {
|
||||
NativeBlockState first = decorator.pickBlockData(rng, data, x, z);
|
||||
if (first == null) {
|
||||
return;
|
||||
}
|
||||
if (twoBlock(first)) {
|
||||
if (room >= 2) {
|
||||
placePair(decorator, first, baseY, slot, support);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int stack = stack(decorator, rng, room);
|
||||
NativeBlockState sampled = first;
|
||||
boolean sampledTop = false;
|
||||
for (int index = 0; index < stack; index++) {
|
||||
int y = baseY + index;
|
||||
boolean top = (stack == 1 ? 1D : (double) index / (stack - 1)) >= decorator.getTopThreshold();
|
||||
if (top != sampledTop) {
|
||||
sampled = top ? decorator.pickBlockDataTop(rng, data, x, z) : decorator.pickBlockData(rng, data, x, z);
|
||||
sampledTop = top;
|
||||
}
|
||||
NativeBlockState block = sampled == null ? null : fit(sampled, y, slot, decorator, index == 0 ? support : null);
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
output.set(localX, y, localZ, block);
|
||||
}
|
||||
}
|
||||
|
||||
private void placePair(IrisDecorator decorator, NativeBlockState block, int y, Slot slot, NativeBlockState support) {
|
||||
NativeBlockState lower = fit(block.withProperty("half", "lower"), y, slot, decorator, support);
|
||||
NativeBlockState upper = lower == null ? null : fit(block.withProperty("half", "upper"), y + 1, slot, decorator, null);
|
||||
if (upper != null) {
|
||||
output.set(localX, y, localZ, lower);
|
||||
output.set(localX, y + 1, localZ, upper);
|
||||
}
|
||||
}
|
||||
|
||||
private NativeBlockState fit(NativeBlockState block, int y, Slot slot, IrisDecorator decorator, NativeBlockState support) {
|
||||
NativeBlockState existing = output.get(localX, y, localZ);
|
||||
if (slot == Slot.FLOOR) {
|
||||
NativeBlockState placed = IrisProceduralBlocks.normalizeWaterlogging(block, existing, true);
|
||||
return isWaterSource(existing) && IrisSurfaceDecorator.isAquaticPlacement(placed) ? placed : null;
|
||||
}
|
||||
if (!B.isAir(existing) || holdsWater(block)) {
|
||||
return null;
|
||||
}
|
||||
if (slot == Slot.SURFACE) {
|
||||
return block;
|
||||
}
|
||||
if (besideWater(y) || support != null && !DecoratorCore.isValidShorelineSupport(decorator, block, support)) {
|
||||
return null;
|
||||
}
|
||||
return IrisSugarCane.canPlace(block, output, localX, y, localZ, x, z, engine) ? block : null;
|
||||
}
|
||||
|
||||
private boolean besideWater(int y) {
|
||||
for (int[] side : SIDES) {
|
||||
int neighbourX = localX + side[0];
|
||||
int neighbourZ = localZ + side[1];
|
||||
if (neighbourX >= 0 && neighbourX < output.getWidth() && neighbourZ >= 0 && neighbourZ < output.getDepth()) {
|
||||
if (holdsWater(output.get(neighbourX, y, neighbourZ))) {
|
||||
return true;
|
||||
}
|
||||
} else if (plannedWater(water, x + side[0], z + side[1], y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Decorators {
|
||||
public static final Decorators EMPTY = new Decorators(new IrisDecorator[0], new IrisDecorator[0], new IrisDecorator[0]);
|
||||
|
||||
private final IrisDecorator[] floor;
|
||||
private final IrisDecorator[] surface;
|
||||
private final IrisDecorator[] shore;
|
||||
|
||||
private Decorators(IrisDecorator[] floor, IrisDecorator[] surface, IrisDecorator[] shore) {
|
||||
this.floor = floor;
|
||||
this.surface = surface;
|
||||
this.shore = shore;
|
||||
}
|
||||
|
||||
public static Decorators compile(List<IrisDecorator> decorators, IrisData data, String path) {
|
||||
HydrologySettings.requireDecorators(decorators, path);
|
||||
if (decorators == null || decorators.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
List<IrisDecorator> floor = new ArrayList<>();
|
||||
List<IrisDecorator> surface = new ArrayList<>();
|
||||
List<IrisDecorator> shore = new ArrayList<>();
|
||||
for (int index = 0; index < decorators.size(); index++) {
|
||||
IrisDecorator decorator = decorators.get(index);
|
||||
String field = path + "[" + index + "]";
|
||||
IrisDecorationPart part = decorator.getPartOf();
|
||||
requirePlaced(decorator.getPalette(), data, field + ".palette", part);
|
||||
requirePlaced(decorator.getTopPalette(), data, field + ".topPalette", part);
|
||||
requireKnown(decorator.getWhitelist(), data, field + ".whitelist");
|
||||
requireKnown(decorator.getBlacklist(), data, field + ".blacklist");
|
||||
switch (part) {
|
||||
case SEA_FLOOR -> floor.add(decorator);
|
||||
case SEA_SURFACE -> surface.add(decorator);
|
||||
default -> shore.add(decorator);
|
||||
}
|
||||
}
|
||||
return new Decorators(floor.toArray(new IrisDecorator[0]), surface.toArray(new IrisDecorator[0]), shore.toArray(new IrisDecorator[0]));
|
||||
}
|
||||
|
||||
public boolean empty() {
|
||||
return floor.length == 0 && surface.length == 0 && shore.length == 0;
|
||||
}
|
||||
|
||||
private static void requirePlaced(List<IrisBlockData> blocks, IrisData data, String path, IrisDecorationPart part) {
|
||||
if (blocks == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < blocks.size(); index++) {
|
||||
String field = path + "[" + index + "]";
|
||||
String name = blocks.get(index).getBlock();
|
||||
NativeBlockState state = requireKnown(blocks.get(index), data, field);
|
||||
if (state.isFluid()) {
|
||||
throw new IllegalArgumentException(field + " names the fluid '" + name + "'; water decorators never place fluids");
|
||||
}
|
||||
if (part == IrisDecorationPart.SEA_FLOOR && !waterCompatible(state)) {
|
||||
throw new IllegalArgumentException(field + " names '" + name + "', which cannot stand in water; SEA_FLOOR decorators place"
|
||||
+ " only waterloggable or water plants such as seagrass, kelp, coral and sea pickles");
|
||||
}
|
||||
if (part != IrisDecorationPart.SEA_FLOOR && holdsWater(state)) {
|
||||
throw new IllegalArgumentException(field + " names '" + name + "', which holds water; " + part
|
||||
+ " decorators grow in air and place only blocks without water, never waterlogged blocks, seagrass, kelp, sea pickles or coral");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireKnown(List<IrisBlockData> blocks, IrisData data, String path) {
|
||||
if (blocks == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < blocks.size(); index++) {
|
||||
requireKnown(blocks.get(index), data, path + "[" + index + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeBlockState requireKnown(IrisBlockData block, IrisData data, String path) {
|
||||
NativeBlockState state = block.getBlockData(data);
|
||||
if (state == null || state.isAir()) {
|
||||
throw new IllegalArgumentException(path + " names the block '" + block.getBlock()
|
||||
+ "', which does not exist on this server; water decorators never place air");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static boolean waterCompatible(NativeBlockState state) {
|
||||
return IrisSurfaceDecorator.isAquaticPlacement(state) || IrisProceduralBlocks.propertyValue(state, "waterlogged") != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package art.arcane.iris.generation.decoration;
|
||||
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
@@ -71,21 +69,18 @@ final class IrisSugarCane {
|
||||
|
||||
private static boolean plannedWater(Engine engine, int x, int y, int z) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z);
|
||||
HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull();
|
||||
if (fluid != null) {
|
||||
return y > fluid.bedY() && y <= fluid.fluidHeadY()
|
||||
&& supportsAdjacent(complex.resolveHydrologyFluid(fluid.profileKey(), x, z));
|
||||
}
|
||||
int terrainHeight = (int) Math.round(complex.getHeightStream().getDouble(x, z));
|
||||
int fluidHeight = (int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(x, z));
|
||||
int fluidHeight = (int) Math.round(complex.getWaterHeadStream().getDouble(x, z));
|
||||
if (y <= terrainHeight || y > fluidHeight) {
|
||||
return false;
|
||||
}
|
||||
if (complex.getWaterRoleStream().get(x, z).wet()) {
|
||||
return supportsAdjacent(complex.resolveWaterFluid(x, z));
|
||||
}
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(x, z);
|
||||
KList<NativeBlockState> layers = biome.generateSeaLayers(x, z,
|
||||
new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData());
|
||||
int depth = fluidHeight - y;
|
||||
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSurfaceFluid(x, z));
|
||||
return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSeaFluid(x, z));
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
|
||||
record CaveCandidateBounds(
|
||||
int entryX,
|
||||
int entryY,
|
||||
int entryZ,
|
||||
int waterHeadY,
|
||||
int horizontalRadius,
|
||||
int maximumDepth,
|
||||
int dryHeadroom
|
||||
) {
|
||||
boolean containsColumn(int x, int z) {
|
||||
long deltaX = (long) x - entryX;
|
||||
long deltaZ = (long) z - entryZ;
|
||||
long radiusSquared = (long) horizontalRadius * horizontalRadius;
|
||||
return deltaX * deltaX + deltaZ * deltaZ <= radiusSquared;
|
||||
}
|
||||
|
||||
int minimumY() {
|
||||
return Math.toIntExact((long) entryY - maximumDepth);
|
||||
}
|
||||
|
||||
int maximumY() {
|
||||
return Math.max(entryY, Math.addExact(waterHeadY, dryHeadroom + 1));
|
||||
}
|
||||
|
||||
boolean contains(CavePosition position) {
|
||||
if (!containsColumn(position.x(), position.z())) {
|
||||
return false;
|
||||
}
|
||||
return position.y() >= minimumY() && position.y() <= maximumY();
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePositionIndex;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveFluidPolicy;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlannerSettings;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
final class CaveCandidateBuilder {
|
||||
private final RiverCourse course;
|
||||
private final HydraulicSegment representative;
|
||||
private final LinkedHashMap<CavePosition, HydrologyCaveAction> actions;
|
||||
private final LinkedHashSet<CavePosition> openings;
|
||||
private final CavePositionIndex openingIndex;
|
||||
private final List<CaveSurfaceOpening> surfaceOpenings;
|
||||
private int minimumX;
|
||||
private int maximumX;
|
||||
private int minimumY;
|
||||
private int maximumY;
|
||||
private int minimumZ;
|
||||
private int maximumZ;
|
||||
|
||||
CaveCandidateBuilder(RiverCourse course, HydraulicSegment representative) {
|
||||
this.course = course;
|
||||
this.representative = representative;
|
||||
this.actions = new LinkedHashMap<>();
|
||||
this.openings = new LinkedHashSet<>();
|
||||
this.openingIndex = new CavePositionIndex();
|
||||
this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course);
|
||||
this.minimumX = Integer.MAX_VALUE;
|
||||
this.maximumX = Integer.MIN_VALUE;
|
||||
this.minimumY = Integer.MAX_VALUE;
|
||||
this.maximumY = Integer.MIN_VALUE;
|
||||
this.minimumZ = Integer.MAX_VALUE;
|
||||
this.maximumZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
RiverCourse course() {
|
||||
return course;
|
||||
}
|
||||
|
||||
private HydraulicSegment representative() {
|
||||
return representative;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return actions.isEmpty();
|
||||
}
|
||||
|
||||
void addAction(CavePosition position, HydrologyCaveAction action) {
|
||||
HydrologyCaveAction existing = actions.get(position);
|
||||
if (existing == null || actionPriority(action) < actionPriority(existing)) {
|
||||
actions.put(position, action);
|
||||
}
|
||||
include(position);
|
||||
}
|
||||
|
||||
void addOpening(CavePosition position) {
|
||||
if (openingIndex.add(position.x(), position.y(), position.z())) {
|
||||
openings.add(position);
|
||||
}
|
||||
include(position);
|
||||
}
|
||||
|
||||
void addOpening(int x, int y, int z) {
|
||||
if (openingIndex.add(x, y, z)) {
|
||||
openings.add(new CavePosition(x, y, z));
|
||||
}
|
||||
include(x, y, z);
|
||||
}
|
||||
|
||||
void addOpeningNeighborhood(CavePosition position) {
|
||||
addOpening(position);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
||||
addOpening(
|
||||
position.x() + offset[0],
|
||||
position.y() + offset[1],
|
||||
position.z() + offset[2]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addAdjacentSurfaceOpenings(
|
||||
HydrologyFootprintCompiler.ValidationRaster validation
|
||||
) {
|
||||
ArrayList<CavePosition> actionPositions = new ArrayList<>(actions.keySet());
|
||||
for (CavePosition position : actionPositions) {
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = position.x() + offset[0];
|
||||
int neighborZ = position.z() + offset[1];
|
||||
if (actions.containsKey(new CavePosition(neighborX, position.y(), neighborZ))) {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
||||
if (layer.feature().courseId() == course.id() && layer.channel() && layer.fluidOwned()
|
||||
&& layer.bedY() < position.y() && position.y() <= layer.fluidHeadY()) {
|
||||
addOpening(neighborX, position.y(), neighborZ);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addSurfaceOpening(HydrologyColumnLayer layer, CavePosition position) {
|
||||
for (CaveSurfaceOpening opening : surfaceOpenings) {
|
||||
if (!opening.matches(layer, position)) {
|
||||
continue;
|
||||
}
|
||||
if (opening.includeNeighborhood()) {
|
||||
addOpening(position);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.NEIGHBORS) {
|
||||
int y = position.y() + offset[1];
|
||||
if (y >= opening.minimumY()) {
|
||||
addOpening(position.x() + offset[0], y, position.z() + offset[2]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addOpening(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void include(CavePosition position) {
|
||||
include(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private void include(int x, int y, int z) {
|
||||
minimumX = Math.min(minimumX, x);
|
||||
maximumX = Math.max(maximumX, x);
|
||||
minimumY = Math.min(minimumY, y);
|
||||
maximumY = Math.max(maximumY, y);
|
||||
minimumZ = Math.min(minimumZ, z);
|
||||
maximumZ = Math.max(maximumZ, z);
|
||||
}
|
||||
|
||||
HydrologyCaveCandidate build(HydrologyCaveCourseFilter.Options options) {
|
||||
HydrologyPoint start = representative.start();
|
||||
int waterHead = representative.upstreamHeadY();
|
||||
int entryY = Math.max(waterHead, maximumY);
|
||||
CavePosition entry = new CavePosition(start.x(), entryY, start.z());
|
||||
CavePosition target = new CavePosition(start.x(), waterHead, start.z());
|
||||
int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius(
|
||||
entry.x(),
|
||||
entry.z(),
|
||||
minimumX,
|
||||
maximumX,
|
||||
minimumZ,
|
||||
maximumZ
|
||||
);
|
||||
int maximumDepth = Math.max(1, entryY - minimumY + 2);
|
||||
int dryHeadroom = Math.max(0, maximumY - waterHead);
|
||||
int volume = Math.max(1, actions.size());
|
||||
HydrologyCavePlannerSettings settings = new HydrologyCavePlannerSettings(
|
||||
horizontalRadius,
|
||||
maximumDepth,
|
||||
volume,
|
||||
1,
|
||||
1,
|
||||
dryHeadroom,
|
||||
HydrologyCaveFluidPolicy.REJECT_EXISTING,
|
||||
horizontalRadius,
|
||||
maximumDepth
|
||||
);
|
||||
return new HydrologyCaveCandidate(
|
||||
new HydrologyCaveSource(
|
||||
course.id(),
|
||||
entry,
|
||||
target,
|
||||
waterHead,
|
||||
HydrologyCaveMode.GENERATED_GROTTO
|
||||
),
|
||||
course.profileKey(),
|
||||
settings,
|
||||
options.connectToExistingCaves() && course.type() == RiverCourseType.UNDERGROUND,
|
||||
actions,
|
||||
openings
|
||||
);
|
||||
}
|
||||
|
||||
private static int actionPriority(HydrologyCaveAction action) {
|
||||
return switch (action) {
|
||||
case WET_SOURCE -> 0;
|
||||
case FALLING_FLUID -> 1;
|
||||
case DRY_AIR -> 2;
|
||||
case SEAL_GUARD -> 3;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
-395
@@ -1,395 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class CaveCandidateSpanBuilder {
|
||||
private final RiverCourse course;
|
||||
private final HydraulicSegment representative;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> actions;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> openings;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> coastalGrottoActions;
|
||||
private final LinkedHashMap<Long, CaveSpanSet> inlandGrottoActions;
|
||||
private final List<CaveSurfaceOpening> surfaceOpenings;
|
||||
private int minimumX;
|
||||
private int maximumX;
|
||||
private int minimumY;
|
||||
private int maximumY;
|
||||
private int minimumZ;
|
||||
private int maximumZ;
|
||||
|
||||
CaveCandidateSpanBuilder(RiverCourse course, HydraulicSegment representative) {
|
||||
this.course = course;
|
||||
this.representative = representative;
|
||||
this.actions = new LinkedHashMap<>();
|
||||
this.openings = new LinkedHashMap<>();
|
||||
this.coastalGrottoActions = new LinkedHashMap<>();
|
||||
this.inlandGrottoActions = new LinkedHashMap<>();
|
||||
this.surfaceOpenings = HydrologyCaveCourseFilter.surfaceOpenings(course);
|
||||
this.minimumX = Integer.MAX_VALUE;
|
||||
this.maximumX = Integer.MIN_VALUE;
|
||||
this.minimumY = Integer.MAX_VALUE;
|
||||
this.maximumY = Integer.MIN_VALUE;
|
||||
this.minimumZ = Integer.MAX_VALUE;
|
||||
this.maximumZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
RiverCourse course() {
|
||||
return course;
|
||||
}
|
||||
|
||||
HydraulicSegment representative() {
|
||||
return representative;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return actions.isEmpty();
|
||||
}
|
||||
|
||||
boolean allowsIntentionalSurfaceExposure() {
|
||||
return course.surfaceSinkholeContinuation();
|
||||
}
|
||||
|
||||
void addAction(int x, int z, HydrologyColumnLayer layer) {
|
||||
int minimumActionY = layer.bedY() + 1;
|
||||
int maximumActionY = layer.ceilingY();
|
||||
if (minimumActionY > maximumActionY) {
|
||||
return;
|
||||
}
|
||||
addSpan(actions, x, z, minimumActionY, maximumActionY);
|
||||
if (layer.feature().type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
addSpan(coastalGrottoActions, x, z, minimumActionY, maximumActionY);
|
||||
} else if (layer.feature().type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
addSpan(inlandGrottoActions, x, z, minimumActionY, maximumActionY);
|
||||
}
|
||||
include(x, minimumActionY, z);
|
||||
include(x, maximumActionY, z);
|
||||
for (CaveSurfaceOpening opening : surfaceOpenings) {
|
||||
if (!opening.matchesColumn(layer, x, z)) {
|
||||
continue;
|
||||
}
|
||||
int minimumOpeningY = Math.max(minimumActionY, opening.minimumY());
|
||||
if (minimumOpeningY > maximumActionY) {
|
||||
continue;
|
||||
}
|
||||
if (!opening.includeNeighborhood()) {
|
||||
addOpening(x, z, minimumOpeningY, maximumActionY);
|
||||
continue;
|
||||
}
|
||||
addOpening(x, z, Math.max(minimumOpeningY - 1, opening.minimumY()), maximumActionY + 1);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumActionY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addOceanOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) {
|
||||
if (minimumOpeningY <= maximumOpeningY) {
|
||||
addOpening(x, z, minimumOpeningY, maximumOpeningY);
|
||||
}
|
||||
}
|
||||
|
||||
void addOpeningNeighborhood(
|
||||
int x,
|
||||
int z,
|
||||
int minimumOpeningY,
|
||||
int maximumOpeningY
|
||||
) {
|
||||
if (minimumOpeningY > maximumOpeningY) {
|
||||
return;
|
||||
}
|
||||
addOpening(x, z, minimumOpeningY - 1, maximumOpeningY + 1);
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
addOpening(x + offset[0], z + offset[1], minimumOpeningY, maximumOpeningY);
|
||||
}
|
||||
}
|
||||
|
||||
void addAdjacentSurfaceOpenings(
|
||||
HydrologyFootprintCompiler.ValidationRaster validation
|
||||
) {
|
||||
for (Map.Entry<Long, CaveSpanSet> entry : actions.entrySet()) {
|
||||
int x = RiverFootprint.unpackX(entry.getKey());
|
||||
int z = RiverFootprint.unpackZ(entry.getKey());
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = x + offset[0];
|
||||
int neighborZ = z + offset[1];
|
||||
HydrologyColumnSample neighbor = validation.surfaceColumnAt(neighborX, neighborZ);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : neighbor.layers()) {
|
||||
if (layer.feature().courseId() != course.id() || !layer.channel() || !layer.fluidOwned()) {
|
||||
continue;
|
||||
}
|
||||
for (CaveYSpan span : entry.getValue().spans()) {
|
||||
int minimumY = Math.max(span.minimumY(), layer.bedY() + 1);
|
||||
int maximumY = Math.min(span.maximumY(), layer.fluidHeadY());
|
||||
if (minimumY <= maximumY) {
|
||||
addAdjacentFluidOpening(neighborX, neighborZ, minimumY, maximumY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addAdjacentFluidOpening(int x, int z, int minimumY, int maximumY) {
|
||||
CaveSpanSet owned = actions.get(RiverFootprint.pack(x, z));
|
||||
if (owned == null) {
|
||||
addOpening(x, z, minimumY, maximumY);
|
||||
return;
|
||||
}
|
||||
int cursor = minimumY;
|
||||
for (CaveYSpan span : owned.spans()) {
|
||||
if (span.maximumY() < cursor) {
|
||||
continue;
|
||||
}
|
||||
if (span.minimumY() > maximumY) {
|
||||
break;
|
||||
}
|
||||
if (cursor < span.minimumY()) {
|
||||
addOpening(x, z, cursor, Math.min(maximumY, span.minimumY() - 1));
|
||||
}
|
||||
cursor = Math.max(cursor, span.maximumY() + 1);
|
||||
if (cursor > maximumY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
addOpening(x, z, cursor, maximumY);
|
||||
}
|
||||
|
||||
HydrologyFeatureType oversizedGrotto(HydrologyCaveCourseFilter.Options options) {
|
||||
if (positionCount(coastalGrottoActions)
|
||||
> options.maximumVolume(HydrologyFeatureType.COASTAL_GROTTO)) {
|
||||
return HydrologyFeatureType.COASTAL_GROTTO;
|
||||
}
|
||||
if (positionCount(inlandGrottoActions)
|
||||
> options.maximumVolume(HydrologyFeatureType.INLAND_GROTTO)) {
|
||||
return HydrologyFeatureType.INLAND_GROTTO;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
long positionCount() {
|
||||
return positionCount(actions);
|
||||
}
|
||||
|
||||
boolean exposed(CaveVoxelView view) {
|
||||
CaveCandidateBounds bounds = bounds();
|
||||
for (Map.Entry<Long, CaveSpanSet> entry : actions.entrySet()) {
|
||||
int x = RiverFootprint.unpackX(entry.getKey());
|
||||
int z = RiverFootprint.unpackZ(entry.getKey());
|
||||
CaveSpanSet actionSpans = entry.getValue();
|
||||
CaveSpanSet openingSpans = openings.get(entry.getKey());
|
||||
if (exposedDifference(actionSpans, null, openingSpans, x, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
for (CaveYSpan span : actionSpans.spans()) {
|
||||
int lowerBoundary = span.minimumY() - 1;
|
||||
if (!actionSpans.contains(lowerBoundary)
|
||||
&& !contains(openingSpans, lowerBoundary)
|
||||
&& exposed(x, lowerBoundary, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
int upperBoundary = span.maximumY() + 1;
|
||||
if (!actionSpans.contains(upperBoundary)
|
||||
&& !contains(openingSpans, upperBoundary)
|
||||
&& exposed(x, upperBoundary, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int[] offset : HydrologyCaveCourseFilter.HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = x + offset[0];
|
||||
int neighborZ = z + offset[1];
|
||||
long neighborKey = RiverFootprint.pack(neighborX, neighborZ);
|
||||
if (exposedDifference(
|
||||
actionSpans,
|
||||
actions.get(neighborKey),
|
||||
openings.get(neighborKey),
|
||||
neighborX,
|
||||
neighborZ,
|
||||
bounds,
|
||||
view
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean exposedDifference(
|
||||
CaveSpanSet source,
|
||||
CaveSpanSet excludedActions,
|
||||
CaveSpanSet excludedOpenings,
|
||||
int x,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
for (CaveYSpan span : source.spans()) {
|
||||
if (exposedDifference(
|
||||
span,
|
||||
excludedActions,
|
||||
excludedOpenings,
|
||||
x,
|
||||
z,
|
||||
bounds,
|
||||
view
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean exposedDifference(
|
||||
CaveYSpan source,
|
||||
CaveSpanSet excludedActions,
|
||||
CaveSpanSet excludedOpenings,
|
||||
int x,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
List<CaveYSpan> actionSpans = excludedActions == null ? List.of() : excludedActions.spans();
|
||||
List<CaveYSpan> openingSpans = excludedOpenings == null ? List.of() : excludedOpenings.spans();
|
||||
int actionIndex = 0;
|
||||
int openingIndex = 0;
|
||||
long cursor = source.minimumY();
|
||||
while (cursor <= source.maximumY()) {
|
||||
while (actionIndex < actionSpans.size()
|
||||
&& actionSpans.get(actionIndex).maximumY() < cursor) {
|
||||
actionIndex++;
|
||||
}
|
||||
while (openingIndex < openingSpans.size()
|
||||
&& openingSpans.get(openingIndex).maximumY() < cursor) {
|
||||
openingIndex++;
|
||||
}
|
||||
CaveYSpan action = actionIndex < actionSpans.size() ? actionSpans.get(actionIndex) : null;
|
||||
CaveYSpan opening = openingIndex < openingSpans.size() ? openingSpans.get(openingIndex) : null;
|
||||
CaveYSpan excluded = first(action, opening);
|
||||
if (excluded == null || excluded.minimumY() > source.maximumY()) {
|
||||
return exposed(x, (int) cursor, source.maximumY(), z, bounds, view);
|
||||
}
|
||||
if (cursor < excluded.minimumY()
|
||||
&& exposed(x, (int) cursor, excluded.minimumY() - 1, z, bounds, view)) {
|
||||
return true;
|
||||
}
|
||||
cursor = Math.max(cursor, (long) excluded.maximumY() + 1L);
|
||||
if (excluded == action) {
|
||||
actionIndex++;
|
||||
} else {
|
||||
openingIndex++;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private CaveYSpan first(CaveYSpan first, CaveYSpan second) {
|
||||
if (first == null) {
|
||||
return second;
|
||||
}
|
||||
if (second == null) {
|
||||
return first;
|
||||
}
|
||||
return first.minimumY() <= second.minimumY() ? first : second;
|
||||
}
|
||||
|
||||
boolean exposed(
|
||||
int x,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
if (!bounds.containsColumn(x, z)) {
|
||||
return false;
|
||||
}
|
||||
int boundedMinimumY = Math.max(minimumY, bounds.minimumY());
|
||||
int boundedMaximumY = Math.min(maximumY, bounds.maximumY());
|
||||
return boundedMinimumY <= boundedMaximumY
|
||||
&& view.hasAboveTerrainSurface(x, z, boundedMinimumY, boundedMaximumY);
|
||||
}
|
||||
|
||||
boolean exposed(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
CaveCandidateBounds bounds,
|
||||
CaveVoxelView view
|
||||
) {
|
||||
CavePosition position = new CavePosition(x, y, z);
|
||||
return bounds.contains(position)
|
||||
&& view.isInWorld(position)
|
||||
&& view.isAboveTerrainSurface(position);
|
||||
}
|
||||
|
||||
private CaveCandidateBounds bounds() {
|
||||
HydrologyPoint start = representative.start();
|
||||
int waterHead = representative.upstreamHeadY();
|
||||
int entryY = Math.max(waterHead, maximumY);
|
||||
int horizontalRadius = HydrologyCaveCourseFilter.horizontalRadius(
|
||||
start.x(),
|
||||
start.z(),
|
||||
minimumX,
|
||||
maximumX,
|
||||
minimumZ,
|
||||
maximumZ
|
||||
);
|
||||
int maximumDepth = Math.max(1, entryY - minimumY + 2);
|
||||
int dryHeadroom = Math.max(0, maximumY - waterHead);
|
||||
return new CaveCandidateBounds(
|
||||
start.x(),
|
||||
entryY,
|
||||
start.z(),
|
||||
waterHead,
|
||||
horizontalRadius,
|
||||
maximumDepth,
|
||||
dryHeadroom
|
||||
);
|
||||
}
|
||||
|
||||
private void addOpening(int x, int z, int minimumOpeningY, int maximumOpeningY) {
|
||||
addSpan(openings, x, z, minimumOpeningY, maximumOpeningY);
|
||||
include(x, minimumOpeningY, z);
|
||||
include(x, maximumOpeningY, z);
|
||||
}
|
||||
|
||||
private void include(int x, int y, int z) {
|
||||
minimumX = Math.min(minimumX, x);
|
||||
maximumX = Math.max(maximumX, x);
|
||||
minimumY = Math.min(minimumY, y);
|
||||
maximumY = Math.max(maximumY, y);
|
||||
minimumZ = Math.min(minimumZ, z);
|
||||
maximumZ = Math.max(maximumZ, z);
|
||||
}
|
||||
|
||||
private static void addSpan(
|
||||
Map<Long, CaveSpanSet> spans,
|
||||
int x,
|
||||
int z,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
spans.computeIfAbsent(RiverFootprint.pack(x, z), (Long ignored) -> new CaveSpanSet())
|
||||
.add(minimumY, maximumY);
|
||||
}
|
||||
|
||||
private static boolean contains(CaveSpanSet spans, int y) {
|
||||
return spans != null && spans.contains(y);
|
||||
}
|
||||
|
||||
static long positionCount(Map<Long, CaveSpanSet> spans) {
|
||||
long count = 0L;
|
||||
for (CaveSpanSet spanSet : spans.values()) {
|
||||
count += spanSet.positionCount();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CaveCourseGraph(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets
|
||||
) {
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
final class CaveSpanSet {
|
||||
private int[] pending;
|
||||
private int size;
|
||||
private List<CaveYSpan> normalized;
|
||||
|
||||
CaveSpanSet() {
|
||||
this.pending = new int[4];
|
||||
this.normalized = null;
|
||||
}
|
||||
|
||||
void add(int minimumY, int maximumY) {
|
||||
if (minimumY > maximumY) {
|
||||
return;
|
||||
}
|
||||
int requiredLength = Math.multiplyExact(size + 1, 2);
|
||||
if (requiredLength > pending.length) {
|
||||
pending = Arrays.copyOf(pending, Math.multiplyExact(pending.length, 2));
|
||||
}
|
||||
pending[size * 2] = minimumY;
|
||||
pending[size * 2 + 1] = maximumY;
|
||||
size++;
|
||||
normalized = null;
|
||||
}
|
||||
|
||||
List<CaveYSpan> spans() {
|
||||
if (normalized != null) {
|
||||
return normalized;
|
||||
}
|
||||
sortPending();
|
||||
ArrayList<CaveYSpan> merged = new ArrayList<>(size);
|
||||
for (int index = 0; index < size; index++) {
|
||||
CaveYSpan span = new CaveYSpan(pending[index * 2], pending[index * 2 + 1]);
|
||||
if (merged.isEmpty()) {
|
||||
merged.add(span);
|
||||
continue;
|
||||
}
|
||||
CaveYSpan previous = merged.getLast();
|
||||
if ((long) span.minimumY() > (long) previous.maximumY() + 1L) {
|
||||
merged.add(span);
|
||||
continue;
|
||||
}
|
||||
merged.set(
|
||||
merged.size() - 1,
|
||||
new CaveYSpan(previous.minimumY(), Math.max(previous.maximumY(), span.maximumY()))
|
||||
);
|
||||
}
|
||||
normalized = List.copyOf(merged);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void sortPending() {
|
||||
for (int index = 1; index < size; index++) {
|
||||
int minimumY = pending[index * 2];
|
||||
int maximumY = pending[index * 2 + 1];
|
||||
int insertionIndex = index;
|
||||
while (insertionIndex > 0) {
|
||||
int previousMinimumY = pending[(insertionIndex - 1) * 2];
|
||||
int previousMaximumY = pending[(insertionIndex - 1) * 2 + 1];
|
||||
if (previousMinimumY < minimumY
|
||||
|| previousMinimumY == minimumY && previousMaximumY <= maximumY) {
|
||||
break;
|
||||
}
|
||||
pending[insertionIndex * 2] = previousMinimumY;
|
||||
pending[insertionIndex * 2 + 1] = previousMaximumY;
|
||||
insertionIndex--;
|
||||
}
|
||||
pending[insertionIndex * 2] = minimumY;
|
||||
pending[insertionIndex * 2 + 1] = maximumY;
|
||||
}
|
||||
}
|
||||
|
||||
boolean contains(int y) {
|
||||
List<CaveYSpan> spans = spans();
|
||||
int minimumIndex = 0;
|
||||
int maximumIndex = spans.size() - 1;
|
||||
while (minimumIndex <= maximumIndex) {
|
||||
int index = (minimumIndex + maximumIndex) >>> 1;
|
||||
CaveYSpan span = spans.get(index);
|
||||
if (y < span.minimumY()) {
|
||||
maximumIndex = index - 1;
|
||||
} else if (y > span.maximumY()) {
|
||||
minimumIndex = index + 1;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
long positionCount() {
|
||||
long count = 0L;
|
||||
for (CaveYSpan span : spans()) {
|
||||
count += (long) span.maximumY() - span.minimumY() + 1L;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record CaveSurfaceComposition(
|
||||
HydrologyCaveCandidate candidate,
|
||||
boolean accepted
|
||||
) {
|
||||
static CaveSurfaceComposition accepted(HydrologyCaveCandidate candidate) {
|
||||
return new CaveSurfaceComposition(Objects.requireNonNull(candidate), true);
|
||||
}
|
||||
|
||||
static CaveSurfaceComposition rejected() {
|
||||
return new CaveSurfaceComposition(null, false);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record CaveSurfaceOpening(
|
||||
long segmentId,
|
||||
HydrologyPoint point,
|
||||
double interiorX,
|
||||
double interiorZ,
|
||||
long radiusSquared,
|
||||
int minimumY,
|
||||
boolean includeNeighborhood,
|
||||
boolean surfaceFall
|
||||
) {
|
||||
static CaveSurfaceOpening create(
|
||||
HydraulicSegment segment,
|
||||
boolean start,
|
||||
long radiusSquared,
|
||||
int minimumY,
|
||||
boolean includeNeighborhood
|
||||
) {
|
||||
List<HydrologyPoint> centerline = segment.centerline();
|
||||
int boundaryIndex = start ? 0 : centerline.size() - 1;
|
||||
int interiorIndex = start
|
||||
? Math.min(1, centerline.size() - 1)
|
||||
: Math.max(0, centerline.size() - 2);
|
||||
HydrologyPoint point = centerline.get(boundaryIndex);
|
||||
HydrologyPoint interior = centerline.get(interiorIndex);
|
||||
double deltaX = interior.x() - point.x();
|
||||
double deltaZ = interior.z() - point.z();
|
||||
double lengthSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double scale = 0D;
|
||||
if (includeNeighborhood) {
|
||||
scale = lengthSquared <= radiusSquared
|
||||
? 1D
|
||||
: StrictMath.sqrt(radiusSquared / lengthSquared);
|
||||
}
|
||||
return new CaveSurfaceOpening(
|
||||
segment.id(),
|
||||
point,
|
||||
point.x() + deltaX * scale,
|
||||
point.z() + deltaZ * scale,
|
||||
radiusSquared,
|
||||
minimumY,
|
||||
includeNeighborhood,
|
||||
segment.type().isSurface() && segment.fallingFluid()
|
||||
);
|
||||
}
|
||||
|
||||
boolean matches(HydrologyColumnLayer layer, CavePosition position) {
|
||||
return matchesColumn(layer, position.x(), position.z())
|
||||
&& position.y() >= minimumY;
|
||||
}
|
||||
|
||||
boolean matchesColumn(HydrologyColumnLayer layer, int x, int z) {
|
||||
if (surfaceFall) {
|
||||
return layer.fallingFluid() && layer.feature().segmentId() == segmentId;
|
||||
}
|
||||
return (includeNeighborhood || layer.feature().segmentId() == segmentId)
|
||||
&& distanceSquared(x, z) <= radiusSquared;
|
||||
}
|
||||
|
||||
private double distanceSquared(int x, int z) {
|
||||
double pathX = interiorX - point.x();
|
||||
double pathZ = interiorZ - point.z();
|
||||
double pathLengthSquared = pathX * pathX + pathZ * pathZ;
|
||||
double positionX = x - point.x();
|
||||
double positionZ = z - point.z();
|
||||
double progress = pathLengthSquared == 0D
|
||||
? 0D
|
||||
: (positionX * pathX + positionZ * pathZ) / pathLengthSquared;
|
||||
double clampedProgress = Math.max(0D, Math.min(1D, progress));
|
||||
double deltaX = positionX - pathX * clampedProgress;
|
||||
double deltaZ = positionZ - pathZ * clampedProgress;
|
||||
return deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record CaveYSpan(int minimumY, int maximumY) {
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
record CompiledGraph(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
Map<Integer, DrainageEdge> edgeByUpstream
|
||||
) {
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record CourseEdgeKey(RiverCourseType type, long edgeId) {
|
||||
CourseEdgeKey {
|
||||
Objects.requireNonNull(type, "type");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package art.arcane.iris.generation.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"));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
interface CrossTileDraftAdmission {
|
||||
void prepare();
|
||||
|
||||
CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result);
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package art.arcane.iris.generation.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"));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
record CrossTileRejectedCourse(RiverCourse course, long winnerSourceId) {
|
||||
CrossTileRejectedCourse {
|
||||
Objects.requireNonNull(course, "course");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record CrossTileRejectionKey(long courseId, long winnerCourseId) {
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package art.arcane.iris.generation.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"));
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
package art.arcane.iris.generation.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;
|
||||
private final Set<HydrologyTileKey> resolving;
|
||||
final HydrologyTileKey root;
|
||||
private final long maximumTileOffset;
|
||||
private 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 for "
|
||||
+ key.tileX() + "," + key.tileZ() + " while planning " + root.tileX() + "," + root.tileZ() + ".");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package art.arcane.iris.generation.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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
enum DraftPhase {
|
||||
CONTEXT,
|
||||
SELECT,
|
||||
SETTLE,
|
||||
PUBLISH
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package art.arcane.iris.generation.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 {
|
||||
private final long[] phaseNanos = new long[DraftPhase.values().length];
|
||||
private long dependencyNanos;
|
||||
private int dependencies;
|
||||
private 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record DrainageEdge(
|
||||
long id,
|
||||
long upstreamNodeId,
|
||||
long downstreamNodeId,
|
||||
long outletId,
|
||||
double cost,
|
||||
int contributingSurfaceSources,
|
||||
int contributingUndergroundSources,
|
||||
List<HydrologyPoint> centerline
|
||||
) {
|
||||
public DrainageEdge {
|
||||
if (upstreamNodeId == downstreamNodeId) {
|
||||
throw new IllegalArgumentException("A drainage edge cannot point to the same node.");
|
||||
}
|
||||
if (!Double.isFinite(cost) || cost <= 0D) {
|
||||
throw new IllegalArgumentException("Drainage edge cost must be finite and positive.");
|
||||
}
|
||||
if (contributingSurfaceSources < 0 || contributingUndergroundSources < 0) {
|
||||
throw new IllegalArgumentException("Contributing source counts cannot be negative.");
|
||||
}
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.size() < 2) {
|
||||
throw new IllegalArgumentException("A drainage edge requires at least two centerline points.");
|
||||
}
|
||||
}
|
||||
|
||||
public int totalContributingSources() {
|
||||
return contributingSurfaceSources + contributingUndergroundSources;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record DrainageNode(
|
||||
long id,
|
||||
int x,
|
||||
int z,
|
||||
HydrologyTerrainSample terrain,
|
||||
double potential,
|
||||
long outletId
|
||||
) {
|
||||
public DrainageNode {
|
||||
Objects.requireNonNull(terrain, "terrain");
|
||||
if (!Double.isFinite(potential) || potential < 0D) {
|
||||
throw new IllegalArgumentException("Drainage potential must be finite and non-negative.");
|
||||
}
|
||||
if (terrain.ocean()) {
|
||||
throw new IllegalArgumentException("Drainage nodes must remain landward of the first ocean column.");
|
||||
}
|
||||
}
|
||||
|
||||
public HydrologyPoint naturalPoint() {
|
||||
return new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintCenterlineProjection(
|
||||
int pointIndex,
|
||||
double x,
|
||||
double z,
|
||||
double distance,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record FootprintCourseRasterKey(
|
||||
long courseId,
|
||||
RiverCourseType courseType,
|
||||
String profileKey,
|
||||
List<HydraulicSegment> segments
|
||||
) {
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintFeatureKey(
|
||||
long courseId,
|
||||
long segmentId,
|
||||
HydrologyFeatureType type,
|
||||
int role,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintLayerShape(
|
||||
int channelRadius,
|
||||
double shoreWidth,
|
||||
double gradingWidth,
|
||||
int bed,
|
||||
int fluidHead,
|
||||
int ceiling,
|
||||
boolean ellipsoid,
|
||||
boolean archedChannel,
|
||||
boolean roundedSurfaceBed,
|
||||
boolean organicBoundary,
|
||||
boolean fallingThroat,
|
||||
boolean inlet
|
||||
) {
|
||||
double totalRadius() {
|
||||
return channelRadius + shoreWidth + gradingWidth;
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
final class FootprintMutableColumn {
|
||||
private final int x;
|
||||
private final int z;
|
||||
private final int naturalHeight;
|
||||
private final int seaLevel;
|
||||
private final boolean ocean;
|
||||
private final String parentBiomeKey;
|
||||
private HydrologyColumnLayer singleLayer;
|
||||
private LinkedHashMap<Long, HydrologyColumnLayer> layers;
|
||||
|
||||
FootprintMutableColumn(int x, int z, HydrologyTerrainSample terrain, int seaLevel) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.naturalHeight = terrain.naturalHeight();
|
||||
this.seaLevel = seaLevel;
|
||||
this.ocean = terrain.ocean();
|
||||
this.parentBiomeKey = terrain.parentBiomeKey();
|
||||
}
|
||||
|
||||
FootprintMutableColumn(HydrologyColumnSample sample) {
|
||||
this.x = sample.x();
|
||||
this.z = sample.z();
|
||||
this.naturalHeight = sample.naturalHeight();
|
||||
this.seaLevel = sample.seaLevel();
|
||||
this.ocean = sample.ocean();
|
||||
this.parentBiomeKey = sample.parentBiomeKey();
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
add(layer);
|
||||
}
|
||||
}
|
||||
|
||||
void add(HydrologyColumnLayer layer) {
|
||||
if (layers == null) {
|
||||
if (singleLayer == null) {
|
||||
singleLayer = layer;
|
||||
return;
|
||||
}
|
||||
if (singleLayer.feature().id() == layer.feature().id()) {
|
||||
singleLayer = merge(singleLayer, layer);
|
||||
return;
|
||||
}
|
||||
layers = new LinkedHashMap<>();
|
||||
layers.put(singleLayer.feature().id(), singleLayer);
|
||||
singleLayer = null;
|
||||
}
|
||||
HydrologyColumnLayer existing = layers.get(layer.feature().id());
|
||||
layers.put(layer.feature().id(), existing == null ? layer : merge(existing, layer));
|
||||
}
|
||||
|
||||
HydrologyColumnSample build() {
|
||||
List<HydrologyColumnLayer> builtLayers;
|
||||
if (layers != null) {
|
||||
builtLayers = resolveSurfaceHeadConflicts(new ArrayList<>(layers.values()));
|
||||
} else if (singleLayer != null) {
|
||||
builtLayers = List.of(singleLayer);
|
||||
} else {
|
||||
builtLayers = List.of();
|
||||
}
|
||||
return new HydrologyColumnSample(
|
||||
x,
|
||||
z,
|
||||
naturalHeight,
|
||||
seaLevel,
|
||||
ocean,
|
||||
parentBiomeKey,
|
||||
builtLayers
|
||||
);
|
||||
}
|
||||
|
||||
private List<HydrologyColumnLayer> resolveSurfaceHeadConflicts(List<HydrologyColumnLayer> candidates) {
|
||||
HashMap<Long, HydrologyColumnLayer> selectedByCourse = new HashMap<>();
|
||||
for (HydrologyColumnLayer candidate : candidates) {
|
||||
if (!conflictingSurfaceCandidate(candidate)) {
|
||||
continue;
|
||||
}
|
||||
long courseId = candidate.feature().courseId();
|
||||
HydrologyColumnLayer selected = selectedByCourse.get(courseId);
|
||||
if (selected == null || prefersSurfaceLayer(candidate, selected)) {
|
||||
selectedByCourse.put(courseId, candidate);
|
||||
}
|
||||
}
|
||||
if (selectedByCourse.isEmpty()) {
|
||||
return candidates;
|
||||
}
|
||||
ArrayList<HydrologyColumnLayer> resolved = new ArrayList<>(candidates.size());
|
||||
for (HydrologyColumnLayer candidate : candidates) {
|
||||
HydrologyColumnLayer selected = selectedByCourse.get(candidate.feature().courseId());
|
||||
if (!conflictingSurfaceCandidate(candidate)
|
||||
|| selected == null
|
||||
|| candidate.fluidHeadY() == selected.fluidHeadY()) {
|
||||
resolved.add(candidate);
|
||||
}
|
||||
}
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
|
||||
private boolean conflictingSurfaceCandidate(HydrologyColumnLayer layer) {
|
||||
return layer.feature().type().isSurface()
|
||||
&& layer.channel()
|
||||
&& layer.connectedFluid()
|
||||
&& layer.fluidOwned()
|
||||
&& !layer.fallingFluid();
|
||||
}
|
||||
|
||||
private boolean prefersSurfaceLayer(HydrologyColumnLayer candidate, HydrologyColumnLayer selected) {
|
||||
if (candidate.receivingPool() != selected.receivingPool()) {
|
||||
return candidate.receivingPool();
|
||||
}
|
||||
if (candidate.fluidHeadY() != selected.fluidHeadY()) {
|
||||
return candidate.fluidHeadY() > selected.fluidHeadY();
|
||||
}
|
||||
return candidate.feature().id() < selected.feature().id();
|
||||
}
|
||||
|
||||
void merge(HydrologyColumnSample sample) {
|
||||
if (sample.x() != x
|
||||
|| sample.z() != z
|
||||
|| sample.naturalHeight() != naturalHeight
|
||||
|| sample.seaLevel() != seaLevel
|
||||
|| sample.ocean() != ocean
|
||||
|| !sample.parentBiomeKey().equals(parentBiomeKey)) {
|
||||
throw new IllegalStateException("Hydrology course footprints disagree on terrain metadata at "
|
||||
+ x + "," + z + ".");
|
||||
}
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
add(layer);
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer merge(HydrologyColumnLayer first, HydrologyColumnLayer second) {
|
||||
if (first.equals(second)) {
|
||||
return first;
|
||||
}
|
||||
HydrologyFeatureRef feature = first.feature().y() <= second.feature().y()
|
||||
? first.feature()
|
||||
: second.feature();
|
||||
boolean drySurfaceBlend = first.feature().type().isSurface()
|
||||
&& second.feature().type().isSurface()
|
||||
&& !first.channel()
|
||||
&& !second.channel();
|
||||
int bedY = drySurfaceBlend
|
||||
? Math.max(first.bedY(), second.bedY())
|
||||
: Math.min(first.bedY(), second.bedY());
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
bedY,
|
||||
Math.max(first.fluidHeadY(), second.fluidHeadY()),
|
||||
Math.max(first.ceilingY(), second.ceilingY()),
|
||||
first.channel() || second.channel(),
|
||||
first.shore() || second.shore(),
|
||||
first.grading() || second.grading(),
|
||||
first.connectedFluid() || second.connectedFluid(),
|
||||
first.fallingFluid() || second.fallingFluid(),
|
||||
first.receivingPool() || second.receivingPool(),
|
||||
(first.terrainOwned() || second.terrainOwned()) && (!drySurfaceBlend || bedY != naturalHeight),
|
||||
first.fluidOwned() || second.fluidOwned(),
|
||||
first.oceanApron() || second.oceanApron(),
|
||||
first.profileKey(),
|
||||
first.surfaceBiomeKey(),
|
||||
first.mouthBiomeKey(),
|
||||
first.shoreBiomeKey(),
|
||||
first.bankBiomeKey(),
|
||||
first.floodedCaveBiomeKey()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintRasterStencil(
|
||||
int[] deltaXs,
|
||||
int[] deltaZs,
|
||||
double[] distances
|
||||
) {
|
||||
int size() {
|
||||
return deltaXs.length;
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
final class FootprintRasterStencilBuilder {
|
||||
private int[] deltaXs;
|
||||
private int[] deltaZs;
|
||||
private double[] distances;
|
||||
private int size;
|
||||
|
||||
FootprintRasterStencilBuilder() {
|
||||
this.deltaXs = new int[64];
|
||||
this.deltaZs = new int[64];
|
||||
this.distances = new double[64];
|
||||
}
|
||||
|
||||
void add(int deltaX, int deltaZ, double distance) {
|
||||
if (size == deltaXs.length) {
|
||||
int expandedSize = Math.multiplyExact(size, 2);
|
||||
deltaXs = Arrays.copyOf(deltaXs, expandedSize);
|
||||
deltaZs = Arrays.copyOf(deltaZs, expandedSize);
|
||||
distances = Arrays.copyOf(distances, expandedSize);
|
||||
}
|
||||
deltaXs[size] = deltaX;
|
||||
deltaZs[size] = deltaZ;
|
||||
distances[size] = distance;
|
||||
size++;
|
||||
}
|
||||
|
||||
FootprintRasterStencil build() {
|
||||
return new FootprintRasterStencil(
|
||||
Arrays.copyOf(deltaXs, size),
|
||||
Arrays.copyOf(deltaZs, size),
|
||||
Arrays.copyOf(distances, size)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record FootprintRasterStencilKey(
|
||||
int channelRadius,
|
||||
double shoreWidth,
|
||||
double gradingWidth
|
||||
) {
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record FootprintValidationCourseRaster(
|
||||
long courseId,
|
||||
List<HydrologyColumnSample> columns,
|
||||
SurfaceFootprint surface
|
||||
) {
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydraulicChannelProfile(double[] widths, double[] depths) {
|
||||
public HydraulicChannelProfile {
|
||||
Objects.requireNonNull(widths, "widths");
|
||||
Objects.requireNonNull(depths, "depths");
|
||||
if (widths.length == 0 || widths.length != depths.length) {
|
||||
throw new IllegalArgumentException("Channel dimensions require equally sized, nonempty station arrays.");
|
||||
}
|
||||
widths = widths.clone();
|
||||
depths = depths.clone();
|
||||
for (int station = 0; station < widths.length; station++) {
|
||||
if (!Double.isFinite(widths[station]) || widths[station] <= 0D
|
||||
|| !Double.isFinite(depths[station]) || depths[station] <= 0D) {
|
||||
throw new IllegalArgumentException("Channel dimensions must be finite and positive.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HydraulicChannelProfile uniform(double width, double depth) {
|
||||
return new HydraulicChannelProfile(new double[]{width}, new double[]{depth});
|
||||
}
|
||||
|
||||
public static HydraulicChannelProfile range(double[] widths, double[] depths, int from, int to) {
|
||||
Objects.checkFromToIndex(from, to, widths.length);
|
||||
Objects.checkFromToIndex(from, to, depths.length);
|
||||
return new HydraulicChannelProfile(Arrays.copyOfRange(widths, from, to), Arrays.copyOfRange(depths, from, to));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] widths() {
|
||||
return widths.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[] depths() {
|
||||
return depths.clone();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return widths.length;
|
||||
}
|
||||
|
||||
public double widthAt(int station) {
|
||||
return widths[widths.length == 1 ? 0 : station];
|
||||
}
|
||||
|
||||
public double depthAt(int station) {
|
||||
return depths[depths.length == 1 ? 0 : station];
|
||||
}
|
||||
|
||||
public double maximumWidth() {
|
||||
double maximum = 0D;
|
||||
for (double width : widths) {
|
||||
maximum = Math.max(maximum, width);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
public double maximumDepth() {
|
||||
double maximum = 0D;
|
||||
for (double depth : depths) {
|
||||
maximum = Math.max(maximum, depth);
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof HydraulicChannelProfile profile
|
||||
&& Arrays.equals(widths, profile.widths)
|
||||
&& Arrays.equals(depths, profile.depths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * Arrays.hashCode(widths) + Arrays.hashCode(depths);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HydraulicChannelProfile[widths=" + Arrays.toString(widths) + ", depths=" + Arrays.toString(depths) + "]";
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydraulicSegment(
|
||||
long id,
|
||||
long courseId,
|
||||
HydrologyFeatureType type,
|
||||
int upstreamHeadY,
|
||||
int downstreamHeadY,
|
||||
int width,
|
||||
int depth,
|
||||
boolean fallingFluid,
|
||||
boolean receivingPool,
|
||||
List<HydrologyPoint> centerline,
|
||||
HydraulicChannelProfile channelProfile
|
||||
) {
|
||||
public HydraulicSegment {
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(centerline, "centerline");
|
||||
Objects.requireNonNull(channelProfile, "channelProfile");
|
||||
centerline = List.copyOf(centerline);
|
||||
if (centerline.isEmpty()) {
|
||||
throw new IllegalArgumentException("A hydraulic segment requires a centerline.");
|
||||
}
|
||||
if (upstreamHeadY < downstreamHeadY) {
|
||||
throw new IllegalArgumentException("Hydraulic heads cannot rise downstream.");
|
||||
}
|
||||
if (width <= 0 || depth <= 0) {
|
||||
throw new IllegalArgumentException("Hydraulic segment width and depth must be positive.");
|
||||
}
|
||||
if (channelProfile.size() != 1 && channelProfile.size() != centerline.size()) {
|
||||
throw new IllegalArgumentException("Channel dimensions must cover every centerline station.");
|
||||
}
|
||||
if (channelProfile.maximumWidth() > width || channelProfile.maximumDepth() > depth) {
|
||||
throw new IllegalArgumentException("Hydraulic segment bounds must contain its channel dimensions.");
|
||||
}
|
||||
int drop = upstreamHeadY - downstreamHeadY;
|
||||
if (fallingFluid && drop == 0) {
|
||||
throw new IllegalArgumentException("Falling fluid requires a positive head transition.");
|
||||
}
|
||||
if (drop > 0 && !type.isDrop()) {
|
||||
throw new IllegalArgumentException("A positive head transition requires a drop segment type.");
|
||||
}
|
||||
if (drop == 0 && type.isDrop()) {
|
||||
throw new IllegalArgumentException("A drop segment type requires a positive head transition.");
|
||||
}
|
||||
if (drop > 0 && !fallingFluid) {
|
||||
if (centerline.getFirst().y() != upstreamHeadY || centerline.getLast().y() != downstreamHeadY) {
|
||||
throw new IllegalArgumentException("A graded head transition must span its declared heads.");
|
||||
}
|
||||
int previousHead = upstreamHeadY;
|
||||
for (HydrologyPoint point : centerline) {
|
||||
if (point.y() > previousHead || point.y() < downstreamHeadY) {
|
||||
throw new IllegalArgumentException("A graded head transition must descend monotonically.");
|
||||
}
|
||||
previousHead = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int drop() {
|
||||
return upstreamHeadY - downstreamHeadY;
|
||||
}
|
||||
|
||||
public HydrologyPoint start() {
|
||||
return centerline.getFirst();
|
||||
}
|
||||
|
||||
public HydrologyPoint end() {
|
||||
return centerline.getLast();
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
final class HydrologyBasisCache {
|
||||
private static final int MAXIMUM_ENTRIES = 32_768;
|
||||
private static final int MAXIMUM_STRIPES = 64;
|
||||
private final Stripe[] full;
|
||||
private final Stripe[] withoutSlope;
|
||||
|
||||
HydrologyBasisCache() {
|
||||
this(MAXIMUM_ENTRIES);
|
||||
}
|
||||
|
||||
HydrologyBasisCache(int maximumEntries) {
|
||||
if (maximumEntries < 1 || Integer.bitCount(maximumEntries) != 1) {
|
||||
throw new IllegalArgumentException("Terrain basis cache size must be a positive power of two.");
|
||||
}
|
||||
int stripeCount = Math.min(MAXIMUM_STRIPES, maximumEntries);
|
||||
full = new Stripe[stripeCount];
|
||||
withoutSlope = new Stripe[stripeCount];
|
||||
for (int index = 0; index < stripeCount; index++) {
|
||||
full[index] = new Stripe(maximumEntries / stripeCount);
|
||||
withoutSlope[index] = new Stripe(maximumEntries / stripeCount);
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
||||
return sample(x, z, sampler, false);
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleWithoutSlope(int x, int z, HydrologyNaturalTerrainSampler sampler) {
|
||||
return sample(x, z, sampler, true);
|
||||
}
|
||||
|
||||
void seed(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
seed(samples, full);
|
||||
}
|
||||
|
||||
HydrologyTerrainSample[] sampleWithoutSlopeBatch(long[] coordinates, int count,
|
||||
HydrologyNaturalTerrainSampler sampler) {
|
||||
Objects.checkFromIndexSize(0, count, coordinates.length);
|
||||
if (count > HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE) {
|
||||
throw new IllegalArgumentException("Terrain basis batch exceeds its sample limit");
|
||||
}
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
Pending[] pending = new Pending[count];
|
||||
long[] ownedCoordinates = new long[count];
|
||||
Pending[] owned = new Pending[count];
|
||||
int ownedCount = count;
|
||||
Completion completion = new Completion();
|
||||
int[] next = new int[count];
|
||||
int[] heads = groupByStripe(coordinates, count, next);
|
||||
try {
|
||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
||||
if (heads[stripeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
Stripe stripe = withoutSlope[stripeIndex];
|
||||
synchronized (stripe) {
|
||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
||||
int index = position - 1;
|
||||
long packed = coordinates[index];
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
samples[index] = cached;
|
||||
continue;
|
||||
}
|
||||
Pending loading = stripe.pending.get(packed);
|
||||
if (loading == null) {
|
||||
loading = new Pending(completion);
|
||||
stripe.pending.put(packed, loading);
|
||||
ownedCoordinates[index] = packed;
|
||||
owned[index] = loading;
|
||||
}
|
||||
pending[index] = loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
int claimed = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (owned[index] != null) {
|
||||
ownedCoordinates[claimed] = ownedCoordinates[index];
|
||||
owned[claimed++] = owned[index];
|
||||
}
|
||||
}
|
||||
ownedCount = claimed;
|
||||
if (ownedCount > 0) {
|
||||
loadBatch(ownedCoordinates, owned, ownedCount, sampler);
|
||||
}
|
||||
} catch (RuntimeException | Error failure) {
|
||||
for (int index = 0; index < ownedCount; index++) {
|
||||
if (owned[index] != null) {
|
||||
fail(withoutSlope, ownedCoordinates[index], owned[index], failure);
|
||||
}
|
||||
}
|
||||
completion.failure = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
completion.complete.countDown();
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (pending[index] != null) {
|
||||
samples[index] = pending[index].await();
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
void seedWithoutSlope(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
seed(samples, withoutSlope);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
Stripe[] stripes = omitSlope ? withoutSlope : full;
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
Pending loading;
|
||||
boolean owner = false;
|
||||
synchronized (stripe) {
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
loading = stripe.pending.get(packed);
|
||||
if (loading == null) {
|
||||
loading = new Pending(new Completion());
|
||||
stripe.pending.put(packed, loading);
|
||||
owner = true;
|
||||
}
|
||||
}
|
||||
if (owner) {
|
||||
try {
|
||||
HydrologyTerrainSample sampled = ProvisionalSampling.memoizable(() -> omitSlope
|
||||
? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z));
|
||||
complete(stripes, packed, loading, sampled, false);
|
||||
} catch (ProvisionalSampling.Unmemoizable provisional) {
|
||||
complete(stripes, packed, loading, provisional.value(), true);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
fail(stripes, packed, loading, failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
loading.completion.complete.countDown();
|
||||
}
|
||||
}
|
||||
return loading.await();
|
||||
}
|
||||
|
||||
private void loadBatch(long[] coordinates, Pending[] pending, int count, HydrologyNaturalTerrainSampler sampler) {
|
||||
HydrologyTerrainSample[] samples;
|
||||
boolean provisional = false;
|
||||
try {
|
||||
samples = ProvisionalSampling.memoizable(() -> sampler.sampleBasisWithoutSlopeBatch(coordinates, count));
|
||||
} catch (ProvisionalSampling.Unmemoizable value) {
|
||||
samples = value.value();
|
||||
provisional = true;
|
||||
}
|
||||
if (samples.length < count) {
|
||||
throw new IllegalStateException("Terrain basis batch returned too few samples");
|
||||
}
|
||||
int[] next = new int[count];
|
||||
int[] heads = groupByStripe(coordinates, count, next);
|
||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
||||
if (heads[stripeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
Stripe stripe = withoutSlope[stripeIndex];
|
||||
synchronized (stripe) {
|
||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
||||
int index = position - 1;
|
||||
complete(stripe, coordinates[index], pending[index], samples[index], provisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int[] groupByStripe(long[] coordinates, int count, int[] next) {
|
||||
int[] heads = new int[withoutSlope.length];
|
||||
for (int index = count - 1; index >= 0; index--) {
|
||||
int stripeIndex = (int) CacheKey.mix(coordinates[index]) & (withoutSlope.length - 1);
|
||||
next[index] = heads[stripeIndex];
|
||||
heads[stripeIndex] = index + 1;
|
||||
}
|
||||
return heads;
|
||||
}
|
||||
|
||||
private void complete(Stripe[] stripes, long packed, Pending pending, HydrologyTerrainSample sample,
|
||||
boolean provisional) {
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
synchronized (stripe) {
|
||||
complete(stripe, packed, pending, sample, provisional);
|
||||
}
|
||||
}
|
||||
|
||||
private void complete(Stripe stripe, long packed, Pending pending, HydrologyTerrainSample sample,
|
||||
boolean provisional) {
|
||||
if (stripe.samples.containsKey(packed)) {
|
||||
sample = stripe.samples.getAndMoveToFirst(packed);
|
||||
provisional = false;
|
||||
} else if (!provisional) {
|
||||
stripe.put(packed, sample);
|
||||
}
|
||||
stripe.pending.remove(packed);
|
||||
pending.sample = sample;
|
||||
pending.provisional = provisional;
|
||||
}
|
||||
|
||||
private void fail(Stripe[] stripes, long packed, Pending pending, Throwable failure) {
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
synchronized (stripe) {
|
||||
if (stripe.pending.remove(packed, pending)) {
|
||||
pending.completion.failure = failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void seed(Long2ObjectMap<HydrologyTerrainSample> samples, Stripe[] stripes) {
|
||||
for (Long2ObjectMap.Entry<HydrologyTerrainSample> entry : samples.long2ObjectEntrySet()) {
|
||||
Stripe stripe = stripe(stripes, entry.getLongKey());
|
||||
synchronized (stripe) {
|
||||
stripe.put(entry.getLongKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stripe stripe(Stripe[] stripes, long packed) {
|
||||
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
|
||||
}
|
||||
|
||||
private static final class Stripe {
|
||||
private final int maximumEntries;
|
||||
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
||||
private final Long2ObjectOpenHashMap<Pending> pending = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
private Stripe(int maximumEntries) {
|
||||
this.maximumEntries = maximumEntries;
|
||||
samples = new Long2ObjectLinkedOpenHashMap<>(Math.min(16, maximumEntries));
|
||||
}
|
||||
|
||||
private void put(long packed, HydrologyTerrainSample value) {
|
||||
if (samples.size() >= maximumEntries && !samples.containsKey(packed)) {
|
||||
samples.removeLast();
|
||||
}
|
||||
samples.putAndMoveToFirst(packed, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
private final Completion completion;
|
||||
private HydrologyTerrainSample sample;
|
||||
private boolean provisional;
|
||||
|
||||
private Pending(Completion completion) {
|
||||
this.completion = completion;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample await() {
|
||||
completion.await();
|
||||
if (provisional) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Completion implements ForkJoinPool.ManagedBlocker {
|
||||
private final Thread owner = Thread.currentThread();
|
||||
private final CountDownLatch complete = new CountDownLatch(1);
|
||||
private Throwable failure;
|
||||
|
||||
private void await() {
|
||||
if (!isReleasable() && owner == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Recursive terrain basis sampling");
|
||||
}
|
||||
boolean interrupted = false;
|
||||
boolean managed = true;
|
||||
try {
|
||||
while (!isReleasable()) {
|
||||
try {
|
||||
if (managed) {
|
||||
ForkJoinPool.managedBlock(this);
|
||||
} else {
|
||||
complete.await();
|
||||
}
|
||||
} catch (InterruptedException interruption) {
|
||||
interrupted = true;
|
||||
managed = false;
|
||||
} catch (RejectedExecutionException unavailable) {
|
||||
managed = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() throws InterruptedException {
|
||||
complete.await();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
return complete.getCount() == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
final class HydrologyCacheBudget {
|
||||
private static final long MEBIBYTE = 1024L * 1024L;
|
||||
private static final HydrologyCacheBudget REGIONAL_RUNTIME = forHeap(Runtime.getRuntime().maxMemory(), true);
|
||||
private static final HydrologyCacheBudget LOCAL_RUNTIME = forHeap(Runtime.getRuntime().maxMemory(), false);
|
||||
private final long plannerBytes;
|
||||
private final long publicationBytes;
|
||||
private final boolean regionalEnabled;
|
||||
|
||||
private HydrologyCacheBudget(long retainedBytes, boolean regionalEnabled) {
|
||||
plannerBytes = retainedBytes / 2L;
|
||||
publicationBytes = retainedBytes - plannerBytes;
|
||||
this.regionalEnabled = regionalEnabled;
|
||||
}
|
||||
|
||||
static HydrologyCacheBudget runtime() {
|
||||
return REGIONAL_RUNTIME;
|
||||
}
|
||||
|
||||
static HydrologyCacheBudget runtime(boolean regionalEnabled) {
|
||||
return regionalEnabled ? REGIONAL_RUNTIME : LOCAL_RUNTIME;
|
||||
}
|
||||
|
||||
static HydrologyCacheBudget forHeap(long maximumHeapBytes, boolean regionalEnabled) {
|
||||
if (maximumHeapBytes <= 0L) {
|
||||
throw new IllegalArgumentException("Maximum heap size must be positive.");
|
||||
}
|
||||
return new HydrologyCacheBudget(Math.clamp(maximumHeapBytes / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE), regionalEnabled);
|
||||
}
|
||||
|
||||
static long sharedStudioBytes() {
|
||||
return Math.clamp(Runtime.getRuntime().maxMemory() / 128L, MEBIBYTE, 16L * MEBIBYTE);
|
||||
}
|
||||
|
||||
long tileBytes() {
|
||||
return publicationBytes * 3L / 4L;
|
||||
}
|
||||
|
||||
long columnBytes() {
|
||||
return publicationBytes / 8L;
|
||||
}
|
||||
|
||||
long diagnosticBytes() {
|
||||
return publicationBytes / 8L;
|
||||
}
|
||||
|
||||
long routingBytes() {
|
||||
return plannerBytes * 3L / 16L;
|
||||
}
|
||||
|
||||
long ownerBytes() {
|
||||
return regionalEnabled ? plannerBytes / 4L : plannerBytes * 3L / 4L;
|
||||
}
|
||||
|
||||
long edgeBytes() {
|
||||
return plannerBytes / 16L;
|
||||
}
|
||||
|
||||
long regionalDraftBytes() {
|
||||
return regionalEnabled ? plannerBytes / 8L : 0L;
|
||||
}
|
||||
|
||||
long regionalCoarseBytes() {
|
||||
return regionalEnabled ? plannerBytes / 8L : 0L;
|
||||
}
|
||||
|
||||
long regionalTerrainBytes() {
|
||||
return regionalEnabled ? plannerBytes / 4L : 0L;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyCacheWeights {
|
||||
private static final long ENTRY_BYTES = 192L;
|
||||
private static final long TERRAIN_BYTES = 256L;
|
||||
private static final long POINT_BYTES = 40L;
|
||||
|
||||
private HydrologyCacheWeights() {
|
||||
}
|
||||
|
||||
static int bounded(long estimatedBytes, long budget, int maximumEntries) {
|
||||
return saturated(Math.max(estimatedBytes, Math.ceilDiv(budget, maximumEntries)));
|
||||
}
|
||||
|
||||
static int bounded(long estimatedBytes, long budget, int maximumEntries, int units, int maximumUnits) {
|
||||
return bounded(Math.max(estimatedBytes, Math.ceilDiv((long) units * budget, maximumUnits)), budget, maximumEntries);
|
||||
}
|
||||
|
||||
static int tile(HydrologyTile tile) {
|
||||
long bytes = network(tile.nodes(), tile.edges(), tile.outlets(), tile.courses(), tile.cavePlans());
|
||||
bytes += diagnostics(tile.localDiagnosticCandidates());
|
||||
bytes += (long) tile.regionalCourseIds().size() * 64L;
|
||||
if (tile.resolvedOwner() != null) {
|
||||
bytes += owner(tile.resolvedOwner());
|
||||
}
|
||||
RiverFootprint footprint = tile.footprint();
|
||||
if (footprint != null) {
|
||||
bytes += (long) footprint.size() * 64L;
|
||||
for (HydrologyColumnSample sample : footprint.columns().values()) {
|
||||
bytes += column(sample);
|
||||
}
|
||||
}
|
||||
return saturated(bytes);
|
||||
}
|
||||
|
||||
static int column(HydrologyColumnSample sample) {
|
||||
return saturated(96L + (long) sample.layers().size() * 192L);
|
||||
}
|
||||
|
||||
static int diagnostics(List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
return saturated(ENTRY_BYTES + (long) diagnostics.size() * 112L);
|
||||
}
|
||||
|
||||
static int routing(SourceRoutingContext context) {
|
||||
return saturated(ENTRY_BYTES + grid(context.grid()) + routingPlan(context.surfaceRouting())
|
||||
+ routingPlan(context.undergroundRouting()) + diagnostics(context.diagnostics()));
|
||||
}
|
||||
|
||||
static int owner(CrossTileResolvedOwner owner) {
|
||||
HydrologyCaveCourseFilter.Result result = owner.draft().result();
|
||||
long bytes = network(result.nodes(), result.edges(), result.outlets(), result.courses(), result.cavePlans())
|
||||
+ diagnostics(owner.draft().diagnostics());
|
||||
for (CrossTileRejectedCourse rejection : owner.observedRejections()) {
|
||||
bytes += 48L + course(rejection.course());
|
||||
}
|
||||
return saturated(bytes);
|
||||
}
|
||||
|
||||
static int points(List<HydrologyPoint> points) {
|
||||
return saturated(ENTRY_BYTES + (long) points.size() * POINT_BYTES);
|
||||
}
|
||||
|
||||
static int regionalNetwork(HydrologyRegionalNetwork network) {
|
||||
return saturated(network(network.nodes(), network.edges(), network.outlets(), network.courses(), network.cavePlans())
|
||||
+ diagnostics(network.diagnostics()));
|
||||
}
|
||||
|
||||
static int regionalCoarse(HydrologyRegionalCoarsePlan plan) {
|
||||
HydrologyRegionalGraph.Tree tree = plan.tree();
|
||||
long treeBytes = tree == null ? 0L : 96L + (long) tree.selected().length * 8L
|
||||
+ ((long) tree.accumulation().length + tree.heads().length) * 4L + tree.retainedLabelCount() * 96L;
|
||||
return saturated(ENTRY_BYTES + grid(plan.grid()) + treeBytes + (long) plan.outlets().size() * 224L
|
||||
+ (long) plan.sources().size() * 32L + (long) plan.envelopes().size() * 64L);
|
||||
}
|
||||
|
||||
static int terrain(HydrologyTerrainSample terrain) {
|
||||
return saturated(128L + (terrain == null ? 0L : TERRAIN_BYTES));
|
||||
}
|
||||
|
||||
private static long grid(HydrologySampledGrid grid) {
|
||||
return grid == null ? 0L : 96L + (long) grid.nodes().size() * (64L + TERRAIN_BYTES + 104L);
|
||||
}
|
||||
|
||||
private static long routingPlan(HydrologyRoutingPlan plan) {
|
||||
if (plan == null) {
|
||||
return 0L;
|
||||
}
|
||||
return 128L + (long) plan.potential().length * 8L
|
||||
+ ((long) plan.parent().length + plan.outletIndex().length + plan.routeLengths().length) * 4L
|
||||
+ (long) plan.outlets().size() * 224L
|
||||
+ (plan.surfaceDrainage() == null ? 0L : plan.surfaceDrainage().maximumRetainedBytes());
|
||||
}
|
||||
|
||||
private static long network(List<DrainageNode> nodes, List<DrainageEdge> edges, List<RiverOutlet> outlets,
|
||||
List<RiverCourse> courses, List<HydrologyCavePlan> cavePlans) {
|
||||
long bytes = ENTRY_BYTES + (long) nodes.size() * (96L + TERRAIN_BYTES) + (long) outlets.size() * 224L;
|
||||
for (DrainageEdge edge : edges) {
|
||||
bytes += 96L + points(edge.centerline());
|
||||
}
|
||||
for (RiverCourse course : courses) {
|
||||
bytes += course(course);
|
||||
}
|
||||
for (HydrologyCavePlan plan : cavePlans) {
|
||||
bytes += plan.estimatedRetainedBytes();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static long course(RiverCourse course) {
|
||||
long bytes = 128L;
|
||||
for (DrainageEdge edge : course.drainageEdges()) {
|
||||
bytes += 96L + points(edge.centerline());
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
bytes += 128L + points(segment.centerline()) + (long) segment.channelProfile().size() * 16L;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static int saturated(long bytes) {
|
||||
return (int) Math.clamp(bytes, 1L, Integer.MAX_VALUE);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public enum HydrologyCandidateKind {
|
||||
SOURCE,
|
||||
OUTLET,
|
||||
DEEP_FLUID,
|
||||
POOL,
|
||||
/** A later course to an outlet that could not be joined to the outlet's main course. */
|
||||
TRIBUTARY,
|
||||
REGIONAL_SOURCE,
|
||||
COASTAL_CHANNEL
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public enum HydrologyCandidateRejection {
|
||||
NO_LEGAL_OUTLET,
|
||||
NO_DRAINAGE_PATH,
|
||||
CONFINED_NO_OUTLET,
|
||||
ROUTE_LIMIT,
|
||||
COURSE_TOO_SHORT,
|
||||
SOURCE_QUOTA,
|
||||
SOURCE_SPACING,
|
||||
OUTLET_LIMIT,
|
||||
OUTLET_LEVEL,
|
||||
CAVE_CONTAINMENT,
|
||||
VOLUME_LIMIT,
|
||||
SURFACE_HEAD_RANGE,
|
||||
SURFACE_HEAD_DISTRIBUTION,
|
||||
SURFACE_EXPOSURE,
|
||||
SURFACE_DROP_UNSUPPORTED,
|
||||
SURFACE_CORRIDOR_UNSUPPORTED,
|
||||
SURFACE_TRANSITION_UNSUPPORTED,
|
||||
SURFACE_SEGMENT_UNSUPPORTED,
|
||||
SURFACE_SINKHOLE_CLEARANCE,
|
||||
SURFACE_SHAPE_UNSUPPORTED,
|
||||
SURFACE_MOUTH_DISCONNECTED,
|
||||
SURFACE_WATER_CONTAINMENT,
|
||||
SURFACE_BANK_BUDGET,
|
||||
POLICY_EXCLUDED;
|
||||
}
|
||||
-1284
File diff suppressed because it is too large.
Load diff
-14
@@ -1,14 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyCaveVoxelViewFactory {
|
||||
CaveVoxelView create(PlannedSurface plannedSurface);
|
||||
|
||||
interface PlannedSurface {
|
||||
int resolve(int x, int z, int naturalHeight);
|
||||
|
||||
boolean ownsTerrain(int x, int z);
|
||||
}
|
||||
}
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceNoise;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceInletRange;
|
||||
|
||||
final class HydrologyChannelGeometry {
|
||||
private final HydrologyFootprintCompiler compiler;
|
||||
private RiverCourse inletCourse;
|
||||
private SurfaceInletRange inletRange;
|
||||
|
||||
HydrologyChannelGeometry(HydrologyFootprintCompiler compiler) {
|
||||
this.compiler = compiler;
|
||||
}
|
||||
|
||||
static final long ORGANIC_SHAPE_FIRST_PHASE_SALT = 0x4f5247414e31L;
|
||||
static final long ORGANIC_SHAPE_SECOND_PHASE_SALT = 0x4f5247414e32L;
|
||||
static final long ORGANIC_BED_VARIATION_SALT = 0x424544564152L;
|
||||
static final long ORGANIC_CEILING_VARIATION_SALT = 0x4345494c564152L;
|
||||
|
||||
double smoothStep(double progress) {
|
||||
return progress * progress * (3D - 2D * progress);
|
||||
}
|
||||
|
||||
FootprintLayerShape shape(
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
HydrologyPoint point,
|
||||
int bed,
|
||||
int fluidHead,
|
||||
boolean falling,
|
||||
boolean receiving
|
||||
) {
|
||||
HydrologyFeatureType type = segment.type();
|
||||
int channelRadius = Math.max(1, segment.width() / 2);
|
||||
double shoreWidth = 0D;
|
||||
double gradingWidth = 0D;
|
||||
int ceiling = fluidHead;
|
||||
boolean ellipsoid = false;
|
||||
boolean archedChannel = false;
|
||||
boolean roundedSurfaceBed = false;
|
||||
boolean organicBoundary = type.isSurface() || type.isUnderground() || type.isDeepFluid() || falling || receiving;
|
||||
if (course.type() == RiverCourseType.SURFACE && type.isSurface()) {
|
||||
roundedSurfaceBed = !falling;
|
||||
shoreWidth = compiler.settings.surface().shoreWidth();
|
||||
gradingWidth = compiler.geometrySampler.sample(
|
||||
HydrologyGeometrySampler.Field.SURFACE_BLEND_WIDTH,
|
||||
course.profileKey(),
|
||||
point.x(),
|
||||
point.z(),
|
||||
segment.id(),
|
||||
compiler.settings.surface().banks().minimumBlendWidth(),
|
||||
compiler.settings.surface().banks().maximumBlendWidth()
|
||||
);
|
||||
if (type == HydrologyFeatureType.WATERFALL) {
|
||||
// A plunge has no floodplain: the shore bench is cut level with the bank top, so a wide one
|
||||
// terraces the cliff the water falls down. Deliberate saturation, paired with the grading cap
|
||||
// below - shoreWidth stops tracking the knob past 2.5.
|
||||
shoreWidth = Math.min(2.5D, shoreWidth);
|
||||
gradingWidth = Math.min(gradingWidth, Math.max(8D, channelRadius * 4D));
|
||||
}
|
||||
}
|
||||
if (type == HydrologyFeatureType.UNDERGROUND_POOL
|
||||
|| type == HydrologyFeatureType.UNDERGROUND_DROP
|
||||
|| type == HydrologyFeatureType.SINKHOLE) {
|
||||
ceiling = fluidHead + compiler.geometrySampler.sample(
|
||||
HydrologyGeometrySampler.Field.UNDERGROUND_HEADROOM,
|
||||
course.profileKey(),
|
||||
point.x(),
|
||||
point.z(),
|
||||
course.id(),
|
||||
compiler.settings.underground().minimumHeadroom(),
|
||||
compiler.settings.underground().maximumHeadroom()
|
||||
);
|
||||
archedChannel = !falling;
|
||||
} else if (type == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
HydrologyPlannerSettings.Grotto grotto = compiler.settings.outlets().coastalGrotto();
|
||||
channelRadius = grotto.horizontalRadius();
|
||||
bed = fluidHead - grotto.verticalRadius();
|
||||
ceiling = fluidHead + grotto.headroom();
|
||||
ellipsoid = true;
|
||||
organicBoundary = true;
|
||||
} else if (type == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
HydrologyPlannerSettings.Grotto grotto = compiler.settings.outlets().inlandGrotto();
|
||||
channelRadius = grotto.horizontalRadius();
|
||||
bed = fluidHead - grotto.verticalRadius();
|
||||
ceiling = fluidHead + grotto.headroom();
|
||||
ellipsoid = true;
|
||||
organicBoundary = true;
|
||||
} else if (type.isDeepFluid()) {
|
||||
HydrologyPlannerSettings.DeepFluid deepFluid = compiler.deepFluids.get(course.profileKey());
|
||||
if (deepFluid == null) {
|
||||
throw new IllegalStateException("Missing deep-fluid planner settings for " + course.profileKey() + ".");
|
||||
}
|
||||
if (type == HydrologyFeatureType.DEEP_POOL) {
|
||||
channelRadius = Math.max(1, segment.width() / 2);
|
||||
bed = fluidHead - Math.max(segment.depth(), deepFluid.minimumVerticalRadius());
|
||||
ellipsoid = true;
|
||||
} else {
|
||||
archedChannel = !falling;
|
||||
}
|
||||
ceiling = fluidHead + deepFluid.headroom();
|
||||
organicBoundary = true;
|
||||
}
|
||||
if (receiving) {
|
||||
HydrologyPlannerSettings.Drops drops = compiler.settings.geometry().drops();
|
||||
if (course.type() != RiverCourseType.SURFACE || !type.isSurface()) {
|
||||
channelRadius = Math.max(
|
||||
channelRadius,
|
||||
(int) StrictMath.ceil(drops.basinWidth(segment.width()) / 2D)
|
||||
);
|
||||
}
|
||||
bed = Math.min(bed, fluidHead - drops.basinDepth(segment.depth(), segment.drop()));
|
||||
if (course.type() == RiverCourseType.SURFACE && type.isSurface()) {
|
||||
gradingWidth = Math.max(
|
||||
gradingWidth,
|
||||
Math.min(compiler.settings.surface().banks().maximumBlendWidth(), channelRadius * 2D)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (type == HydrologyFeatureType.UNDERGROUND_POOL && segment.centerline().size() > 1) {
|
||||
int depthVariation = Math.min(
|
||||
2,
|
||||
Math.max(0, compiler.settings.underground().maximumDepth() - (fluidHead - bed))
|
||||
);
|
||||
bed -= organicVerticalVariation(
|
||||
HydrologyHash.mix(segment.id(), ORGANIC_BED_VARIATION_SALT),
|
||||
point.x(),
|
||||
point.z(),
|
||||
depthVariation
|
||||
);
|
||||
}
|
||||
if (falling) {
|
||||
bed = Math.min(bed, segment.downstreamHeadY() - 1);
|
||||
ceiling = Math.max(ceiling, segment.upstreamHeadY());
|
||||
}
|
||||
return new FootprintLayerShape(
|
||||
channelRadius,
|
||||
shoreWidth,
|
||||
gradingWidth,
|
||||
bed,
|
||||
fluidHead,
|
||||
ceiling,
|
||||
ellipsoid,
|
||||
archedChannel,
|
||||
roundedSurfaceBed,
|
||||
organicBoundary,
|
||||
falling,
|
||||
inletStation(course, segment, point)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean inletStation(RiverCourse course, HydraulicSegment segment, HydrologyPoint point) {
|
||||
if (course.type() != RiverCourseType.SURFACE || !segment.type().isSurface() || !segment.fallingFluid()) {
|
||||
return false;
|
||||
}
|
||||
if (inletCourse != course) {
|
||||
HydrologyTerrainSampler receiving = HydrologyOceanReceiver.forCourse(
|
||||
compiler.settings, compiler::sampleTerrainBasis, course);
|
||||
inletRange = SurfaceInletRange.forCourse(compiler.settings, course, receiving);
|
||||
inletCourse = course;
|
||||
}
|
||||
return inletRange.allowsFallingStation(segment.id(), point.x(), point.z());
|
||||
}
|
||||
|
||||
int organicVerticalVariation(
|
||||
long seed,
|
||||
int x,
|
||||
int z,
|
||||
int maximum
|
||||
) {
|
||||
if (maximum == 0) {
|
||||
return 0;
|
||||
}
|
||||
double sampled = SurfaceNoise.value(seed, x, z, 12);
|
||||
return (int) StrictMath.round(sampled * maximum);
|
||||
}
|
||||
|
||||
double ellipsoidScale(int radius, double distance, double roundness) {
|
||||
double normalized = Math.min(1D, distance / Math.max(1D, radius));
|
||||
return StrictMath.pow(
|
||||
Math.max(0D, 1D - StrictMath.pow(normalized, roundness)),
|
||||
1D / roundness
|
||||
);
|
||||
}
|
||||
|
||||
double shapedDistance(
|
||||
FootprintLayerShape shape,
|
||||
HydraulicSegment segment,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
double deltaX,
|
||||
double deltaZ,
|
||||
int flowX,
|
||||
int flowZ,
|
||||
double rawDistance
|
||||
) {
|
||||
if (segment.type() == HydrologyFeatureType.DEEP_POOL) {
|
||||
return deepPoolDistance(shape, segment, worldX, worldZ, deltaX, deltaZ);
|
||||
}
|
||||
if (segment.type().isSurface() && !shape.fallingThroat()) {
|
||||
return surfaceChannelDistance(shape, segment, worldX, worldZ, deltaX, deltaZ, flowX, flowZ);
|
||||
}
|
||||
if (rawDistance == 0D) {
|
||||
return 0D;
|
||||
}
|
||||
double distance = rawDistance;
|
||||
if (shape.fallingThroat() && (flowX != 0 || flowZ != 0)) {
|
||||
double flowLength = StrictMath.hypot(flowX, flowZ);
|
||||
double along = (deltaX * flowX + deltaZ * flowZ) / flowLength;
|
||||
double across = StrictMath.abs(deltaX * -flowZ + deltaZ * flowX) / flowLength;
|
||||
if (StrictMath.abs(along) > 0.75D || segment.type().isSurface() && along < 0D) {
|
||||
return shape.totalRadius() + 1D;
|
||||
}
|
||||
distance = across;
|
||||
}
|
||||
if (!shape.organicBoundary()) {
|
||||
return distance;
|
||||
}
|
||||
HydrologyPlannerSettings.ChannelShape channelShape = channelShape(segment.type());
|
||||
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO
|
||||
|| segment.type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
double orientation = HydrologyHash.unit(HydrologyHash.mix(
|
||||
segment.courseId(),
|
||||
ORGANIC_SHAPE_FIRST_PHASE_SALT
|
||||
)) * StrictMath.PI * 2D;
|
||||
double cosine = StrictMath.cos(orientation);
|
||||
double sine = StrictMath.sin(orientation);
|
||||
double rotatedX = deltaX * cosine + deltaZ * sine;
|
||||
double rotatedZ = -deltaX * sine + deltaZ * cosine;
|
||||
double aspect = channelShape.aspectMinimum() + HydrologyHash.unit(HydrologyHash.mix(
|
||||
segment.courseId(),
|
||||
ORGANIC_SHAPE_SECOND_PHASE_SALT
|
||||
)) * channelShape.aspectRange();
|
||||
distance = StrictMath.hypot(rotatedX, rotatedZ / aspect);
|
||||
}
|
||||
double angle = StrictMath.atan2(deltaZ, deltaX);
|
||||
double firstPhase = HydrologyHash.unit(HydrologyHash.mix(
|
||||
segment.courseId(),
|
||||
ORGANIC_SHAPE_FIRST_PHASE_SALT
|
||||
)) * StrictMath.PI * 2D;
|
||||
double secondPhase = HydrologyHash.unit(HydrologyHash.mix(
|
||||
segment.courseId(),
|
||||
ORGANIC_SHAPE_SECOND_PHASE_SALT
|
||||
)) * StrictMath.PI * 2D;
|
||||
double firstLobe = 0.5D + 0.5D * StrictMath.sin(angle * 3D + firstPhase);
|
||||
double secondLobe = 0.5D + 0.5D * StrictMath.sin(angle * 5D + secondPhase);
|
||||
double coherent = SurfaceNoise.signed(
|
||||
segment.courseId(),
|
||||
worldX,
|
||||
worldZ,
|
||||
channelShape.roughnessWavelength()
|
||||
);
|
||||
double detail = SurfaceNoise.signed(
|
||||
HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT),
|
||||
worldX,
|
||||
worldZ,
|
||||
Math.max(3, channelShape.roughnessWavelength() / 2)
|
||||
);
|
||||
double radialScale = channelShape.radialBase()
|
||||
+ (firstLobe - 0.5D) * channelShape.primaryLobeStrength()
|
||||
+ (secondLobe - 0.5D) * channelShape.detailLobeStrength()
|
||||
+ coherent * channelShape.wallRoughness() * 0.7D
|
||||
+ detail * channelShape.wallRoughness() * 0.3D;
|
||||
radialScale = Math.max(channelShape.radialMinimum(), Math.min(channelShape.radialMaximum(), radialScale));
|
||||
if (segment.type().isSurface() && shape.fallingThroat()) {
|
||||
radialScale = Math.max(1D, radialScale);
|
||||
}
|
||||
return distance / radialScale;
|
||||
}
|
||||
|
||||
double surfaceChannelDistance(
|
||||
FootprintLayerShape shape,
|
||||
HydraulicSegment segment,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
double deltaX,
|
||||
double deltaZ,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
double flowLength = StrictMath.hypot(flowX, flowZ);
|
||||
if (flowLength == 0D) {
|
||||
return deepPoolDistance(shape, segment, worldX, worldZ, deltaX, deltaZ);
|
||||
}
|
||||
double signedCross = (deltaX * -flowZ + deltaZ * flowX) / flowLength;
|
||||
HydrologyPlannerSettings.ChannelShape channelShape = compiler.settings.geometry().surface();
|
||||
double thalweg = SurfaceNoise.signed(
|
||||
HydrologyHash.mix(segment.courseId(), ORGANIC_BED_VARIATION_SALT),
|
||||
worldX,
|
||||
worldZ,
|
||||
channelShape.roughnessWavelength()
|
||||
) * shape.channelRadius() * channelShape.wallRoughness();
|
||||
long bankSeed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_SECOND_PHASE_SALT);
|
||||
double bankNoise = SurfaceNoise.signed(
|
||||
bankSeed,
|
||||
worldX,
|
||||
worldZ,
|
||||
channelShape.roughnessWavelength()
|
||||
);
|
||||
double sideBias = signedCross < thalweg
|
||||
? HydrologyHash.unit(HydrologyHash.mix(bankSeed, 1L))
|
||||
: HydrologyHash.unit(HydrologyHash.mix(bankSeed, 2L));
|
||||
double widthScale = 0.86D
|
||||
+ bankNoise * channelShape.wallRoughness()
|
||||
+ (sideBias - 0.5D) * 0.08D;
|
||||
widthScale = Math.max(1D, Math.min(1.18D, widthScale));
|
||||
double organicDistance = StrictMath.abs(signedCross - thalweg) / widthScale;
|
||||
double along = (deltaX * flowX + deltaZ * flowZ) / flowLength;
|
||||
return StrictMath.hypot(along, Math.min(StrictMath.abs(signedCross), organicDistance));
|
||||
}
|
||||
|
||||
HydrologyPlannerSettings.ChannelShape channelShape(HydrologyFeatureType type) {
|
||||
if (type.isGrotto() || type == HydrologyFeatureType.DEEP_POOL) {
|
||||
return compiler.settings.geometry().grottos();
|
||||
}
|
||||
if (type.isUnderground() || type == HydrologyFeatureType.DEEP_CHANNEL) {
|
||||
return compiler.settings.geometry().underground();
|
||||
}
|
||||
return compiler.settings.geometry().surface();
|
||||
}
|
||||
|
||||
double deepPoolDistance(
|
||||
FootprintLayerShape shape,
|
||||
HydraulicSegment segment,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
double deltaX,
|
||||
double deltaZ
|
||||
) {
|
||||
double radius = Math.max(1D, shape.channelRadius());
|
||||
long seed = HydrologyHash.mix(segment.courseId(), ORGANIC_SHAPE_FIRST_PHASE_SALT);
|
||||
double orientation = HydrologyHash.unit(seed) * StrictMath.PI * 2D;
|
||||
double cosine = StrictMath.cos(orientation);
|
||||
double sine = StrictMath.sin(orientation);
|
||||
double rotatedX = deltaX * cosine + deltaZ * sine;
|
||||
double rotatedZ = -deltaX * sine + deltaZ * cosine;
|
||||
double angle = StrictMath.atan2(rotatedZ, rotatedX);
|
||||
double secondPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 2L)) * StrictMath.PI * 2D;
|
||||
double thirdPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 3L)) * StrictMath.PI * 2D;
|
||||
double fifthPhase = HydrologyHash.unit(HydrologyHash.mix(seed, 5L)) * StrictMath.PI * 2D;
|
||||
double radialScale = 0.84D
|
||||
+ StrictMath.sin(angle * 2D + secondPhase) * 0.07D
|
||||
+ StrictMath.sin(angle * 3D + thirdPhase) * 0.05D
|
||||
+ StrictMath.sin(angle * 5D + fifthPhase) * 0.035D;
|
||||
radialScale = Math.max(0.68D, Math.min(1D, radialScale));
|
||||
double normalizedDistance = StrictMath.hypot(
|
||||
rotatedX / (radius * 0.92D),
|
||||
rotatedZ / (radius * 0.7D)
|
||||
);
|
||||
return normalizedDistance * radius / radialScale;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
/**
|
||||
* The chunks a planned tile publishes at least one footprint column into. A chunk outside every
|
||||
* relevant tile's occupancy composes to no hydrology at all, so it can be answered without the
|
||||
* tiles being resident.
|
||||
*/
|
||||
final class HydrologyChunkOccupancy {
|
||||
static final HydrologyChunkOccupancy EMPTY = new HydrologyChunkOccupancy(0, 0, 0, 0, new long[0]);
|
||||
|
||||
private final int minimumChunkX;
|
||||
private final int minimumChunkZ;
|
||||
private final int width;
|
||||
private final int depth;
|
||||
private final long[] bits;
|
||||
|
||||
private HydrologyChunkOccupancy(int minimumChunkX, int minimumChunkZ, int width, int depth, long[] bits) {
|
||||
this.minimumChunkX = minimumChunkX;
|
||||
this.minimumChunkZ = minimumChunkZ;
|
||||
this.width = width;
|
||||
this.depth = depth;
|
||||
this.bits = bits;
|
||||
}
|
||||
|
||||
static HydrologyChunkOccupancy of(RiverFootprint footprint) {
|
||||
if (footprint == null) {
|
||||
return EMPTY;
|
||||
}
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
int maximumX = Integer.MIN_VALUE;
|
||||
int maximumZ = Integer.MIN_VALUE;
|
||||
for (long packed : footprint.columns().keySet()) {
|
||||
int chunkX = RiverFootprint.unpackX(packed) >> 4;
|
||||
int chunkZ = RiverFootprint.unpackZ(packed) >> 4;
|
||||
minimumX = Math.min(minimumX, chunkX);
|
||||
minimumZ = Math.min(minimumZ, chunkZ);
|
||||
maximumX = Math.max(maximumX, chunkX);
|
||||
maximumZ = Math.max(maximumZ, chunkZ);
|
||||
}
|
||||
if (minimumX > maximumX) {
|
||||
return EMPTY;
|
||||
}
|
||||
int width = maximumX - minimumX + 1;
|
||||
int depth = maximumZ - minimumZ + 1;
|
||||
long[] bits = new long[Math.toIntExact(Math.ceilDiv(Math.multiplyExact((long) width, depth), 64L))];
|
||||
for (long packed : footprint.columns().keySet()) {
|
||||
int index = ((RiverFootprint.unpackZ(packed) >> 4) - minimumZ) * width
|
||||
+ (RiverFootprint.unpackX(packed) >> 4) - minimumX;
|
||||
bits[index >>> 6] |= 1L << index;
|
||||
}
|
||||
return new HydrologyChunkOccupancy(minimumX, minimumZ, width, depth, bits);
|
||||
}
|
||||
|
||||
boolean occupies(int chunkX, int chunkZ) {
|
||||
int localX = chunkX - minimumChunkX;
|
||||
int localZ = chunkZ - minimumChunkZ;
|
||||
if (localX < 0 || localZ < 0 || localX >= width || localZ >= depth) {
|
||||
return false;
|
||||
}
|
||||
int index = localZ * width + localX;
|
||||
return (bits[index >>> 6] & 1L << index) != 0L;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyColumnLayer(
|
||||
HydrologyFeatureRef feature,
|
||||
int bedY,
|
||||
int fluidHeadY,
|
||||
int ceilingY,
|
||||
boolean channel,
|
||||
boolean shore,
|
||||
boolean grading,
|
||||
boolean connectedFluid,
|
||||
boolean fallingFluid,
|
||||
boolean receivingPool,
|
||||
boolean terrainOwned,
|
||||
boolean fluidOwned,
|
||||
boolean oceanApron,
|
||||
String profileKey,
|
||||
String surfaceBiomeKey,
|
||||
String mouthBiomeKey,
|
||||
String shoreBiomeKey,
|
||||
String bankBiomeKey,
|
||||
String floodedCaveBiomeKey
|
||||
) {
|
||||
public HydrologyColumnLayer {
|
||||
Objects.requireNonNull(feature, "feature");
|
||||
profileKey = requireKey(profileKey, "profileKey");
|
||||
surfaceBiomeKey = requireKey(surfaceBiomeKey, "surfaceBiomeKey");
|
||||
mouthBiomeKey = requireKey(mouthBiomeKey, "mouthBiomeKey");
|
||||
shoreBiomeKey = requireKey(shoreBiomeKey, "shoreBiomeKey");
|
||||
bankBiomeKey = requireKey(bankBiomeKey, "bankBiomeKey");
|
||||
floodedCaveBiomeKey = requireKey(floodedCaveBiomeKey, "floodedCaveBiomeKey");
|
||||
if (channel && bedY > fluidHeadY) {
|
||||
throw new IllegalArgumentException("A channel bed cannot be above its fluid head.");
|
||||
}
|
||||
if (ceilingY < fluidHeadY) {
|
||||
throw new IllegalArgumentException("A hydrology carve ceiling cannot be below its fluid head.");
|
||||
}
|
||||
if (!feature.type().isUnderground() && !feature.type().isDeepFluid() && ceilingY != fluidHeadY) {
|
||||
throw new IllegalArgumentException("Surface hydrology ceilings must equal the fluid head.");
|
||||
}
|
||||
if (fluidOwned && !channel) {
|
||||
throw new IllegalArgumentException("Owned fluid requires a channel footprint.");
|
||||
}
|
||||
if (fallingFluid && !connectedFluid) {
|
||||
throw new IllegalArgumentException("Falling fluid must be connected.");
|
||||
}
|
||||
if (receivingPool && !connectedFluid) {
|
||||
throw new IllegalArgumentException("A receiving pool must be connected.");
|
||||
}
|
||||
if (oceanApron && (terrainOwned || fluidOwned || grading || shore)) {
|
||||
throw new IllegalArgumentException("An ocean apron cannot own river writes or grading.");
|
||||
}
|
||||
}
|
||||
|
||||
public String biomeKey() {
|
||||
if (grading && !channel && !shore) {
|
||||
return bankBiomeKey;
|
||||
}
|
||||
if (shore) {
|
||||
return shoreBiomeKey;
|
||||
}
|
||||
if (feature.type() == HydrologyFeatureType.MOUTH || feature.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
return mouthBiomeKey;
|
||||
}
|
||||
if (feature.type().isUnderground() || feature.type().isDeepFluid()) {
|
||||
return floodedCaveBiomeKey;
|
||||
}
|
||||
return surfaceBiomeKey;
|
||||
}
|
||||
|
||||
/** Every owned channel water column is published so steps between stations receive fluid updates. */
|
||||
public boolean publishesSurfaceFluid() {
|
||||
return feature.type().isSurface()
|
||||
&& !oceanApron
|
||||
&& channel
|
||||
&& fluidOwned
|
||||
&& (terrainOwned || fallingFluid)
|
||||
&& fluidHeadY > bedY;
|
||||
}
|
||||
|
||||
private static String requireKey(String value, String name) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(name + " must not be blank.");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record HydrologyColumnSample(
|
||||
int x,
|
||||
int z,
|
||||
int naturalHeight,
|
||||
int seaLevel,
|
||||
boolean ocean,
|
||||
String parentBiomeKey,
|
||||
List<HydrologyColumnLayer> layers
|
||||
) {
|
||||
private static final Comparator<HydrologyColumnLayer> LAYER_ORDER = Comparator
|
||||
.comparingInt((HydrologyColumnLayer layer) -> layer.feature().type().renderPriority())
|
||||
.thenComparing(Comparator.comparingInt(HydrologyColumnLayer::fluidHeadY).reversed())
|
||||
.thenComparingLong((HydrologyColumnLayer layer) -> layer.feature().id());
|
||||
private static final Comparator<HydrologyColumnLayer> SURFACE_LAYER_ORDER = Comparator
|
||||
.comparingInt(HydrologyColumnSample::surfaceRolePriority)
|
||||
.thenComparingInt((HydrologyColumnLayer layer) -> layer.feature().type().renderPriority())
|
||||
.thenComparing(Comparator.comparingInt(HydrologyColumnLayer::fluidHeadY).reversed())
|
||||
.thenComparingInt(HydrologyColumnLayer::bedY)
|
||||
.thenComparingLong((HydrologyColumnLayer layer) -> layer.feature().id());
|
||||
|
||||
public HydrologyColumnSample {
|
||||
if (parentBiomeKey == null || parentBiomeKey.isBlank()) {
|
||||
throw new IllegalArgumentException("parentBiomeKey must not be blank.");
|
||||
}
|
||||
parentBiomeKey = parentBiomeKey.trim();
|
||||
Objects.requireNonNull(layers, "layers");
|
||||
if (layers.size() < 2) {
|
||||
layers = List.copyOf(layers);
|
||||
} else {
|
||||
ArrayList<HydrologyColumnLayer> ordered = new ArrayList<>(layers);
|
||||
ordered.sort(LAYER_ORDER);
|
||||
layers = List.copyOf(ordered);
|
||||
}
|
||||
if (ocean) {
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore()) {
|
||||
throw new IllegalArgumentException("Ocean columns cannot contain river-owned writes or grading.");
|
||||
}
|
||||
if (layer.fluidHeadY() > seaLevel) {
|
||||
throw new IllegalArgumentException("Ocean column fluid cannot be elevated above sea level.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (naturalHeight <= seaLevel) {
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type().isSurface()
|
||||
&& !layer.oceanApron()
|
||||
&& (layer.terrainOwned() || layer.fluidOwned() || layer.grading() || layer.shore())
|
||||
&& (naturalHeight < seaLevel || layer.fluidHeadY() > seaLevel)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Surface hydrology cannot own submerged ground or raise sea-level water."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
return !layers.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFeature(HydrologyFeatureType type) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type() == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasConnectedFluid() {
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.connectedFluid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnLayer> primaryLayer() {
|
||||
return layers.isEmpty() ? Optional.empty() : Optional.of(layers.getFirst());
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnLayer> primarySurfaceLayer() {
|
||||
return Optional.ofNullable(selectSurfaceLayer(false));
|
||||
}
|
||||
|
||||
public Optional<HydrologyColumnLayer> primarySurfaceFluidLayer() {
|
||||
return Optional.ofNullable(selectSurfaceLayer(true));
|
||||
}
|
||||
|
||||
public HydrologyColumnLayer primarySurfaceLayerOrNull() {
|
||||
return selectSurfaceLayer(false);
|
||||
}
|
||||
|
||||
public HydrologyColumnLayer primarySurfaceFluidLayerOrNull() {
|
||||
return selectSurfaceLayer(true);
|
||||
}
|
||||
|
||||
public Optional<SurfacePublicationCell> surfacePublicationCellAt(int y) {
|
||||
if (y <= terrainHeight()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
SurfacePublicationCell selected = null;
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (!layer.publishesSurfaceFluid() || y <= layer.bedY() || y > layer.fluidHeadY()) {
|
||||
continue;
|
||||
}
|
||||
HydrologyCaveAction action = layer.fallingFluid() && y < layer.fluidHeadY()
|
||||
? HydrologyCaveAction.FALLING_FLUID
|
||||
: HydrologyCaveAction.WET_SOURCE;
|
||||
SurfacePublicationCell candidate = new SurfacePublicationCell(layer, action);
|
||||
if (selected == null || surfaceActionPriority(action) < surfaceActionPriority(selected.action())) {
|
||||
selected = candidate;
|
||||
}
|
||||
}
|
||||
return Optional.ofNullable(selected);
|
||||
}
|
||||
|
||||
public int terrainHeight() {
|
||||
HydrologyColumnLayer primary = selectSurfaceLayer(false);
|
||||
if (primary == null || !primary.terrainOwned()) {
|
||||
return naturalHeight;
|
||||
}
|
||||
if (primary.channel()) {
|
||||
return primary.bedY();
|
||||
}
|
||||
int terrainHeight = primary.bedY();
|
||||
for (HydrologyColumnLayer candidate : layers) {
|
||||
if (candidate.feature().type().isSurface()
|
||||
&& candidate.terrainOwned()
|
||||
&& !candidate.channel()) {
|
||||
terrainHeight = Math.max(terrainHeight, candidate.bedY());
|
||||
}
|
||||
}
|
||||
return terrainHeight;
|
||||
}
|
||||
|
||||
public HydrologyRenderSample renderSample() {
|
||||
ArrayList<HydrologyFeatureRef> features = new ArrayList<>(layers.size());
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
features.add(layer.feature());
|
||||
}
|
||||
return new HydrologyRenderSample(x, z, features);
|
||||
}
|
||||
|
||||
private HydrologyColumnLayer selectSurfaceLayer(boolean fluidOnly) {
|
||||
HydrologyColumnLayer selected = null;
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.oceanApron() || !layer.feature().type().isSurface()) {
|
||||
continue;
|
||||
}
|
||||
if (fluidOnly) {
|
||||
if (!layer.channel() || !layer.connectedFluid() || !layer.fluidOwned()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (selected == null || SURFACE_LAYER_ORDER.compare(layer, selected) < 0) {
|
||||
selected = layer;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static int surfaceRolePriority(HydrologyColumnLayer layer) {
|
||||
if (layer.channel()) {
|
||||
return 0;
|
||||
}
|
||||
if (layer.terrainOwned()) {
|
||||
return layer.shore() ? 1 : 2;
|
||||
}
|
||||
return layer.shore() ? 3 : 4;
|
||||
}
|
||||
|
||||
private static int surfaceActionPriority(HydrologyCaveAction action) {
|
||||
return action == HydrologyCaveAction.WET_SOURCE ? 0 : 1;
|
||||
}
|
||||
|
||||
public record SurfacePublicationCell(
|
||||
HydrologyColumnLayer layer,
|
||||
HydrologyCaveAction action
|
||||
) {
|
||||
public SurfacePublicationCell {
|
||||
Objects.requireNonNull(layer, "layer");
|
||||
if (action != HydrologyCaveAction.WET_SOURCE
|
||||
&& action != HydrologyCaveAction.FALLING_FLUID) {
|
||||
throw new IllegalArgumentException("Surface publication cells must contain fluid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public record HydrologyColumnSnapshot(boolean available, HydrologyColumnSample column) {
|
||||
private static final HydrologyColumnSnapshot UNAVAILABLE = new HydrologyColumnSnapshot(false, null);
|
||||
private static final HydrologyColumnSnapshot READY_NATURAL = new HydrologyColumnSnapshot(true, null);
|
||||
|
||||
public HydrologyColumnSnapshot {
|
||||
if (!available && column != null) {
|
||||
throw new IllegalArgumentException("Unavailable hydrology cannot contain a planned column.");
|
||||
}
|
||||
}
|
||||
|
||||
public static HydrologyColumnSnapshot unavailable() {
|
||||
return UNAVAILABLE;
|
||||
}
|
||||
|
||||
public static HydrologyColumnSnapshot ready(HydrologyColumnSample column) {
|
||||
return column == null ? READY_NATURAL : new HydrologyColumnSnapshot(true, column);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package art.arcane.iris.generation.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
|
||||
) {
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveConflictPolicy;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
final class HydrologyCrossTileCaveAdmission {
|
||||
private static final Comparator<Claim> CLAIM_PRIORITY = Comparator
|
||||
.comparing((Claim claim) -> claim.plan().source(), HydrologyCaveConflictPolicy.sourcePriority())
|
||||
.thenComparing(Claim::profileKey);
|
||||
private static final Comparator<RankedClaim> BLOCKER_PRIORITY = Comparator
|
||||
.comparingInt(RankedClaim::ownerRank)
|
||||
.thenComparing(RankedClaim::claim, CLAIM_PRIORITY)
|
||||
.thenComparing(RankedClaim::ownerKey);
|
||||
|
||||
private HydrologyCrossTileCaveAdmission() {
|
||||
}
|
||||
|
||||
static Result admit(
|
||||
int currentOwnerRank,
|
||||
Collection<Claim> currentClaims,
|
||||
Collection<RankedClaim> blockers
|
||||
) {
|
||||
if (currentOwnerRank < 0) {
|
||||
throw new IllegalArgumentException("currentOwnerRank cannot be negative");
|
||||
}
|
||||
ArrayList<Claim> orderedClaims = new ArrayList<>(Objects.requireNonNull(currentClaims, "currentClaims"));
|
||||
orderedClaims.sort(CLAIM_PRIORITY);
|
||||
ArrayList<RankedClaim> orderedBlockers = new ArrayList<>(Objects.requireNonNull(blockers, "blockers"));
|
||||
orderedBlockers.sort(BLOCKER_PRIORITY);
|
||||
for (RankedClaim blocker : orderedBlockers) {
|
||||
if (blocker.ownerRank() >= currentOwnerRank) {
|
||||
throw new IllegalArgumentException("Cross-tile blockers must have a lower owner color rank");
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<Rejection> rejections = new ArrayList<>();
|
||||
for (Claim claim : orderedClaims) {
|
||||
RankedClaim winner = null;
|
||||
for (RankedClaim blocker : orderedBlockers) {
|
||||
if (claim.courseId() == blocker.claim().courseId()) {
|
||||
throw new IllegalArgumentException("A cave course cannot belong to multiple color-ranked owners");
|
||||
}
|
||||
if (!conflicts(claim, blocker.claim())) {
|
||||
continue;
|
||||
}
|
||||
if (winner == null || BLOCKER_PRIORITY.compare(blocker, winner) < 0) {
|
||||
winner = blocker;
|
||||
}
|
||||
}
|
||||
if (winner != null) {
|
||||
rejections.add(new Rejection(claim, winner));
|
||||
}
|
||||
}
|
||||
return new Result(rejections);
|
||||
}
|
||||
|
||||
private static boolean conflicts(Claim left, Claim right) {
|
||||
return HydrologyCaveConflictPolicy.hasIncompatibleOverlap(
|
||||
left.profileKey(),
|
||||
left.plan().actions(),
|
||||
right.profileKey(),
|
||||
right.plan().actions()
|
||||
);
|
||||
}
|
||||
|
||||
record Claim(String profileKey, HydrologyCavePlan plan) {
|
||||
Claim {
|
||||
if (profileKey == null || profileKey.isBlank()) {
|
||||
throw new IllegalArgumentException("profileKey must not be blank");
|
||||
}
|
||||
profileKey = profileKey.trim();
|
||||
Objects.requireNonNull(plan, "plan");
|
||||
if (!plan.accepted()) {
|
||||
throw new IllegalArgumentException("Cross-tile admission requires accepted cave plans");
|
||||
}
|
||||
}
|
||||
|
||||
long courseId() {
|
||||
return plan.source().sourceId();
|
||||
}
|
||||
}
|
||||
|
||||
record RankedClaim(HydrologyTileKey ownerKey, int ownerRank, Claim claim) {
|
||||
RankedClaim {
|
||||
Objects.requireNonNull(ownerKey, "ownerKey");
|
||||
if (ownerRank < 0) {
|
||||
throw new IllegalArgumentException("ownerRank cannot be negative");
|
||||
}
|
||||
Objects.requireNonNull(claim, "claim");
|
||||
}
|
||||
}
|
||||
|
||||
record Result(List<Rejection> rejections) {
|
||||
Result {
|
||||
rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections"));
|
||||
}
|
||||
|
||||
Map<Long, Long> loserCourseIdsToWinnerSourceIds() {
|
||||
TreeMap<Long, Long> ordered = new TreeMap<>();
|
||||
for (Rejection rejection : rejections) {
|
||||
ordered.put(rejection.loser().courseId(), rejection.winnerSourceId());
|
||||
}
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(ordered));
|
||||
}
|
||||
|
||||
Rejection rejection(Claim claim) {
|
||||
Objects.requireNonNull(claim, "claim");
|
||||
for (Rejection rejection : rejections) {
|
||||
if (rejection.loser().equals(claim)) {
|
||||
return rejection;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
record Rejection(Claim loser, RankedClaim winner) {
|
||||
Rejection {
|
||||
Objects.requireNonNull(loser, "loser");
|
||||
Objects.requireNonNull(winner, "winner");
|
||||
}
|
||||
|
||||
long winnerSourceId() {
|
||||
return winner.claim().plan().source().sourceId();
|
||||
}
|
||||
}
|
||||
}
|
||||
-1114
File diff suppressed because it is too large.
Load diff
-164
@@ -1,164 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyCrossTileSurfaceAdmission {
|
||||
private HydrologyCrossTileSurfaceAdmission() {
|
||||
}
|
||||
|
||||
static Result admit(
|
||||
List<Claim> currentClaims,
|
||||
List<RankedClaim> blockers
|
||||
) {
|
||||
ArrayList<Rejection> rejections = new ArrayList<>();
|
||||
for (Claim current : currentClaims) {
|
||||
for (RankedClaim ranked : blockers) {
|
||||
Claim blocker = ranked.claim();
|
||||
if (!conflicts(current, blocker)) {
|
||||
continue;
|
||||
}
|
||||
rejections.add(new Rejection(current, blocker.courseId()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Result(List.copyOf(rejections));
|
||||
}
|
||||
|
||||
private static boolean conflicts(Claim first, Claim second) {
|
||||
if (first.outletId() == second.outletId() && first.reachesOutlet() && second.reachesOutlet()) {
|
||||
return true;
|
||||
}
|
||||
int minimumSeparation = Math.max(first.sourceSpacing(), second.sourceSpacing());
|
||||
long minimumSeparationSquared = (long) minimumSeparation * minimumSeparation;
|
||||
if (first.centerline().getFirst().distanceSquared2D(second.centerline().getFirst())
|
||||
< minimumSeparationSquared) {
|
||||
return true;
|
||||
}
|
||||
if (first.reachesOutlet() && second.reachesOutlet()
|
||||
&& first.terminal().distanceSquared2D(second.terminal())
|
||||
< minimumSeparationSquared) {
|
||||
return true;
|
||||
}
|
||||
double clearance = Math.max(4D, (first.maximumWidth() + second.maximumWidth()) / 2D + 4D);
|
||||
return centerlinesWithin(first.centerline(), second.centerline(), clearance);
|
||||
}
|
||||
|
||||
private static boolean centerlinesWithin(
|
||||
List<HydrologyPoint> first,
|
||||
List<HydrologyPoint> second,
|
||||
double clearance
|
||||
) {
|
||||
double clearanceSquared = clearance * clearance;
|
||||
for (int firstIndex = 1; firstIndex < first.size(); firstIndex++) {
|
||||
HydrologyPoint firstStart = first.get(firstIndex - 1);
|
||||
HydrologyPoint firstEnd = first.get(firstIndex);
|
||||
for (int secondIndex = 1; secondIndex < second.size(); secondIndex++) {
|
||||
HydrologyPoint secondStart = second.get(secondIndex - 1);
|
||||
HydrologyPoint secondEnd = second.get(secondIndex);
|
||||
if (segmentDistanceSquared(firstStart, firstEnd, secondStart, secondEnd) <= clearanceSquared) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static double segmentDistanceSquared(
|
||||
HydrologyPoint firstStart,
|
||||
HydrologyPoint firstEnd,
|
||||
HydrologyPoint secondStart,
|
||||
HydrologyPoint secondEnd
|
||||
) {
|
||||
if (segmentsIntersect(firstStart, firstEnd, secondStart, secondEnd)) {
|
||||
return 0D;
|
||||
}
|
||||
return Math.min(
|
||||
Math.min(pointSegmentDistanceSquared(firstStart, secondStart, secondEnd),
|
||||
pointSegmentDistanceSquared(firstEnd, secondStart, secondEnd)),
|
||||
Math.min(pointSegmentDistanceSquared(secondStart, firstStart, firstEnd),
|
||||
pointSegmentDistanceSquared(secondEnd, firstStart, firstEnd))
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean segmentsIntersect(
|
||||
HydrologyPoint firstStart,
|
||||
HydrologyPoint firstEnd,
|
||||
HydrologyPoint secondStart,
|
||||
HydrologyPoint secondEnd
|
||||
) {
|
||||
long firstA = orientation(firstStart, firstEnd, secondStart);
|
||||
long firstB = orientation(firstStart, firstEnd, secondEnd);
|
||||
long secondA = orientation(secondStart, secondEnd, firstStart);
|
||||
long secondB = orientation(secondStart, secondEnd, firstEnd);
|
||||
return Long.signum(firstA) != Long.signum(firstB)
|
||||
&& Long.signum(secondA) != Long.signum(secondB);
|
||||
}
|
||||
|
||||
private static long orientation(HydrologyPoint start, HydrologyPoint end, HydrologyPoint point) {
|
||||
long firstX = end.x() - (long) start.x();
|
||||
long firstZ = end.z() - (long) start.z();
|
||||
long secondX = point.x() - (long) start.x();
|
||||
long secondZ = point.z() - (long) start.z();
|
||||
return firstX * secondZ - firstZ * secondX;
|
||||
}
|
||||
|
||||
private static double pointSegmentDistanceSquared(
|
||||
HydrologyPoint point,
|
||||
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) {
|
||||
return point.distanceSquared2D(start);
|
||||
}
|
||||
double progress = ((point.x() - start.x()) * deltaX + (point.z() - start.z()) * deltaZ)
|
||||
/ lengthSquared;
|
||||
progress = Math.max(0D, Math.min(1D, progress));
|
||||
double closestX = start.x() + deltaX * progress;
|
||||
double closestZ = start.z() + deltaZ * progress;
|
||||
double pointX = point.x() - closestX;
|
||||
double pointZ = point.z() - closestZ;
|
||||
return pointX * pointX + pointZ * pointZ;
|
||||
}
|
||||
|
||||
record Claim(
|
||||
long courseId,
|
||||
long outletId,
|
||||
HydrologyPoint terminal,
|
||||
boolean reachesOutlet,
|
||||
int maximumWidth,
|
||||
List<HydrologyPoint> centerline,
|
||||
int sourceSpacing
|
||||
) {
|
||||
Claim {
|
||||
Objects.requireNonNull(terminal, "terminal");
|
||||
centerline = List.copyOf(Objects.requireNonNull(centerline, "centerline"));
|
||||
if (maximumWidth < 1 || centerline.size() < 2 || sourceSpacing < 0 || sourceSpacing > 8192) {
|
||||
throw new IllegalArgumentException("Surface claims require a positive width and centerline.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record RankedClaim(HydrologyTileKey ownerKey, int ownerRank, Claim claim) {
|
||||
RankedClaim {
|
||||
Objects.requireNonNull(ownerKey, "ownerKey");
|
||||
Objects.requireNonNull(claim, "claim");
|
||||
}
|
||||
}
|
||||
|
||||
record Rejection(Claim loser, long winnerCourseId) {
|
||||
Rejection {
|
||||
Objects.requireNonNull(loser, "loser");
|
||||
}
|
||||
}
|
||||
|
||||
record Result(List<Rejection> rejections) {
|
||||
Result {
|
||||
rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections"));
|
||||
}
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyDiagnosticCandidate(
|
||||
long id,
|
||||
HydrologyCandidateKind kind,
|
||||
HydrologyFeatureType projectedType,
|
||||
HydrologyPoint point,
|
||||
HydrologyCandidateRejection rejection,
|
||||
int detail
|
||||
) {
|
||||
public HydrologyDiagnosticCandidate {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
Objects.requireNonNull(projectedType, "projectedType");
|
||||
Objects.requireNonNull(point, "point");
|
||||
Objects.requireNonNull(rejection, "rejection");
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyDiagnosticRenderSample(
|
||||
int x,
|
||||
int z,
|
||||
List<HydrologyDiagnosticCandidate> candidates
|
||||
) {
|
||||
private static final Comparator<HydrologyDiagnosticCandidate> CANDIDATE_ORDER = Comparator
|
||||
.comparing(HydrologyDiagnosticCandidate::kind)
|
||||
.thenComparing(HydrologyDiagnosticCandidate::rejection)
|
||||
.thenComparingLong(HydrologyDiagnosticCandidate::id);
|
||||
|
||||
public HydrologyDiagnosticRenderSample {
|
||||
Objects.requireNonNull(candidates, "candidates");
|
||||
ArrayList<HydrologyDiagnosticCandidate> ordered = new ArrayList<>(candidates);
|
||||
ordered.sort(CANDIDATE_ORDER);
|
||||
candidates = List.copyOf(ordered);
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
return !candidates.isEmpty();
|
||||
}
|
||||
}
|
||||
-280
@@ -1,280 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
final class HydrologyDrainageGeometry {
|
||||
private final HydrologyPlanner planner;
|
||||
private final Basin basin;
|
||||
private final int[] heads;
|
||||
private final AtomicReferenceArray<HydrologyPoint> anchors;
|
||||
private final AtomicReferenceArray<CompletableFuture<List<HydrologyPoint>>> edges;
|
||||
|
||||
HydrologyDrainageGeometry(HydrologyPlanner planner, Basin basin) {
|
||||
this.planner = planner;
|
||||
this.basin = basin;
|
||||
this.heads = new int[basin.grid().nodes().size()];
|
||||
this.anchors = new AtomicReferenceArray<>(heads.length);
|
||||
this.edges = new AtomicReferenceArray<>(heads.length);
|
||||
Arrays.fill(heads, Integer.MIN_VALUE);
|
||||
planHeads();
|
||||
}
|
||||
|
||||
int head(int index) {
|
||||
return heads[index];
|
||||
}
|
||||
|
||||
HydrologyPoint anchor(int index) {
|
||||
HydrologyPoint existing = anchors.get(index);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
HydrologyGridNode node = basin.grid().node(index);
|
||||
HydrologyPoint point;
|
||||
if (basin.parent()[index] < 0 && basin.outletIndex()[index] >= 0) {
|
||||
HydrologyPoint landward = basin.outlets().get(basin.outletIndex()[index]).outlet().landwardPoint();
|
||||
point = new HydrologyPoint(landward.x(), heads[index], landward.z());
|
||||
} else {
|
||||
point = displacedAnchor(node);
|
||||
}
|
||||
anchors.compareAndSet(index, null, point);
|
||||
return anchors.get(index);
|
||||
}
|
||||
|
||||
List<HydrologyPoint> edge(int index) {
|
||||
DraftProfile profile = planner.currentDraftProfile();
|
||||
profile.routeCalls++;
|
||||
CompletableFuture<List<HydrologyPoint>> future = edges.get(index);
|
||||
if (future == null) {
|
||||
CompletableFuture<List<HydrologyPoint>> owned = new CompletableFuture<>();
|
||||
CompletableFuture<List<HydrologyPoint>> existing = edges.compareAndExchange(index, null, owned);
|
||||
if (existing == null) {
|
||||
long started = System.nanoTime();
|
||||
profile.routeSolves++;
|
||||
try {
|
||||
List<HydrologyPoint> points = compile(index);
|
||||
owned.complete(points);
|
||||
return points;
|
||||
} catch (Throwable failure) {
|
||||
owned.completeExceptionally(failure);
|
||||
edges.compareAndSet(index, owned, null);
|
||||
throw HydrologyPlanner.propagateOwnerFailure(failure);
|
||||
} finally {
|
||||
profile.routeSolveNanos += System.nanoTime() - started;
|
||||
}
|
||||
}
|
||||
future = existing;
|
||||
}
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException failure) {
|
||||
throw HydrologyPlanner.propagateOwnerFailure(failure.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
long maximumRetainedBytes() {
|
||||
int stations = Math.ceilDiv(basin.grid().spacing() * 3, planner.settings.routing().refinementSpacing()) + 2;
|
||||
return (long) heads.length * (112L + stations * 40L);
|
||||
}
|
||||
|
||||
private void planHeads() {
|
||||
int[] path = new int[heads.length];
|
||||
int[] nominal = new int[heads.length];
|
||||
for (int index = 0; index < heads.length; index++) {
|
||||
if (heads[index] != Integer.MIN_VALUE || basin.outletIndex()[index] < 0) {
|
||||
continue;
|
||||
}
|
||||
int current = index;
|
||||
int size = 0;
|
||||
while (current >= 0 && heads[current] == Integer.MIN_VALUE) {
|
||||
if (size == path.length) {
|
||||
throw new IllegalStateException("Surface drainage must be acyclic.");
|
||||
}
|
||||
path[size++] = current;
|
||||
current = basin.parent()[current];
|
||||
}
|
||||
int downstream = current < 0 ? Integer.MIN_VALUE : heads[current];
|
||||
while (size > 0) {
|
||||
int node = path[--size];
|
||||
HydrologyTerrainSample terrain = basin.grid().node(node).terrain();
|
||||
if (basin.parent()[node] < 0) {
|
||||
HydrologyPoint landward = basin.outlets().get(basin.outletIndex()[node]).outlet().landwardPoint();
|
||||
HydrologyTerrainSample sampled = planner.sampleLandBasisWithoutSlope(landward.x(), landward.z());
|
||||
if (sampled != null) {
|
||||
terrain = sampled;
|
||||
}
|
||||
}
|
||||
int depth = (planner.settings.surface().minimumDepth() + planner.settings.surface().maximumDepth()) / 2;
|
||||
nominal[node] = terrain.naturalHeight() - planner.settings.surface().banks().sink() - depth;
|
||||
int required = terrain.naturalHeight() - planner.sourcePlanner.permittedSurfaceIncision(terrain) + depth;
|
||||
heads[node] = Math.max(Math.max(planner.settings.seaLevel(), required), downstream);
|
||||
downstream = heads[node];
|
||||
}
|
||||
}
|
||||
int[] required = heads.clone();
|
||||
int[] ceiling = new int[heads.length];
|
||||
Arrays.fill(ceiling, Integer.MAX_VALUE);
|
||||
int[] upstream = new int[heads.length];
|
||||
for (int index = 0; index < heads.length; index++) {
|
||||
if (heads[index] == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
if (planner.sourcePlanner.rawSourceEligible(basin.grid().node(index).terrain(),
|
||||
planner.settings.surface().sources(), true)) {
|
||||
ceiling[index] = Math.max(required[index], nominal[index]);
|
||||
}
|
||||
int parent = basin.parent()[index];
|
||||
if (parent >= 0) {
|
||||
upstream[parent]++;
|
||||
}
|
||||
}
|
||||
int end = 0;
|
||||
for (int index = 0; index < heads.length; index++) {
|
||||
if (heads[index] != Integer.MIN_VALUE && upstream[index] == 0) {
|
||||
path[end++] = index;
|
||||
}
|
||||
}
|
||||
for (int position = 0; position < end; position++) {
|
||||
int node = path[position];
|
||||
int parent = basin.parent()[node];
|
||||
if (parent >= 0) {
|
||||
ceiling[parent] = Math.min(ceiling[parent], ceiling[node]);
|
||||
if (--upstream[parent] == 0) {
|
||||
path[end++] = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int position = end - 1; position >= 0; position--) {
|
||||
int node = path[position];
|
||||
int parent = basin.parent()[node];
|
||||
int downstream = parent < 0 ? Integer.MIN_VALUE : heads[parent];
|
||||
heads[node] = Math.max(Math.max(required[node], downstream), Math.min(nominal[node], ceiling[node]));
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyPoint displacedAnchor(HydrologyGridNode node) {
|
||||
double ratio = Math.min(HydrologyRouteGeometry.ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO,
|
||||
planner.settings.geometry().meanders().maximumOffsetRatio() * 0.625D);
|
||||
double wavelength = planner.settings.geometry().meanders().primaryWavelength() * 5D;
|
||||
int x = node.x() + (int) StrictMath.round(basin.grid().spacing() * ratio
|
||||
* planner.routeAnchorX.noiseSigned(node.x() / wavelength, node.z() / wavelength));
|
||||
int z = node.z() + (int) StrictMath.round(basin.grid().spacing() * ratio
|
||||
* planner.routeAnchorZ.noiseSigned(node.x() / wavelength, node.z() / wavelength));
|
||||
if (!permittedAnchor(node, x, z)) {
|
||||
x = node.x();
|
||||
z = node.z();
|
||||
}
|
||||
if (nearCoast(node)) {
|
||||
int clearance = (int) StrictMath.ceil(planner.settings.surface().maximumWidth()
|
||||
* planner.settings.surface().banks().springWidthRatio() / 2D) + 1;
|
||||
if (!landClearance(x, z, clearance)) {
|
||||
int radius = Math.max(1, (int) StrictMath.ceil(basin.grid().spacing()
|
||||
* HydrologyRouteGeometry.ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO));
|
||||
long nearest = Long.MAX_VALUE;
|
||||
int selectedX = x;
|
||||
int selectedZ = z;
|
||||
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||
int candidateX = node.x() + offset.x() * radius;
|
||||
int candidateZ = node.z() + offset.z() * radius;
|
||||
long dx = (long) candidateX - x;
|
||||
long dz = (long) candidateZ - z;
|
||||
long distance = dx * dx + dz * dz;
|
||||
if (distance < nearest && permittedAnchor(node, candidateX, candidateZ)
|
||||
&& landClearance(candidateX, candidateZ, clearance)) {
|
||||
nearest = distance;
|
||||
selectedX = candidateX;
|
||||
selectedZ = candidateZ;
|
||||
}
|
||||
}
|
||||
x = selectedX;
|
||||
z = selectedZ;
|
||||
}
|
||||
}
|
||||
return new HydrologyPoint(x, heads[node.index()], z);
|
||||
}
|
||||
|
||||
private boolean permittedAnchor(HydrologyGridNode node, int x, int z) {
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x, z);
|
||||
return terrain != null && !terrain.ocean() && terrain.transitAllowed()
|
||||
&& node.terrain().drainsInto(terrain) && terrain.drainsInto(node.terrain())
|
||||
&& HydrologySurfaceProfiles.sharesProfile(node.terrain(), terrain)
|
||||
&& node.terrain().surfacePolicy().areaKey().equals(terrain.surfacePolicy().areaKey());
|
||||
}
|
||||
|
||||
private boolean nearCoast(HydrologyGridNode node) {
|
||||
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||
HydrologyGridNode neighbor = basin.grid().nodeAt(node.gridX() + offset.x(), node.gridZ() + offset.z());
|
||||
if (neighbor != null && neighbor.terrain().ocean()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean landClearance(int x, int z, int radius) {
|
||||
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x + offset.x() * radius,
|
||||
z + offset.z() * radius);
|
||||
if (terrain == null || terrain.ocean()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> compile(int index) {
|
||||
int downstream = basin.parent()[index];
|
||||
if (downstream < 0) {
|
||||
return List.of();
|
||||
}
|
||||
HydrologyPoint start = anchor(index);
|
||||
HydrologyPoint end = anchor(downstream);
|
||||
int continuation = basin.parent()[downstream];
|
||||
HydrologyPoint next = continuation < 0 ? basin.outlets().get(basin.outletIndex()[index])
|
||||
.outlet().connectionPoint() : anchor(continuation);
|
||||
RouteDirection startTangent = planner.routePaths.direction(start.x(), start.z(), end.x(), end.z());
|
||||
RouteDirection endTangent = planner.routePaths.direction(end.x(), end.z(), next.x(), next.z());
|
||||
if (endTangent.x() == 0D && endTangent.z() == 0D) {
|
||||
endTangent = startTangent;
|
||||
}
|
||||
double distance = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
|
||||
int steps = Math.max(1, (int) StrictMath.ceil(distance / planner.settings.routing().refinementSpacing()));
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>(steps + 1);
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
RoutePosition position = planner.routeGeometry.routePosition(start, end, startTangent, endTangent,
|
||||
distance * HydrologyRouteGeometry.ROUTE_TANGENT_SCALE, distance, progress, 1D);
|
||||
HydrologyPoint point = step == 0 ? start : step == steps ? end
|
||||
: new HydrologyPoint((int) StrictMath.round(position.x()),
|
||||
(int) StrictMath.round(start.y() + (end.y() - start.y()) * progress),
|
||||
(int) StrictMath.round(position.z()));
|
||||
if (points.isEmpty() || points.getLast().distanceSquared2D(point) > 0L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
return permitted(points) ? List.copyOf(points) : List.of();
|
||||
}
|
||||
|
||||
private boolean permitted(List<HydrologyPoint> points) {
|
||||
HydrologyTerrainSample previous = null;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
for (HydrologyPoint point : planner.segments.rasterLine(points.get(index - 1), points.get(index))) {
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
|
||||
if (terrain == null || terrain.ocean() || !terrain.transitAllowed()
|
||||
|| previous != null && (!previous.drainsInto(terrain)
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(previous, terrain))) {
|
||||
return false;
|
||||
}
|
||||
previous = terrain;
|
||||
}
|
||||
}
|
||||
return points.size() >= 2;
|
||||
}
|
||||
|
||||
record Basin(HydrologySampledGrid grid, int[] parent, int[] outletIndex, List<OutletCandidate> outlets) {
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
final class HydrologyDrainageGraph {
|
||||
private final HydrologyPlannerSettings settings;
|
||||
private final int[] upstream;
|
||||
private final double[] costs;
|
||||
private final byte[] eligibility;
|
||||
|
||||
HydrologyDrainageGraph(HydrologySampledGrid grid, HydrologySourcePlanner planner) {
|
||||
settings = planner.settings();
|
||||
int directions = HydrologySourcePlanner.ROUTING_OFFSETS.size();
|
||||
int size = Math.multiplyExact(grid.nodes().size(), directions);
|
||||
upstream = new int[size];
|
||||
costs = new double[size];
|
||||
eligibility = new byte[size];
|
||||
for (HydrologyGridNode downstream : grid.nodes()) {
|
||||
int valleyFloor = planner.minimumNeighborHeight(grid, downstream);
|
||||
for (int direction = 0; direction < directions; direction++) {
|
||||
int edge = downstream.index() * directions + direction;
|
||||
HydrologyGridOffset offset = HydrologySourcePlanner.ROUTING_OFFSETS.get(direction);
|
||||
HydrologyGridNode source = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
|
||||
upstream[edge] = source == null ? -1 : source.index();
|
||||
if (source == null || source.terrain().ocean() || !source.terrain().transitAllowed()
|
||||
|| !source.terrain().drainsInto(downstream.terrain())) {
|
||||
continue;
|
||||
}
|
||||
eligibility[edge] = 1;
|
||||
if (HydrologySurfaceProfiles.sharesProfile(source.terrain(), downstream.terrain())
|
||||
&& downstream.terrain().naturalHeight() - source.terrain().naturalHeight()
|
||||
<= planner.maximumSurfaceEdgeRise(downstream.terrain())) {
|
||||
eligibility[edge] |= 2;
|
||||
}
|
||||
costs[edge] = planner.routeCost(source, downstream, offset, valleyFloor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyPlannerSettings settings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
int upstream(int downstream, int direction, boolean surface) {
|
||||
int edge = downstream * HydrologySourcePlanner.ROUTING_OFFSETS.size() + direction;
|
||||
return (eligibility[edge] & (surface ? 2 : 1)) == 0 ? -1 : upstream[edge];
|
||||
}
|
||||
|
||||
double cost(int downstream, int direction) {
|
||||
return costs[downstream * HydrologySourcePlanner.ROUTING_OFFSETS.size() + direction];
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public record HydrologyFeatureQuery(Set<HydrologyFeatureType> types, String profileKey) {
|
||||
private static final Set<HydrologyFeatureType> SURFACE = Set.of(
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
HydrologyFeatureType.RIFFLE,
|
||||
HydrologyFeatureType.CASCADE,
|
||||
HydrologyFeatureType.WATERFALL,
|
||||
HydrologyFeatureType.MOUTH
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> UNDERGROUND = Set.of(
|
||||
HydrologyFeatureType.UNDERGROUND_POOL,
|
||||
HydrologyFeatureType.UNDERGROUND_DROP,
|
||||
HydrologyFeatureType.SINKHOLE
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> GROTTOS = Set.of(
|
||||
HydrologyFeatureType.COASTAL_GROTTO,
|
||||
HydrologyFeatureType.INLAND_GROTTO
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> DEEP = Set.of(
|
||||
HydrologyFeatureType.DEEP_POOL,
|
||||
HydrologyFeatureType.DEEP_CHANNEL
|
||||
);
|
||||
private static final Set<HydrologyFeatureType> PROFILED = Set.of(
|
||||
HydrologyFeatureType.DEEP_POOL,
|
||||
HydrologyFeatureType.DEEP_CHANNEL,
|
||||
HydrologyFeatureType.STANDING_POOL
|
||||
);
|
||||
private static final Map<String, Set<HydrologyFeatureType>> BUILT_INS = builtIns();
|
||||
|
||||
public HydrologyFeatureQuery {
|
||||
types = Set.copyOf(types);
|
||||
if (types.isEmpty()) {
|
||||
throw new IllegalArgumentException("Hydrology feature query requires at least one feature type.");
|
||||
}
|
||||
profileKey = profileKey == null || profileKey.isBlank() ? null : profileKey.trim();
|
||||
}
|
||||
|
||||
public static HydrologyFeatureQuery parse(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Hydrology feature type must not be blank.");
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.regionMatches(true, 0, "type=", 0, 5)) {
|
||||
normalized = normalized.substring(5).trim();
|
||||
}
|
||||
String key = normalizeKeyword(normalized);
|
||||
Set<HydrologyFeatureType> builtInTypes = BUILT_INS.get(key);
|
||||
return builtInTypes == null
|
||||
? new HydrologyFeatureQuery(PROFILED, normalized)
|
||||
: new HydrologyFeatureQuery(builtInTypes, null);
|
||||
}
|
||||
|
||||
public static List<String> suggestions(Collection<String> deepFluidIds) {
|
||||
LinkedHashSet<String> suggestions = new LinkedHashSet<>(BUILT_INS.keySet());
|
||||
TreeSet<String> orderedIds = new TreeSet<>();
|
||||
if (deepFluidIds != null) {
|
||||
for (String id : deepFluidIds) {
|
||||
if (id != null && !id.isBlank() && !isReservedKeyword(id)) {
|
||||
orderedIds.add(id.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
suggestions.addAll(orderedIds);
|
||||
return List.copyOf(suggestions);
|
||||
}
|
||||
|
||||
public static boolean isReservedKeyword(String value) {
|
||||
return value != null && !value.isBlank() && BUILT_INS.containsKey(normalizeKeyword(value.trim()));
|
||||
}
|
||||
|
||||
private static String normalizeKeyword(String value) {
|
||||
return value.toLowerCase(Locale.ROOT).replace('-', '_');
|
||||
}
|
||||
|
||||
private static Map<String, Set<HydrologyFeatureType>> builtIns() {
|
||||
LinkedHashMap<String, Set<HydrologyFeatureType>> builtIns = new LinkedHashMap<>();
|
||||
builtIns.put("surface", SURFACE);
|
||||
builtIns.put("waterfall", Set.of(HydrologyFeatureType.WATERFALL));
|
||||
builtIns.put("sinkhole", Set.of(HydrologyFeatureType.SINKHOLE));
|
||||
builtIns.put("underground", UNDERGROUND);
|
||||
builtIns.put("grotto", GROTTOS);
|
||||
builtIns.put("coastal_grotto", Set.of(HydrologyFeatureType.COASTAL_GROTTO));
|
||||
builtIns.put("inland_grotto", Set.of(HydrologyFeatureType.INLAND_GROTTO));
|
||||
builtIns.put("mouth", Set.of(HydrologyFeatureType.MOUTH));
|
||||
builtIns.put("deep", DEEP);
|
||||
builtIns.put("pool", Set.of(HydrologyFeatureType.STANDING_POOL));
|
||||
return Collections.unmodifiableMap(builtIns);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record HydrologyFeatureRef(
|
||||
long id,
|
||||
HydrologyFeatureType type,
|
||||
long courseId,
|
||||
long segmentId,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int flowDeltaX,
|
||||
int flowDeltaZ,
|
||||
boolean source
|
||||
) {
|
||||
public HydrologyFeatureRef {
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (flowDeltaX < -1 || flowDeltaX > 1 || flowDeltaZ < -1 || flowDeltaZ > 1) {
|
||||
throw new IllegalArgumentException("Flow direction components must be between -1 and 1.");
|
||||
}
|
||||
}
|
||||
|
||||
public HydrologyPoint point() {
|
||||
return new HydrologyPoint(x, y, z);
|
||||
}
|
||||
}
|
||||
-767
@@ -1,767 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.ChannelProfile;
|
||||
import art.arcane.iris.generation.hydrology.surface.ValleyProfile;
|
||||
import art.arcane.iris.generation.hydrology.surface.ValleyProfileSolver;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.generation.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, HydraulicChannelProfile.uniform(radius * 2, pool.depth())
|
||||
);
|
||||
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, false);
|
||||
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), HydraulicChannelProfile.uniform(horizontalRadius * 2 + 1, Math.max(deepFluid.channelDepth(), verticalRadius))
|
||||
));
|
||||
}
|
||||
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()
|
||||
), HydraulicChannelProfile.uniform(deepFluid.channelWidth(), deepFluid.channelDepth())
|
||||
));
|
||||
}
|
||||
}
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public enum HydrologyFeatureType {
|
||||
SURFACE_POOL,
|
||||
RIFFLE,
|
||||
CASCADE,
|
||||
WATERFALL,
|
||||
// No planner produces a ridge bore any more. The constant stays because its ordinal seeds the
|
||||
// segment, outlet and feature ids of every constant after it on the cave and deep-fluid paths.
|
||||
RIDGE_BORE,
|
||||
UNDERGROUND_POOL,
|
||||
UNDERGROUND_DROP,
|
||||
SINKHOLE,
|
||||
COASTAL_GROTTO,
|
||||
INLAND_GROTTO,
|
||||
MOUTH,
|
||||
DEEP_POOL,
|
||||
DEEP_CHANNEL,
|
||||
// A standing surface pool (lava or any fluid) with no course: a bowl cut into the ground with a lip.
|
||||
STANDING_POOL;
|
||||
|
||||
public boolean isSurface() {
|
||||
return switch (this) {
|
||||
case SURFACE_POOL, RIFFLE, CASCADE, WATERFALL, MOUTH, STANDING_POOL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isUnderground() {
|
||||
return switch (this) {
|
||||
case RIDGE_BORE, UNDERGROUND_POOL, UNDERGROUND_DROP, SINKHOLE, COASTAL_GROTTO, INLAND_GROTTO -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isGrotto() {
|
||||
return this == COASTAL_GROTTO || this == INLAND_GROTTO;
|
||||
}
|
||||
|
||||
public boolean isDeepFluid() {
|
||||
return this == DEEP_POOL || this == DEEP_CHANNEL;
|
||||
}
|
||||
|
||||
public boolean isDrop() {
|
||||
return this == RIFFLE || this == CASCADE || this == WATERFALL
|
||||
|| this == UNDERGROUND_DROP || this == SINKHOLE;
|
||||
}
|
||||
|
||||
int renderPriority() {
|
||||
return switch (this) {
|
||||
case WATERFALL -> 0;
|
||||
case SINKHOLE -> 1;
|
||||
case CASCADE -> 2;
|
||||
case RIFFLE -> 3;
|
||||
case COASTAL_GROTTO -> 4;
|
||||
case INLAND_GROTTO -> 5;
|
||||
case MOUTH -> 6;
|
||||
case RIDGE_BORE -> 7;
|
||||
case UNDERGROUND_DROP -> 8;
|
||||
case UNDERGROUND_POOL -> 9;
|
||||
case DEEP_CHANNEL -> 10;
|
||||
case DEEP_POOL -> 11;
|
||||
case SURFACE_POOL -> 12;
|
||||
case STANDING_POOL -> 13;
|
||||
};
|
||||
}
|
||||
}
|
||||
-1386
File diff suppressed because it is too large.
Load diff
-577
@@ -1,577 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyFootprintRasterizer {
|
||||
private final HydrologyFootprintCompiler compiler;
|
||||
|
||||
HydrologyFootprintRasterizer(HydrologyFootprintCompiler compiler) {
|
||||
this.compiler = compiler;
|
||||
}
|
||||
|
||||
void rasterizeSegment(
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
boolean firstSegment,
|
||||
boolean clipStart,
|
||||
boolean clipEnd,
|
||||
boolean validationOnly,
|
||||
HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface
|
||||
) {
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> segmentColumns = segment.type().isSurface() && segment.fallingFluid()
|
||||
? new Long2ObjectLinkedOpenHashMap<>() : columns;
|
||||
List<HydrologyPoint> centerline = continuousCenterline(segment);
|
||||
if (segment.fallingFluid() && centerline.size() > 1) {
|
||||
HydrologyPoint throat = centerline.getFirst();
|
||||
int fluidHead = segment.upstreamHeadY();
|
||||
FootprintLayerShape throatShape = compiler.channelGeometry.shape(
|
||||
course,
|
||||
segment,
|
||||
throat,
|
||||
segment.downstreamHeadY() - 1,
|
||||
fluidHead,
|
||||
true,
|
||||
false
|
||||
);
|
||||
rasterizePoint(
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
throat,
|
||||
0,
|
||||
throatShape,
|
||||
compiler.flowDelta(centerline, 0, true),
|
||||
compiler.flowDelta(centerline, 0, false),
|
||||
firstSegment,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
validationOnly,
|
||||
plannedSurface
|
||||
);
|
||||
rasterizeSweptSegment(
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
List.copyOf(centerline.subList(1, centerline.size())),
|
||||
false,
|
||||
true,
|
||||
clipEnd,
|
||||
validationOnly,
|
||||
plannedSurface
|
||||
);
|
||||
if (segmentColumns != columns) {
|
||||
compiler.mergeSurfaceDrop(columns, segmentColumns);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sweptChannel(segment, centerline)) {
|
||||
rasterizeSweptSegment(
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
centerline,
|
||||
firstSegment,
|
||||
clipStart,
|
||||
clipEnd,
|
||||
validationOnly,
|
||||
plannedSurface
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) {
|
||||
HydrologyPoint point = centerline.get(pointIndex);
|
||||
boolean oceanConnection = (segment.type() == HydrologyFeatureType.MOUTH
|
||||
|| segment.type() == HydrologyFeatureType.COASTAL_GROTTO)
|
||||
&& pointIndex == centerline.size() - 1;
|
||||
boolean falling = segment.fallingFluid() && pointIndex == 0;
|
||||
boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1;
|
||||
int fluidHead = falling ? segment.upstreamHeadY() : point.y();
|
||||
int bed = falling
|
||||
? segment.downstreamHeadY() - 1
|
||||
: fluidHead - segment.depth();
|
||||
FootprintLayerShape shape = compiler.channelGeometry.shape(course, segment, point, bed, fluidHead, falling, receiving);
|
||||
int flowX = compiler.flowDelta(centerline, pointIndex, true);
|
||||
int flowZ = compiler.flowDelta(centerline, pointIndex, false);
|
||||
rasterizePoint(
|
||||
segmentColumns,
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
pointIndex,
|
||||
shape,
|
||||
flowX,
|
||||
flowZ,
|
||||
firstSegment && pointIndex == 0,
|
||||
falling,
|
||||
receiving,
|
||||
oceanConnection,
|
||||
validationOnly,
|
||||
plannedSurface
|
||||
);
|
||||
}
|
||||
if (segmentColumns != columns) {
|
||||
compiler.mergeSurfaceDrop(columns, segmentColumns);
|
||||
}
|
||||
}
|
||||
|
||||
boolean sweptChannel(
|
||||
HydraulicSegment segment,
|
||||
List<HydrologyPoint> centerline
|
||||
) {
|
||||
if (centerline.size() < 2 || segment.fallingFluid()) {
|
||||
return false;
|
||||
}
|
||||
return segment.type().isSurface()
|
||||
|| segment.type() == HydrologyFeatureType.UNDERGROUND_POOL
|
||||
|| segment.type() == HydrologyFeatureType.UNDERGROUND_DROP
|
||||
|| segment.type() == HydrologyFeatureType.SINKHOLE
|
||||
|| segment.type() == HydrologyFeatureType.DEEP_CHANNEL;
|
||||
}
|
||||
|
||||
void rasterizeSweptSegment(
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
List<HydrologyPoint> centerline,
|
||||
boolean firstSegment,
|
||||
boolean clipStart,
|
||||
boolean clipEnd,
|
||||
boolean validationOnly,
|
||||
HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface
|
||||
) {
|
||||
FootprintLayerShape[] shapes = new FootprintLayerShape[centerline.size()];
|
||||
int maximumRadius = 1;
|
||||
int minimumX = Integer.MAX_VALUE;
|
||||
int maximumX = Integer.MIN_VALUE;
|
||||
int minimumZ = Integer.MAX_VALUE;
|
||||
int maximumZ = Integer.MIN_VALUE;
|
||||
for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) {
|
||||
HydrologyPoint point = centerline.get(pointIndex);
|
||||
int fluidHead = point.y();
|
||||
boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1;
|
||||
FootprintLayerShape shape = compiler.channelGeometry.shape(
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
fluidHead - segment.depth(),
|
||||
fluidHead,
|
||||
false,
|
||||
receiving
|
||||
);
|
||||
shapes[pointIndex] = shape;
|
||||
int rasterRadius = segment.type().isSurface()
|
||||
? (int) StrictMath.ceil(shape.totalRadius())
|
||||
: shape.channelRadius();
|
||||
maximumRadius = Math.max(maximumRadius, rasterRadius);
|
||||
minimumX = Math.min(minimumX, point.x());
|
||||
maximumX = Math.max(maximumX, point.x());
|
||||
minimumZ = Math.min(minimumZ, point.z());
|
||||
maximumZ = Math.max(maximumZ, point.z());
|
||||
}
|
||||
minimumX = Math.subtractExact(minimumX, maximumRadius);
|
||||
maximumX = Math.addExact(maximumX, maximumRadius);
|
||||
minimumZ = Math.subtractExact(minimumZ, maximumRadius);
|
||||
maximumZ = Math.addExact(maximumZ, maximumRadius);
|
||||
HydrologyFeatureRef[][] pointFeatures = new HydrologyFeatureRef[centerline.size()][HydrologyFootprintCompiler.FEATURE_ROLE_COUNT];
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
if (!withinLongitudinalBounds(centerline, segment.start(), x, z, clipStart, clipEnd)) {
|
||||
continue;
|
||||
}
|
||||
FootprintCenterlineProjection projection = projectCenterline(centerline, segment.start(), x, z);
|
||||
int pointIndex = projection.pointIndex();
|
||||
HydrologyPoint point = centerline.get(pointIndex);
|
||||
boolean receiving = segment.receivingPool() && pointIndex == centerline.size() - 1;
|
||||
FootprintLayerShape shape = shapes[pointIndex];
|
||||
int deltaX = x - point.x();
|
||||
int deltaZ = z - point.z();
|
||||
double rawDistance = projection.distance();
|
||||
double rasterRadius = segment.type().isSurface()
|
||||
? shape.totalRadius()
|
||||
: shape.channelRadius();
|
||||
if (rawDistance > rasterRadius + 0.25D) {
|
||||
continue;
|
||||
}
|
||||
int flowX = projection.flowX();
|
||||
int flowZ = projection.flowZ();
|
||||
double channelDistance = compiler.channelGeometry.shapedDistance(
|
||||
shape,
|
||||
segment,
|
||||
x,
|
||||
z,
|
||||
x - projection.x(),
|
||||
z - projection.z(),
|
||||
flowX,
|
||||
flowZ,
|
||||
rawDistance
|
||||
);
|
||||
boolean channel = channelDistance <= shape.channelRadius() + 0.25D;
|
||||
if (!segment.type().isSurface() && !channel) {
|
||||
continue;
|
||||
}
|
||||
boolean shore = !channel
|
||||
&& channelDistance <= shape.channelRadius() + shape.shoreWidth() + 0.25D;
|
||||
boolean grading = !channel && !shore && rawDistance <= shape.totalRadius() + 0.25D;
|
||||
if (!channel && !shore && !grading) {
|
||||
continue;
|
||||
}
|
||||
double distance = grading ? rawDistance : channelDistance;
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classification = compiler.classifyNatural(x, z);
|
||||
boolean oceanConnection = (segment.type() == HydrologyFeatureType.MOUTH
|
||||
|| segment.type() == HydrologyFeatureType.COASTAL_GROTTO)
|
||||
&& pointIndex == centerline.size() - 1;
|
||||
boolean apronEligible = compiler.oceanApronEligible(segment, distance);
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
&& !apronEligible) {
|
||||
continue;
|
||||
}
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND
|
||||
&& oceanConnection) {
|
||||
continue;
|
||||
}
|
||||
boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
&& !oceanConnection
|
||||
&& !channel
|
||||
&& segment.type() != HydrologyFeatureType.WATERFALL
|
||||
&& segment.type() != HydrologyFeatureType.CASCADE;
|
||||
HydrologyTerrainSample terrain = exactSlope ? compiler.sampleTerrain(x, z) : compiler.sampleTerrainBasis(x, z);
|
||||
if (terrain == null || compiler.elevatedSeaLevelSurfaceColumn(segment, terrain, shape.fluidHead())) {
|
||||
continue;
|
||||
}
|
||||
boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
|| terrain.ocean()
|
||||
|| compiler.naturallySubmergedSurfaceColumn(segment, terrain);
|
||||
if (oceanConnection || naturalOcean) {
|
||||
if (naturalOcean && apronEligible) {
|
||||
compiler.addOceanApron(columns, course, segment, point, x, z, distance, flowX, flowZ,
|
||||
terrain, pointFeatures[pointIndex]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
boolean source = firstSegment && pointIndex == 0;
|
||||
if (validationOnly && segment.type().isSurface() && !segment.fallingFluid()) {
|
||||
compiler.feature(course, segment, x, shape.fluidHead(), z, flowX, flowZ,
|
||||
source && deltaX == 0 && deltaZ == 0, channel, shore, grading,
|
||||
false, false, pointFeatures[pointIndex]);
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnLayer layer = compiler.regularLayer(
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
shape,
|
||||
terrain,
|
||||
distance,
|
||||
deltaX,
|
||||
deltaZ,
|
||||
flowX,
|
||||
flowZ,
|
||||
source,
|
||||
channel,
|
||||
shore,
|
||||
grading,
|
||||
false,
|
||||
receiving,
|
||||
pointFeatures[pointIndex]
|
||||
);
|
||||
layer = compiler.terrainContainedCaveLayer(course, segment, terrain, layer, x, z, plannedSurface);
|
||||
if (layer == null) {
|
||||
continue;
|
||||
}
|
||||
compiler.addLayer(columns, x, z, terrain, layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FootprintCenterlineProjection projectCenterline(List<HydrologyPoint> centerline, HydrologyPoint incoming, int x, int z) {
|
||||
if (centerline.size() == 1) {
|
||||
HydrologyPoint point = centerline.getFirst();
|
||||
return new FootprintCenterlineProjection(
|
||||
0,
|
||||
point.x(),
|
||||
point.z(),
|
||||
StrictMath.hypot(x - point.x(), z - point.z()),
|
||||
point.x() - incoming.x(),
|
||||
point.z() - incoming.z()
|
||||
);
|
||||
}
|
||||
int selectedPoint = 0;
|
||||
double selectedX = centerline.getFirst().x();
|
||||
double selectedZ = centerline.getFirst().z();
|
||||
double selectedDistanceSquared = Double.POSITIVE_INFINITY;
|
||||
int selectedFlowX = 1;
|
||||
int selectedFlowZ = 0;
|
||||
for (int segmentIndex = 0; segmentIndex < centerline.size() - 1; segmentIndex++) {
|
||||
HydrologyPoint start = centerline.get(segmentIndex);
|
||||
HydrologyPoint end = centerline.get(segmentIndex + 1);
|
||||
double flowX = end.x() - start.x();
|
||||
double flowZ = end.z() - start.z();
|
||||
double lengthSquared = flowX * flowX + flowZ * flowZ;
|
||||
if (lengthSquared == 0D) {
|
||||
continue;
|
||||
}
|
||||
double progress = ((x - start.x()) * flowX + (z - start.z()) * flowZ) / lengthSquared;
|
||||
progress = Math.max(0D, Math.min(1D, progress));
|
||||
double projectedX = start.x() + flowX * progress;
|
||||
double projectedZ = start.z() + flowZ * progress;
|
||||
double deltaX = x - projectedX;
|
||||
double deltaZ = z - projectedZ;
|
||||
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (distanceSquared >= selectedDistanceSquared) {
|
||||
continue;
|
||||
}
|
||||
selectedDistanceSquared = distanceSquared;
|
||||
selectedX = projectedX;
|
||||
selectedZ = projectedZ;
|
||||
selectedPoint = progress < 0.5D ? segmentIndex : segmentIndex + 1;
|
||||
selectedFlowX = end.x() - start.x();
|
||||
selectedFlowZ = end.z() - start.z();
|
||||
}
|
||||
int tangentReach = Math.max(2, compiler.settings.routing().refinementSpacing() * 2);
|
||||
HydrologyPoint tangentStart = centerline.get(Math.max(0, selectedPoint - tangentReach));
|
||||
HydrologyPoint tangentEnd = centerline.get(Math.min(centerline.size() - 1, selectedPoint + tangentReach));
|
||||
int tangentX = tangentEnd.x() - tangentStart.x();
|
||||
int tangentZ = tangentEnd.z() - tangentStart.z();
|
||||
if (tangentX != 0 || tangentZ != 0) {
|
||||
selectedFlowX = tangentX;
|
||||
selectedFlowZ = tangentZ;
|
||||
}
|
||||
return new FootprintCenterlineProjection(
|
||||
selectedPoint,
|
||||
selectedX,
|
||||
selectedZ,
|
||||
StrictMath.sqrt(selectedDistanceSquared),
|
||||
selectedFlowX,
|
||||
selectedFlowZ
|
||||
);
|
||||
}
|
||||
|
||||
boolean withinLongitudinalBounds(
|
||||
List<HydrologyPoint> centerline,
|
||||
HydrologyPoint incoming,
|
||||
int x,
|
||||
int z,
|
||||
boolean clipStart,
|
||||
boolean clipEnd
|
||||
) {
|
||||
if (centerline.size() < 2) {
|
||||
HydrologyPoint point = centerline.getFirst();
|
||||
long dot = (long) (x - point.x()) * (point.x() - incoming.x())
|
||||
+ (long) (z - point.z()) * (point.z() - incoming.z());
|
||||
return (!clipStart || dot >= 0L) && (!clipEnd || dot <= 0L);
|
||||
}
|
||||
HydrologyPoint start = centerline.getFirst();
|
||||
HydrologyPoint afterStart = centerline.get(1);
|
||||
long startDot = (long) (x - start.x()) * (afterStart.x() - start.x())
|
||||
+ (long) (z - start.z()) * (afterStart.z() - start.z());
|
||||
if (clipStart && startDot < 0L) {
|
||||
return false;
|
||||
}
|
||||
if (!clipEnd) {
|
||||
return true;
|
||||
}
|
||||
HydrologyPoint end = centerline.getLast();
|
||||
HydrologyPoint beforeEnd = centerline.get(centerline.size() - 2);
|
||||
long endDot = (long) (x - end.x()) * (end.x() - beforeEnd.x())
|
||||
+ (long) (z - end.z()) * (end.z() - beforeEnd.z());
|
||||
return endDot <= 0L;
|
||||
}
|
||||
|
||||
boolean segmentsJoin(HydraulicSegment upstream, HydraulicSegment downstream) {
|
||||
HydrologyPoint upstreamEnd = upstream.centerline().getLast();
|
||||
HydrologyPoint downstreamStart = downstream.centerline().getFirst();
|
||||
return upstreamEnd.x() == downstreamStart.x()
|
||||
&& upstreamEnd.y() == downstreamStart.y()
|
||||
&& upstreamEnd.z() == downstreamStart.z();
|
||||
}
|
||||
|
||||
List<HydrologyPoint> continuousCenterline(HydraulicSegment segment) {
|
||||
List<HydrologyPoint> configured = segment.centerline();
|
||||
if (configured.size() == 1) {
|
||||
return configured;
|
||||
}
|
||||
ArrayList<HydrologyPoint> continuous = new ArrayList<>();
|
||||
for (int pairIndex = 0; pairIndex < configured.size() - 1; pairIndex++) {
|
||||
HydrologyPoint start = configured.get(pairIndex);
|
||||
HydrologyPoint end = configured.get(pairIndex + 1);
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
if (steps == 0) {
|
||||
if (continuous.isEmpty()) {
|
||||
continuous.add(start);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int firstStep = continuous.isEmpty() ? 0 : 1;
|
||||
for (int step = firstStep; 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);
|
||||
int y = segment.fallingFluid() && !continuous.isEmpty()
|
||||
? segment.downstreamHeadY()
|
||||
: (int) StrictMath.round(start.y() + (end.y() - start.y()) * progress);
|
||||
HydrologyPoint previous = continuous.isEmpty() ? null : continuous.getLast();
|
||||
if (previous == null || previous.x() != x || previous.z() != z) {
|
||||
continuous.add(new HydrologyPoint(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(continuous);
|
||||
}
|
||||
|
||||
void rasterizePoint(
|
||||
Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns,
|
||||
RiverCourse course,
|
||||
HydraulicSegment segment,
|
||||
HydrologyPoint point,
|
||||
int pointIndex,
|
||||
FootprintLayerShape shape,
|
||||
int flowX,
|
||||
int flowZ,
|
||||
boolean source,
|
||||
boolean falling,
|
||||
boolean receiving,
|
||||
boolean oceanConnection,
|
||||
boolean validationOnly,
|
||||
HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface
|
||||
) {
|
||||
FootprintRasterStencil stencil = compiler.rasterStencil(shape);
|
||||
HydrologyFeatureRef[] pointFeatures = new HydrologyFeatureRef[HydrologyFootprintCompiler.FEATURE_ROLE_COUNT];
|
||||
boolean underground = segment.type().isUnderground() || segment.type().isDeepFluid();
|
||||
for (int offsetIndex = 0; offsetIndex < stencil.size(); offsetIndex++) {
|
||||
int deltaX = stencil.deltaXs()[offsetIndex];
|
||||
int deltaZ = stencil.deltaZs()[offsetIndex];
|
||||
int x = point.x() + deltaX;
|
||||
int z = point.z() + deltaZ;
|
||||
double channelDistance = compiler.channelGeometry.shapedDistance(
|
||||
shape,
|
||||
segment,
|
||||
x,
|
||||
z,
|
||||
deltaX,
|
||||
deltaZ,
|
||||
flowX,
|
||||
flowZ,
|
||||
stencil.distances()[offsetIndex]
|
||||
);
|
||||
boolean channel = channelDistance <= shape.channelRadius() + 0.25D;
|
||||
if (underground && !channel) {
|
||||
continue;
|
||||
}
|
||||
boolean shore = !channel
|
||||
&& stencil.distances()[offsetIndex] <= shape.channelRadius() + shape.shoreWidth() + 0.25D;
|
||||
boolean grading = !channel
|
||||
&& !shore
|
||||
&& stencil.distances()[offsetIndex] <= shape.totalRadius() + 0.25D;
|
||||
double distance = grading ? stencil.distances()[offsetIndex] : channelDistance;
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classification = compiler.classifyNatural(x, z);
|
||||
boolean apronEligible = compiler.oceanApronEligible(segment, distance);
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
&& !apronEligible) {
|
||||
continue;
|
||||
}
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.LAND
|
||||
&& oceanConnection) {
|
||||
continue;
|
||||
}
|
||||
boolean exactSlope = classification != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
&& !oceanConnection
|
||||
&& !channel
|
||||
&& segment.type() != HydrologyFeatureType.WATERFALL
|
||||
&& segment.type() != HydrologyFeatureType.CASCADE;
|
||||
HydrologyTerrainSample terrain = exactSlope
|
||||
? compiler.sampleTerrain(x, z)
|
||||
: compiler.sampleTerrainBasis(x, z);
|
||||
if (terrain == null || compiler.elevatedSeaLevelSurfaceColumn(segment, terrain, shape.fluidHead())) {
|
||||
continue;
|
||||
}
|
||||
boolean naturalOcean = classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN
|
||||
|| terrain.ocean()
|
||||
|| compiler.naturallySubmergedSurfaceColumn(segment, terrain);
|
||||
if (oceanConnection) {
|
||||
if (naturalOcean) {
|
||||
compiler.addOceanApron(
|
||||
columns,
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
x,
|
||||
z,
|
||||
distance,
|
||||
flowX,
|
||||
flowZ,
|
||||
terrain,
|
||||
pointFeatures
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (naturalOcean) {
|
||||
compiler.addOceanApron(
|
||||
columns,
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
x,
|
||||
z,
|
||||
distance,
|
||||
flowX,
|
||||
flowZ,
|
||||
terrain,
|
||||
pointFeatures
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (validationOnly && !underground && !segment.fallingFluid()) {
|
||||
compiler.feature(
|
||||
course,
|
||||
segment,
|
||||
x,
|
||||
shape.fluidHead(),
|
||||
z,
|
||||
flowX,
|
||||
flowZ,
|
||||
source && deltaX == 0 && deltaZ == 0,
|
||||
channel,
|
||||
shore,
|
||||
grading,
|
||||
channel && falling,
|
||||
channel && receiving,
|
||||
pointFeatures
|
||||
);
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnLayer layer = compiler.regularLayer(
|
||||
course,
|
||||
segment,
|
||||
point,
|
||||
shape,
|
||||
terrain,
|
||||
distance,
|
||||
deltaX,
|
||||
deltaZ,
|
||||
flowX,
|
||||
flowZ,
|
||||
source,
|
||||
channel,
|
||||
shore,
|
||||
grading,
|
||||
falling,
|
||||
receiving,
|
||||
pointFeatures
|
||||
);
|
||||
layer = compiler.terrainContainedCaveLayer(course, segment, terrain, layer, x, z, plannedSurface);
|
||||
if (layer == null) {
|
||||
continue;
|
||||
}
|
||||
compiler.addLayer(columns, x, z, terrain, layer);
|
||||
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
compiler.addAdjacentSeaApron(columns, course, segment, point, x, z, flowX, flowZ, pointFeatures);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Runs independent planning tasks and returns results in request order, using the current pool or fallback.
|
||||
* A worker claims its own queued tasks before waiting, and never helps unrelated work while holding
|
||||
* an owner draft. Every started task finishes before the batch returns or throws.
|
||||
*/
|
||||
public final class HydrologyForkJoin {
|
||||
private HydrologyForkJoin() {
|
||||
}
|
||||
|
||||
public static <T> List<T> invokeAll(List<Callable<T>> tasks, Executor fallback) {
|
||||
Objects.requireNonNull(tasks, "tasks");
|
||||
boolean callerRuns = Thread.currentThread() instanceof ForkJoinWorkerThread;
|
||||
if (tasks.size() < 2 || fallback == null && !callerRuns) {
|
||||
ArrayList<T> results = new ArrayList<>(tasks.size());
|
||||
for (Callable<T> task : tasks) {
|
||||
results.add(call(task));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
Executor executor = callerRuns ? ((ForkJoinWorkerThread) Thread.currentThread()).getPool() : fallback;
|
||||
ArrayList<Task<T>> pending = new ArrayList<>(tasks.size());
|
||||
for (Callable<T> task : tasks) {
|
||||
pending.add(new Task<>(task));
|
||||
}
|
||||
Throwable failure = null;
|
||||
int submitted = callerRuns ? 1 : 0;
|
||||
for (int index = callerRuns ? 1 : 0; index < pending.size(); index++) {
|
||||
try {
|
||||
executor.execute(pending.get(index));
|
||||
submitted = index + 1;
|
||||
} catch (RuntimeException | Error rejected) {
|
||||
failure = rejected;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ArrayList<T> results = new ArrayList<>(tasks.size());
|
||||
int drainCount = callerRuns ? pending.size() : submitted;
|
||||
for (int index = 0; index < drainCount; index++) {
|
||||
try {
|
||||
results.add(pending.get(index).await(callerRuns));
|
||||
} catch (RuntimeException | Error failed) {
|
||||
if (failure == null) {
|
||||
failure = failed;
|
||||
} else if (failure != failed) {
|
||||
failure.addSuppressed(failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
throw unwrap(failure);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static <T> T call(Callable<T> task) {
|
||||
try {
|
||||
return task.call();
|
||||
} catch (RuntimeException | Error failure) {
|
||||
throw failure;
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Hydrology planning task failed.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static RuntimeException unwrap(Throwable cause) {
|
||||
if (cause instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (cause instanceof RuntimeException runtime) {
|
||||
return runtime;
|
||||
}
|
||||
return new IllegalStateException("Hydrology planning task failed.", cause);
|
||||
}
|
||||
|
||||
static final class Task<T> implements Runnable, ForkJoinPool.ManagedBlocker {
|
||||
private final Callable<T> work;
|
||||
private final AtomicBoolean claimed = new AtomicBoolean();
|
||||
private final CountDownLatch complete = new CountDownLatch(1);
|
||||
private T result;
|
||||
private Throwable failure;
|
||||
private boolean provisional;
|
||||
|
||||
Task(Callable<T> work) {
|
||||
this.work = Objects.requireNonNull(work);
|
||||
}
|
||||
|
||||
T await() {
|
||||
return await(true);
|
||||
}
|
||||
|
||||
private T await(boolean claimPending) {
|
||||
if (claimPending) {
|
||||
run();
|
||||
}
|
||||
boolean interrupted = false;
|
||||
boolean managed = true;
|
||||
Throwable blockingFailure = null;
|
||||
while (!isReleasable()) {
|
||||
try {
|
||||
if (managed) {
|
||||
ForkJoinPool.managedBlock(this);
|
||||
} else {
|
||||
complete.await();
|
||||
}
|
||||
} catch (InterruptedException interruption) {
|
||||
interrupted = true;
|
||||
managed = false;
|
||||
} catch (RejectedExecutionException unavailable) {
|
||||
managed = false;
|
||||
} catch (RuntimeException | Error failed) {
|
||||
blockingFailure = failed;
|
||||
managed = false;
|
||||
}
|
||||
}
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
if (blockingFailure != null) {
|
||||
if (failure != null && failure != blockingFailure) {
|
||||
blockingFailure.addSuppressed(failure);
|
||||
}
|
||||
throw unwrap(blockingFailure);
|
||||
}
|
||||
if (failure != null) {
|
||||
throw unwrap(failure);
|
||||
}
|
||||
if (provisional) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
return complete.getCount() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() throws InterruptedException {
|
||||
complete.await();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (!claimed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
result = ProvisionalSampling.memoizable(() -> call(work));
|
||||
} catch (ProvisionalSampling.Unmemoizable value) {
|
||||
result = value.value();
|
||||
provisional = true;
|
||||
} catch (RuntimeException | Error failed) {
|
||||
failure = failed;
|
||||
} finally {
|
||||
complete.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface HydrologyGeometrySampler {
|
||||
int sample(Request request);
|
||||
|
||||
default int sample(
|
||||
Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
return sample(field, profileKey, x, z, 0L, minimum, maximum);
|
||||
}
|
||||
|
||||
default int sample(
|
||||
Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
long stableId,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
Request request = new Request(field, profileKey, x, z, stableId, minimum, maximum);
|
||||
int value = sample(request);
|
||||
if (value < minimum || value > maximum) {
|
||||
throw new IllegalStateException(
|
||||
"Hydrology geometry sampler returned " + value + " outside " + minimum + ".." + maximum
|
||||
+ " for " + field + " at " + x + "," + z + "."
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static HydrologyGeometrySampler deterministic(HydrologyTerrainSampler terrainSampler) {
|
||||
Objects.requireNonNull(terrainSampler, "terrainSampler");
|
||||
return request -> {
|
||||
if (request.field() == Field.UNDERGROUND_FLUID_LEVEL) {
|
||||
HydrologyTerrainSample terrain = Objects.requireNonNull(
|
||||
terrainSampler.sample(request.x(), request.z()),
|
||||
"Hydrology terrain sampler returned null at " + request.x() + "," + request.z()
|
||||
);
|
||||
return Math.max(request.minimum(), Math.min(request.maximum(), terrain.caveFluidY()));
|
||||
}
|
||||
if (request.field() == Field.SURFACE_WIDTH
|
||||
|| request.field() == Field.SURFACE_DEPTH
|
||||
|| request.field() == Field.UNDERGROUND_WIDTH
|
||||
|| request.field() == Field.UNDERGROUND_DEPTH) {
|
||||
return request.minimum();
|
||||
}
|
||||
long stable = switch (request.field()) {
|
||||
case SURFACE_BLEND_WIDTH -> HydrologyHash.mix(
|
||||
request.stableId(),
|
||||
request.x(),
|
||||
request.z(),
|
||||
0x424c454e44L
|
||||
);
|
||||
case UNDERGROUND_HEADROOM -> HydrologyHash.mix(
|
||||
request.stableId(),
|
||||
request.x(),
|
||||
request.z(),
|
||||
0x48454144L
|
||||
);
|
||||
case DEEP_FLUID_HEIGHT -> HydrologyHash.mix(request.stableId(), 0x484549474854L);
|
||||
default -> throw new IllegalStateException("Missing deterministic geometry rule for " + request.field() + ".");
|
||||
};
|
||||
return HydrologyHash.between(stable, request.minimum(), request.maximum());
|
||||
};
|
||||
}
|
||||
|
||||
enum Field {
|
||||
SURFACE_WIDTH,
|
||||
SURFACE_DEPTH,
|
||||
SURFACE_BLEND_WIDTH,
|
||||
UNDERGROUND_FLUID_LEVEL,
|
||||
UNDERGROUND_WIDTH,
|
||||
UNDERGROUND_DEPTH,
|
||||
UNDERGROUND_HEADROOM,
|
||||
DEEP_FLUID_HEIGHT
|
||||
}
|
||||
|
||||
record Request(
|
||||
Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
long stableId,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
public Request {
|
||||
field = Objects.requireNonNull(field, "field");
|
||||
profileKey = profileKey == null ? "" : profileKey;
|
||||
if (minimum > maximum) {
|
||||
throw new IllegalArgumentException("Hydrology geometry sample bounds are reversed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package art.arcane.iris.generation.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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
record HydrologyGridOffset(int x, int z) {
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public final class HydrologyHash {
|
||||
private HydrologyHash() {
|
||||
}
|
||||
|
||||
public static long mix(long seed, long... values) {
|
||||
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
|
||||
for (long value : values) {
|
||||
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
|
||||
}
|
||||
return mixed;
|
||||
}
|
||||
|
||||
public static long text(String value) {
|
||||
long hash = 0xcbf29ce484222325L;
|
||||
for (int index = 0; index < value.length(); index++) {
|
||||
hash ^= value.charAt(index);
|
||||
hash *= 0x100000001b3L;
|
||||
}
|
||||
return avalanche(hash);
|
||||
}
|
||||
|
||||
public static double unit(long value) {
|
||||
return (avalanche(value) >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
|
||||
public static int between(long value, int minimum, int maximum) {
|
||||
if (minimum > maximum) {
|
||||
throw new IllegalArgumentException("minimum cannot exceed maximum.");
|
||||
}
|
||||
long span = (long) maximum - minimum + 1L;
|
||||
return minimum + (int) Long.remainderUnsigned(avalanche(value), span);
|
||||
}
|
||||
|
||||
private static long avalanche(long value) {
|
||||
value ^= value >>> 30;
|
||||
value *= 0xbf58476d1ce4e5b9L;
|
||||
value ^= value >>> 27;
|
||||
value *= 0x94d049bb133111ebL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleMap;
|
||||
|
||||
final class HydrologyLandHeightCache {
|
||||
private static final int MAXIMUM_ENTRIES = 131_072;
|
||||
private static final int MAXIMUM_STRIPES = 64;
|
||||
private final Stripe[] stripes;
|
||||
|
||||
HydrologyLandHeightCache() {
|
||||
this(MAXIMUM_ENTRIES);
|
||||
}
|
||||
|
||||
HydrologyLandHeightCache(int maximumEntries) {
|
||||
if (maximumEntries < 1 || Integer.bitCount(maximumEntries) != 1) {
|
||||
throw new IllegalArgumentException("Land height cache size must be a positive power of two.");
|
||||
}
|
||||
int stripeCount = Math.min(MAXIMUM_STRIPES, maximumEntries);
|
||||
stripes = new Stripe[stripeCount];
|
||||
for (int index = 0; index < stripeCount; index++) {
|
||||
stripes[index] = new Stripe(maximumEntries / stripeCount);
|
||||
}
|
||||
}
|
||||
|
||||
double sample(int blockX, int blockZ, HydrologyNaturalTerrainSampler sampler) {
|
||||
long packed = RiverFootprint.pack(blockX, blockZ);
|
||||
Stripe stripe = stripe(packed);
|
||||
synchronized (stripe) {
|
||||
double cached = stripe.heights.getAndMoveToFirst(packed);
|
||||
if (!Double.isNaN(cached) || stripe.heights.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
double sampled = sampler.sampleLandHeight(blockX, blockZ);
|
||||
stripe.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
}
|
||||
|
||||
void seed(Long2DoubleMap samples) {
|
||||
for (Long2DoubleMap.Entry entry : samples.long2DoubleEntrySet()) {
|
||||
Stripe stripe = stripe(entry.getLongKey());
|
||||
synchronized (stripe) {
|
||||
stripe.put(entry.getLongKey(), entry.getDoubleValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stripe stripe(long packed) {
|
||||
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
|
||||
}
|
||||
|
||||
private static final class Stripe {
|
||||
private final int maximumEntries;
|
||||
private final Long2DoubleLinkedOpenHashMap heights;
|
||||
|
||||
private Stripe(int maximumEntries) {
|
||||
this.maximumEntries = maximumEntries;
|
||||
heights = new Long2DoubleLinkedOpenHashMap(maximumEntries);
|
||||
heights.defaultReturnValue(Double.NaN);
|
||||
}
|
||||
|
||||
private void put(long packed, double height) {
|
||||
if (heights.size() >= maximumEntries && !heights.containsKey(packed)) {
|
||||
heights.removeLastDouble();
|
||||
}
|
||||
heights.putAndMoveToFirst(packed, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainSampler {
|
||||
HydrologyTerrainSample sampleBasis(int blockX, int blockZ);
|
||||
|
||||
default HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
|
||||
return sampleBasis(blockX, blockZ);
|
||||
}
|
||||
|
||||
default double sampleLandHeight(int blockX, int blockZ) {
|
||||
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(blockX, blockZ);
|
||||
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
|
||||
}
|
||||
|
||||
default HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
for (int index = 0; index < count; index++) {
|
||||
samples[index] = sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class HydrologyObservedPlannedSurface implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private final HydrologyCaveVoxelViewFactory.PlannedSurface delegate;
|
||||
private final Long2ObjectOpenHashMap<ArrayList<Observation>> observations;
|
||||
|
||||
public HydrologyObservedPlannedSurface(HydrologyCaveVoxelViewFactory.PlannedSurface delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
this.observations = new Long2ObjectOpenHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
int resolvedHeight = delegate.resolve(x, z, naturalHeight);
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
ArrayList<Observation> column = observations.get(key);
|
||||
if (column == null) {
|
||||
column = new ArrayList<>(1);
|
||||
observations.put(key, column);
|
||||
}
|
||||
for (Observation observation : column) {
|
||||
if (observation instanceof HeightObservation height && height.naturalHeight() == naturalHeight) {
|
||||
return resolvedHeight;
|
||||
}
|
||||
}
|
||||
column.add(new HeightObservation(x, z, naturalHeight, resolvedHeight));
|
||||
return resolvedHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
boolean owned = delegate.ownsTerrain(x, z);
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
ArrayList<Observation> column = observations.get(key);
|
||||
if (column == null) {
|
||||
column = new ArrayList<>(1);
|
||||
observations.put(key, column);
|
||||
}
|
||||
for (Observation observation : column) {
|
||||
if (observation instanceof OwnershipObservation) {
|
||||
return owned;
|
||||
}
|
||||
}
|
||||
column.add(new OwnershipObservation(x, z, owned));
|
||||
return owned;
|
||||
}
|
||||
|
||||
public List<Observation> observationsAt(int x, int z) {
|
||||
ArrayList<Observation> column = observations.get(RiverFootprint.pack(x, z));
|
||||
return column == null ? List.of() : List.copyOf(column);
|
||||
}
|
||||
|
||||
public sealed interface Observation permits HeightObservation, OwnershipObservation {
|
||||
boolean matches(HydrologyCaveVoxelViewFactory.PlannedSurface surface);
|
||||
}
|
||||
|
||||
public record HeightObservation(
|
||||
int x,
|
||||
int z,
|
||||
int naturalHeight,
|
||||
int resolvedHeight
|
||||
) implements Observation {
|
||||
@Override
|
||||
public boolean matches(HydrologyCaveVoxelViewFactory.PlannedSurface surface) {
|
||||
return surface.resolve(x, z, naturalHeight) == resolvedHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public record OwnershipObservation(int x, int z, boolean terrainOwned) implements Observation {
|
||||
@Override
|
||||
public boolean matches(HydrologyCaveVoxelViewFactory.PlannedSurface surface) {
|
||||
return surface.ownsTerrain(x, z) == terrainOwned;
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyOceanBoundaryRefiner {
|
||||
private HydrologyOceanBoundaryRefiner() {
|
||||
}
|
||||
|
||||
static Result refine(
|
||||
List<HydrologyPoint> crossing,
|
||||
HydrologyTerrainSampler terrainSampler,
|
||||
HydrologyRoutingTerrainSampler routingSampler,
|
||||
int seaLevel
|
||||
) {
|
||||
Objects.requireNonNull(crossing, "crossing");
|
||||
Objects.requireNonNull(terrainSampler, "terrainSampler");
|
||||
Objects.requireNonNull(routingSampler, "routingSampler");
|
||||
if (crossing.size() < 2) {
|
||||
return null;
|
||||
}
|
||||
for (int index = 1; index < crossing.size(); index++) {
|
||||
HydrologyPoint point = crossing.get(index);
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classification =
|
||||
routingSampler.classifyNatural(point.x(), point.z());
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE) {
|
||||
return null;
|
||||
}
|
||||
if (classification == HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN) {
|
||||
for (int landward = index - 1; landward >= 0; landward--) {
|
||||
HydrologyPoint previous = crossing.get(landward);
|
||||
HydrologyTerrainSample landwardTerrain = terrainSampler.sample(previous.x(), previous.z());
|
||||
if (landwardTerrain == null || landwardTerrain.ocean()) {
|
||||
return null;
|
||||
}
|
||||
if (landwardTerrain.naturalHeight() >= seaLevel) {
|
||||
return new Result(previous, point, landwardTerrain);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
record Result(
|
||||
HydrologyPoint landwardPoint,
|
||||
HydrologyPoint oceanPoint,
|
||||
HydrologyTerrainSample landwardTerrain
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class HydrologyOceanReceiver implements HydrologyTerrainSampler {
|
||||
private static final int MAXIMUM_SAMPLES = 65_536;
|
||||
private static final int[][] CARDINALS = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}};
|
||||
|
||||
private final HydrologyTerrainSampler source;
|
||||
private final LongOpenHashSet receiving;
|
||||
private final int seaLevel;
|
||||
|
||||
private HydrologyOceanReceiver(HydrologyTerrainSampler source, LongOpenHashSet receiving, int seaLevel) {
|
||||
this.source = source;
|
||||
this.receiving = receiving;
|
||||
this.seaLevel = seaLevel;
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forOutlet(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, RiverOutlet outlet) {
|
||||
if (!outlet.directOcean() || outlet.type() != HydrologyFeatureType.MOUTH) {
|
||||
return source;
|
||||
}
|
||||
return forConnection(settings, source, outlet.landwardPoint(), outlet.connectionPoint());
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forConnection(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source,
|
||||
HydrologyPoint landward, HydrologyPoint receiving) {
|
||||
HydrologyTerrainSample shore = source.sample(landward.x(), landward.z());
|
||||
if (shore == null || shore.ocean() || shore.naturalHeight() < settings.seaLevel()) {
|
||||
return source;
|
||||
}
|
||||
double width = settings.surface().maximumWidth() * shore.widthMultiplier()
|
||||
* settings.surface().banks().mouthFlareRatio();
|
||||
return create(settings, source, List.of(new Mouth(
|
||||
List.of(landward, receiving), width)));
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forCourse(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, RiverCourse course) {
|
||||
if (course.type() != RiverCourseType.SURFACE || course.segments().isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
ArrayList<Mouth> mouths = new ArrayList<>(2);
|
||||
for (int index = 0; index < course.segments().size(); index++) {
|
||||
if (index != 0 && index != course.segments().size() - 1) {
|
||||
continue;
|
||||
}
|
||||
Mouth mouth = mouth(settings, source, course.segments().get(index));
|
||||
if (mouth != null) {
|
||||
mouths.add(mouth);
|
||||
}
|
||||
}
|
||||
return create(settings, source, mouths);
|
||||
}
|
||||
|
||||
public static HydrologyTerrainSampler forMouth(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, HydraulicSegment segment) {
|
||||
Mouth mouth = mouth(settings, source, segment);
|
||||
return mouth == null ? source : create(settings, source, List.of(mouth));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sample(int blockX, int blockZ) {
|
||||
return source.sample(blockX, blockZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBatch(long[] coordinates, int count) {
|
||||
return source.sampleBatch(coordinates, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receivingWater(int blockX, int blockZ, int queriedSeaLevel) {
|
||||
return queriedSeaLevel == seaLevel && receiving.contains(RiverFootprint.pack(blockX, blockZ))
|
||||
|| source.receivingWater(blockX, blockZ, queriedSeaLevel);
|
||||
}
|
||||
|
||||
private static Mouth mouth(HydrologyPlannerSettings settings, HydrologyTerrainSampler source,
|
||||
HydraulicSegment segment) {
|
||||
if (segment.type() != HydrologyFeatureType.MOUTH || segment.upstreamHeadY() != settings.seaLevel()
|
||||
|| segment.downstreamHeadY() != settings.seaLevel()) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>(segment.centerline());
|
||||
HydrologyPoint first = points.getFirst();
|
||||
HydrologyTerrainSample terrain = source.sample(first.x(), first.z());
|
||||
if (terrain != null && terrain.ocean() && terrain.naturalHeight() < settings.seaLevel()) {
|
||||
Collections.reverse(points);
|
||||
}
|
||||
double width = segment.width();
|
||||
for (int station = 0; station < segment.centerline().size(); station++) {
|
||||
width = Math.max(width, segment.channelProfile().widthAt(station));
|
||||
}
|
||||
return new Mouth(List.copyOf(points), width);
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSampler create(HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler source, List<Mouth> mouths) {
|
||||
if (mouths.isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
Samples samples = new Samples(source);
|
||||
LongOpenHashSet receiving = new LongOpenHashSet();
|
||||
for (Mouth mouth : mouths) {
|
||||
LongArrayList run = submergedRun(mouth.points(), samples, settings.seaLevel());
|
||||
if (run.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
receiving.addAll(run);
|
||||
double radius = mouth.width() * settings.surface().banks().channel().outlineMaximumRatio() / 2D
|
||||
+ settings.outlets().maximumOceanApron() + 2D;
|
||||
extendHalo(receiving, run, samples, settings.seaLevel(), radius);
|
||||
}
|
||||
return receiving.isEmpty() ? source : new HydrologyOceanReceiver(source, receiving, settings.seaLevel());
|
||||
}
|
||||
|
||||
private static LongArrayList submergedRun(List<HydrologyPoint> points, Samples samples, int seaLevel) {
|
||||
LongArrayList run = new LongArrayList();
|
||||
if (points.size() < 2) {
|
||||
return run;
|
||||
}
|
||||
HydrologyPoint endpoint = points.getLast();
|
||||
HydrologyTerrainSample ocean = samples.sample(endpoint.x(), endpoint.z());
|
||||
if (ocean == null || !ocean.ocean() || ocean.naturalHeight() >= seaLevel) {
|
||||
return run;
|
||||
}
|
||||
long previous = RiverFootprint.pack(points.getFirst().x(), points.getFirst().z());
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
long steps = Math.max(Math.abs((long) second.x() - first.x()), Math.abs((long) second.z() - first.z()));
|
||||
if (steps > MAXIMUM_SAMPLES) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0L ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + ((long) second.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + ((long) second.z() - first.z()) * progress);
|
||||
HydrologyTerrainSample terrain = samples.sample(x, z);
|
||||
if (terrain == null) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (terrain.naturalHeight() >= seaLevel) {
|
||||
if (!run.isEmpty()) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
} else {
|
||||
if (!run.isEmpty() && !cardinalBridge(run, previous, key, samples, seaLevel)) {
|
||||
return new LongArrayList();
|
||||
}
|
||||
if (run.isEmpty() || run.getLong(run.size() - 1) != key) {
|
||||
run.add(key);
|
||||
}
|
||||
}
|
||||
previous = key;
|
||||
}
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
private static boolean cardinalBridge(LongArrayList run, long previous, long next,
|
||||
Samples samples, int seaLevel) {
|
||||
int previousX = RiverFootprint.unpackX(previous);
|
||||
int previousZ = RiverFootprint.unpackZ(previous);
|
||||
int nextX = RiverFootprint.unpackX(next);
|
||||
int nextZ = RiverFootprint.unpackZ(next);
|
||||
if (previousX == nextX || previousZ == nextZ) {
|
||||
return true;
|
||||
}
|
||||
long first = RiverFootprint.pack(previousX, nextZ);
|
||||
long second = RiverFootprint.pack(nextX, previousZ);
|
||||
for (long bridge : new long[] {Math.min(first, second), Math.max(first, second)}) {
|
||||
HydrologyTerrainSample terrain = samples.sample(RiverFootprint.unpackX(bridge), RiverFootprint.unpackZ(bridge));
|
||||
if (terrain != null && terrain.naturalHeight() < seaLevel) {
|
||||
run.add(bridge);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void extendHalo(LongOpenHashSet receiving, LongArrayList run, Samples samples,
|
||||
int seaLevel, double radius) {
|
||||
long first = run.getLong(0);
|
||||
int anchorX = RiverFootprint.unpackX(first);
|
||||
int anchorZ = RiverFootprint.unpackZ(first);
|
||||
double squaredRadius = radius * radius;
|
||||
LongOpenHashSet visited = new LongOpenHashSet();
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
for (int index = 0; index < run.size(); index++) {
|
||||
long key = run.getLong(index);
|
||||
double dx = (double) RiverFootprint.unpackX(key) - anchorX;
|
||||
double dz = (double) RiverFootprint.unpackZ(key) - anchorZ;
|
||||
if (dx * dx + dz * dz <= squaredRadius && visited.add(key)) {
|
||||
queue.enqueue(key);
|
||||
}
|
||||
}
|
||||
while (!queue.isEmpty() && samples.available()) {
|
||||
long key = queue.dequeueLong();
|
||||
int x = RiverFootprint.unpackX(key);
|
||||
int z = RiverFootprint.unpackZ(key);
|
||||
for (int[] offset : CARDINALS) {
|
||||
long nextX = (long) x + offset[0];
|
||||
long nextZ = (long) z + offset[1];
|
||||
double dx = nextX - anchorX;
|
||||
double dz = nextZ - anchorZ;
|
||||
if (nextX < Integer.MIN_VALUE || nextX > Integer.MAX_VALUE
|
||||
|| nextZ < Integer.MIN_VALUE || nextZ > Integer.MAX_VALUE
|
||||
|| dx * dx + dz * dz > squaredRadius) {
|
||||
continue;
|
||||
}
|
||||
long next = RiverFootprint.pack((int) nextX, (int) nextZ);
|
||||
if (!visited.add(next)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample terrain = samples.sample((int) nextX, (int) nextZ);
|
||||
if (terrain != null && terrain.naturalHeight() < seaLevel) {
|
||||
receiving.add(next);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Mouth(List<HydrologyPoint> points, double width) {
|
||||
}
|
||||
|
||||
private static final class Samples {
|
||||
private final HydrologyTerrainSampler source;
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> samples = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
private Samples(HydrologyTerrainSampler source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (samples.containsKey(key)) {
|
||||
return samples.get(key);
|
||||
}
|
||||
if (!available()) {
|
||||
return null;
|
||||
}
|
||||
HydrologyTerrainSample terrain = source.sample(x, z);
|
||||
samples.put(key, terrain);
|
||||
return terrain;
|
||||
}
|
||||
|
||||
private boolean available() {
|
||||
return samples.size() < MAXIMUM_SAMPLES;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,789 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
final class HydrologyOutletPlanner {
|
||||
private static final int REGIONAL_SHORELINE_WINDOW = 4;
|
||||
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 = surface
|
||||
? limitSurfaceOutlets(key, grid, oceanCandidates, true)
|
||||
: limitOutletsByType(oceanCandidates, planner.settings.outlets().maximumCoastalPerTile());
|
||||
addRejectedOutlets(oceanCandidates, selectedOcean, diagnostics);
|
||||
if (!planner.settings.outlets().inlandGrotto().enabled()) {
|
||||
return selectedOcean;
|
||||
}
|
||||
if (surface) {
|
||||
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
|
||||
if (budgets.overridden()) {
|
||||
oceanCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, true));
|
||||
}
|
||||
}
|
||||
boolean[] oceanReachable = outletReachable(grid, oceanCandidates, surface);
|
||||
ArrayList<OutletCandidate> inlandCandidates = inlandOutletCandidates(grid, oceanReachable);
|
||||
sortInlandOutletCandidates(grid, inlandCandidates);
|
||||
List<OutletCandidate> selectedInland = styledInlandOutlets(
|
||||
grid,
|
||||
surface ? limitSurfaceOutlets(key, grid, inlandCandidates, false)
|
||||
: 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()) {
|
||||
Shoreline shoreline = shoreline(grid, land, surface);
|
||||
if (shoreline == null) {
|
||||
continue;
|
||||
}
|
||||
OutletCandidate candidate = oceanOutletCandidate(shoreline.land(), shoreline.ocean(), surface);
|
||||
if (candidate != null) {
|
||||
oceanCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
return oceanCandidates;
|
||||
}
|
||||
|
||||
ArrayList<OutletCandidate> regionalOceanOutletCandidates(HydrologySampledGrid grid) {
|
||||
ArrayList<OutletCandidate> oceanCandidates = new ArrayList<>();
|
||||
int index = 0;
|
||||
while (index < grid.nodes().size()) {
|
||||
ArrayList<Callable<OutletCandidate>> tasks = new ArrayList<>(REGIONAL_SHORELINE_WINDOW);
|
||||
while (index < grid.nodes().size() && tasks.size() < REGIONAL_SHORELINE_WINDOW) {
|
||||
Shoreline shoreline = shoreline(grid, grid.nodes().get(index++), true);
|
||||
if (shoreline != null) {
|
||||
tasks.add(() -> regionalOceanOutletCandidate(shoreline));
|
||||
}
|
||||
}
|
||||
for (OutletCandidate candidate : HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null)) {
|
||||
if (candidate != null) {
|
||||
oceanCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return oceanCandidates;
|
||||
}
|
||||
|
||||
private OutletCandidate regionalOceanOutletCandidate(Shoreline shoreline) {
|
||||
HydrologyPlanner.PlanningSamples previous = planner.planningSamples.get();
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
return oceanOutletCandidate(shoreline.land(), shoreline.ocean(), true);
|
||||
} finally {
|
||||
if (previous == null) {
|
||||
planner.planningSamples.remove();
|
||||
} else {
|
||||
planner.planningSamples.set(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Shoreline shoreline(HydrologySampledGrid grid, HydrologyGridNode land, boolean surface) {
|
||||
if (land.terrain().ocean() || !land.terrain().transitAllowed() || !land.terrain().outletAllowed()) {
|
||||
return null;
|
||||
}
|
||||
HydrologyGridNode ocean = firstOceanNeighbor(grid, land, surface);
|
||||
if (ocean == null || !land.terrain().drainsInto(ocean.terrain())) {
|
||||
// A confined shore only reaches a sea that belongs to its own area.
|
||||
return null;
|
||||
}
|
||||
return new Shoreline(land, ocean);
|
||||
}
|
||||
|
||||
private OutletCandidate oceanOutletCandidate(HydrologyGridNode land, HydrologyGridNode ocean, boolean surface) {
|
||||
HydrologyOceanBoundaryRefiner.Result boundary = refineOceanBoundary(land, ocean);
|
||||
if (boundary == null) {
|
||||
return null;
|
||||
}
|
||||
HydrologyTerrainSample receiving = planner.sampleBasisWithoutSlope(boundary.oceanPoint().x(), boundary.oceanPoint().z());
|
||||
if (receiving == null || !boundary.landwardTerrain().drainsInto(receiving)
|
||||
|| !boundary.landwardTerrain().transitAllowed()
|
||||
|| !boundary.landwardTerrain().outletAllowed()
|
||||
|| surface && !HydrologySurfaceProfiles.sharesProfile(boundary.landwardTerrain(), receiving)) {
|
||||
return null;
|
||||
}
|
||||
HydrologyFeatureType type = coastalOutletType(boundary.landwardTerrain());
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
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
|
||||
);
|
||||
return new OutletCandidate(land.index(), ocean.index(), outlet);
|
||||
}
|
||||
|
||||
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);
|
||||
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
|
||||
if (budgets.overridden()) {
|
||||
HydrologyTileKey key = HydrologyTileKey.fromBlock(grid.ownerMinimumX(), grid.ownerMinimumZ(), grid.ownerSize());
|
||||
oceanCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, true));
|
||||
inlandCandidates.removeIf(candidate -> !surfaceOutletHasBudget(key, grid, budgets, candidate, false));
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
private boolean surfaceOutletHasBudget(HydrologyTileKey key, HydrologySampledGrid grid,
|
||||
HydrologySurfaceBudgets budgets, OutletCandidate candidate, boolean coastal) {
|
||||
HydrologySurfaceBudgets.Area area = budgets.area(grid.node(candidate.landIndex()).terrain().surfacePolicy());
|
||||
return area != null && budgets.outletTarget(area, coastal, planner, key) > 0;
|
||||
}
|
||||
|
||||
List<OutletCandidate> limitSurfaceOutlets(HydrologyTileKey key, HydrologySampledGrid grid,
|
||||
List<OutletCandidate> candidates, boolean coastal) {
|
||||
HydrologySurfaceBudgets budgets = grid.surfaceBudgets(planner.settings.surface().sources());
|
||||
if (!budgets.overridden()) {
|
||||
return coastal ? limitOutletsByType(candidates, planner.settings.outlets().maximumCoastalPerTile())
|
||||
: limitOutlets(candidates, planner.settings.outlets().maximumPerTile());
|
||||
}
|
||||
LinkedHashMap<SurfaceRiverPolicy.Budget, ArrayList<OutletCandidate>> grouped = new LinkedHashMap<>();
|
||||
for (OutletCandidate candidate : candidates) {
|
||||
SurfaceRiverPolicy policy = grid.node(candidate.landIndex()).terrain().surfacePolicy();
|
||||
if (budgets.area(policy) != null) {
|
||||
grouped.computeIfAbsent(policy.budget(), ignored -> new ArrayList<>()).add(candidate);
|
||||
}
|
||||
}
|
||||
ArrayList<OutletCandidate> selected = new ArrayList<>();
|
||||
long spacingSquared = outletSpacingSquared();
|
||||
for (Map.Entry<SurfaceRiverPolicy.Budget, ArrayList<OutletCandidate>> entry : grouped.entrySet()) {
|
||||
int target = budgets.outletTarget(budgets.area(entry.getKey()), coastal, planner, key);
|
||||
List<OutletCandidate> local = coastal ? limitOutletsByType(entry.getValue(), target)
|
||||
: limitOutlets(entry.getValue(), target);
|
||||
for (OutletCandidate candidate : local) {
|
||||
if (!withinOutletSpacing(candidate, selected, spacingSquared)) {
|
||||
selected.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(selected);
|
||||
}
|
||||
|
||||
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) {
|
||||
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()
|
||||
|| surface && !HydrologySurfaceProfiles.sharesProfile(neighbor.terrain(), node.terrain())
|
||||
|| surface && node.terrain().naturalHeight() - neighbor.terrain().naturalHeight()
|
||||
> planner.sourcePlanner.maximumSurfaceEdgeRise(node.terrain())) {
|
||||
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, surface);
|
||||
HydrologyTerrainSample landwardTerrain = Objects.requireNonNull(
|
||||
planner.sampleDetailed(landward.x(), landward.z()),
|
||||
"Hydrology inland outlet anchor left sampled terrain"
|
||||
);
|
||||
if (surface && (!HydrologySurfaceProfiles.sharesProfile(node.terrain(), landwardTerrain)
|
||||
|| !node.terrain().surfacePolicy().areaKey().equals(landwardTerrain.surfacePolicy().areaKey()))) {
|
||||
continue;
|
||||
}
|
||||
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, boolean surface) {
|
||||
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()
|
||||
&& (!surface || HydrologySurfaceProfiles.sharesProfile(land.terrain(), neighbor.terrain()))) {
|
||||
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, planner.settings.seaLevel());
|
||||
}
|
||||
|
||||
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.sourcePlanner.minimumCourseLength(downstream.terrain(), true)) {
|
||||
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()
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(upstream.terrain(), downstream.terrain())) {
|
||||
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) {
|
||||
}
|
||||
|
||||
private record Shoreline(HydrologyGridNode land, HydrologyGridNode ocean) {
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record HydrologyOwnerDraft(
|
||||
HydrologyTileKey key,
|
||||
HydrologyCaveCourseFilter.Result result,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
HydrologyFootprintCompiler footprintCompiler
|
||||
) {
|
||||
HydrologyOwnerDraft {
|
||||
Objects.requireNonNull(key, "key");
|
||||
Objects.requireNonNull(result, "result");
|
||||
result = new HydrologyCaveCourseFilter.Result(List.copyOf(result.nodes()), List.copyOf(result.edges()),
|
||||
List.copyOf(result.outlets()), List.copyOf(result.courses()), List.copyOf(result.cavePlans()));
|
||||
diagnostics = List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
HydrologyOwnerDraft withoutFootprintCompiler() {
|
||||
if (footprintCompiler == null) {
|
||||
return this;
|
||||
}
|
||||
return new HydrologyOwnerDraft(key, result, diagnostics, null);
|
||||
}
|
||||
}
|
||||
@@ -1,957 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveContainmentPlanner;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCourseBuilder;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class HydrologyPlanner {
|
||||
static final int ROUTING_CONTEXT_CACHE_SIZE = 64;
|
||||
static final int RESOLVED_OWNER_CACHE_SIZE = 64;
|
||||
static final int REFINED_EDGE_CACHE_SIZE = 4096;
|
||||
final long worldSeed;
|
||||
final HydrologyPlannerSettings settings;
|
||||
final HydrologyTerrainSampler sampler;
|
||||
final HydrologyRoutingTerrainSampler routingSampler;
|
||||
final HydrologyNaturalTerrainSampler naturalSampler;
|
||||
final HydrologyGeometrySampler geometrySampler;
|
||||
final int minimumY;
|
||||
final HydrologyCaveVoxelViewFactory caveViewFactory;
|
||||
final Cache<HydrologyTileKey, SourceRoutingContext> routingContexts;
|
||||
final ConcurrentHashMap<HydrologyTileKey, RoutingContextLoad> resolvingRoutingContexts;
|
||||
final Cache<HydrologyTileKey, CrossTileResolvedOwner> resolvedOwners;
|
||||
final Cache<RefinedEdgeKey, List<HydrologyPoint>> refinedEdgeCache;
|
||||
final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<CrossTileResolvedOwner>> resolvingOwners;
|
||||
final ThreadLocal<PlanningSamples> planningSamples;
|
||||
final ThreadLocal<DraftProfile> draftProfiles;
|
||||
final SimplexNoise routeAnchorX;
|
||||
final SimplexNoise routeAnchorZ;
|
||||
final SimplexNoise routeWormPrimary;
|
||||
final SimplexNoise routeWormDetail;
|
||||
final SurfaceCourseBuilder surfaceCourseBuilder;
|
||||
final HydrologyCrossTileResolver crossTile;
|
||||
final HydrologyOutletPlanner outletPlanner;
|
||||
final HydrologySourcePlanner sourcePlanner;
|
||||
final HydrologyRouteGeometry routeGeometry;
|
||||
final HydrologyRoutePath routePaths;
|
||||
final HydrologySurfaceCoursePlanner surfaceCourses;
|
||||
final HydrologyTributaryPlanner tributaries;
|
||||
final HydrologyUndergroundCoursePlanner undergroundCourses;
|
||||
final HydrologySegmentBuilder segments;
|
||||
final HydrologyFeatureSitePlanner featureSites;
|
||||
final HydrologyRegionalPlanner regional;
|
||||
private volatile Predicate<HydrologyTileKey> earlyOwnerScope;
|
||||
private volatile Function<HydrologyTileKey, HydrologyTile> plannedTiles;
|
||||
|
||||
public HydrologyPlanner(long worldSeed, HydrologyPlannerSettings settings, HydrologyTerrainSampler sampler) {
|
||||
this(
|
||||
worldSeed,
|
||||
settings,
|
||||
sampler,
|
||||
HydrologyGeometrySampler.deterministic(sampler),
|
||||
-4096,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(sampler, settings.seaLevel(), -4096, 4096)
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyPlanner(
|
||||
long worldSeed,
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
int worldHeight
|
||||
) {
|
||||
this(
|
||||
worldSeed,
|
||||
settings,
|
||||
sampler,
|
||||
HydrologyGeometrySampler.deterministic(sampler),
|
||||
0,
|
||||
footprint -> new HydrologyTerrainCaveVoxelView(sampler, settings.seaLevel(), worldHeight)
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyPlanner(
|
||||
long worldSeed,
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
CaveVoxelView caveView
|
||||
) {
|
||||
this(
|
||||
worldSeed,
|
||||
settings,
|
||||
sampler,
|
||||
HydrologyGeometrySampler.deterministic(sampler),
|
||||
Integer.MIN_VALUE,
|
||||
footprint -> caveView
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyPlanner(
|
||||
long worldSeed,
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
int minimumY,
|
||||
HydrologyCaveVoxelViewFactory caveViewFactory
|
||||
) {
|
||||
this(
|
||||
worldSeed,
|
||||
settings,
|
||||
sampler,
|
||||
HydrologyGeometrySampler.deterministic(sampler),
|
||||
minimumY,
|
||||
caveViewFactory
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyPlanner(
|
||||
long worldSeed,
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
HydrologyGeometrySampler geometrySampler,
|
||||
int minimumY,
|
||||
HydrologyCaveVoxelViewFactory caveViewFactory
|
||||
) {
|
||||
this(
|
||||
worldSeed,
|
||||
settings,
|
||||
sampler,
|
||||
new PointRoutingTerrainSampler(sampler),
|
||||
geometrySampler,
|
||||
minimumY,
|
||||
caveViewFactory
|
||||
);
|
||||
}
|
||||
|
||||
public HydrologyPlanner(
|
||||
long worldSeed,
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
HydrologyRoutingTerrainSampler routingSampler,
|
||||
HydrologyGeometrySampler geometrySampler,
|
||||
int minimumY,
|
||||
HydrologyCaveVoxelViewFactory caveViewFactory
|
||||
) {
|
||||
this.worldSeed = worldSeed;
|
||||
this.settings = Objects.requireNonNull(settings, "settings");
|
||||
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||
this.routingSampler = Objects.requireNonNull(routingSampler, "routingSampler");
|
||||
this.naturalSampler = routingSampler instanceof HydrologyNaturalTerrainSampler pointSampler
|
||||
? pointSampler
|
||||
: null;
|
||||
this.geometrySampler = Objects.requireNonNull(geometrySampler, "geometrySampler");
|
||||
this.minimumY = minimumY;
|
||||
this.caveViewFactory = Objects.requireNonNull(caveViewFactory, "caveViewFactory");
|
||||
HydrologyCacheBudget cacheBudget = HydrologyCacheBudget.runtime(settings.routing().regional().enabled());
|
||||
this.routingContexts = Caffeine.newBuilder()
|
||||
.maximumWeight(cacheBudget.routingBytes())
|
||||
.weigher((HydrologyTileKey key, SourceRoutingContext context) -> HydrologyCacheWeights.bounded(
|
||||
HydrologyCacheWeights.routing(context), cacheBudget.routingBytes(), ROUTING_CONTEXT_CACHE_SIZE))
|
||||
.build();
|
||||
this.resolvingRoutingContexts = new ConcurrentHashMap<>();
|
||||
this.resolvedOwners = Caffeine.newBuilder()
|
||||
.maximumWeight(cacheBudget.ownerBytes())
|
||||
.weigher((HydrologyTileKey key, CrossTileResolvedOwner owner) -> HydrologyCacheWeights.bounded(
|
||||
HydrologyCacheWeights.owner(owner), cacheBudget.ownerBytes(), RESOLVED_OWNER_CACHE_SIZE))
|
||||
.build();
|
||||
this.refinedEdgeCache = Caffeine.newBuilder()
|
||||
.maximumWeight(cacheBudget.edgeBytes())
|
||||
.weigher((RefinedEdgeKey key, List<HydrologyPoint> points) -> HydrologyCacheWeights.bounded(
|
||||
HydrologyCacheWeights.points(points), cacheBudget.edgeBytes(), REFINED_EDGE_CACHE_SIZE))
|
||||
.build();
|
||||
this.resolvingOwners = new ConcurrentHashMap<>();
|
||||
this.planningSamples = new ThreadLocal<>();
|
||||
this.draftProfiles = new ThreadLocal<>();
|
||||
this.routeAnchorX = new SimplexNoise(HydrologyHash.mix(worldSeed, HydrologyRouteGeometry.ROUTE_ANCHOR_X_SALT));
|
||||
this.routeAnchorZ = new SimplexNoise(HydrologyHash.mix(worldSeed, HydrologyRouteGeometry.ROUTE_ANCHOR_Z_SALT));
|
||||
this.routeWormPrimary = new SimplexNoise(HydrologyHash.mix(worldSeed, HydrologyRouteGeometry.ROUTE_WORM_PRIMARY_SALT));
|
||||
this.routeWormDetail = new SimplexNoise(HydrologyHash.mix(worldSeed, HydrologyRouteGeometry.ROUTE_WORM_DETAIL_SALT));
|
||||
this.surfaceCourseBuilder = new SurfaceCourseBuilder(
|
||||
settings.surface(),
|
||||
this::sampleBasisWithoutSlope,
|
||||
geometrySampler,
|
||||
settings.seaLevel()
|
||||
);
|
||||
this.crossTile = new HydrologyCrossTileResolver(this, HydrologyPlanningAdmission.maximumTrials());
|
||||
this.outletPlanner = new HydrologyOutletPlanner(this);
|
||||
this.sourcePlanner = new HydrologySourcePlanner(this);
|
||||
this.routeGeometry = new HydrologyRouteGeometry(this);
|
||||
this.routePaths = new HydrologyRoutePath(this);
|
||||
this.surfaceCourses = new HydrologySurfaceCoursePlanner(this);
|
||||
this.tributaries = new HydrologyTributaryPlanner(this);
|
||||
this.undergroundCourses = new HydrologyUndergroundCoursePlanner(this);
|
||||
this.segments = new HydrologySegmentBuilder(this);
|
||||
this.featureSites = new HydrologyFeatureSitePlanner(this);
|
||||
this.regional = new HydrologyRegionalPlanner(this);
|
||||
}
|
||||
|
||||
public long worldSeed() {
|
||||
return worldSeed;
|
||||
}
|
||||
|
||||
public HydrologyPlannerSettings settings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
public HydrologyTile plan(HydrologyTileKey key) {
|
||||
long started = System.nanoTime();
|
||||
CrossTileResolution resolution = resolveCrossTileOwner(key);
|
||||
long materializeStarted = System.nanoTime();
|
||||
HydrologyTile tile = crossTile.materializeAcceptedTile(resolution)
|
||||
.withResolvedOwner(new CrossTileResolvedOwner(resolution.draft(), resolution.observedRejections()));
|
||||
long finished = System.nanoTime();
|
||||
IrisLogging.debug(
|
||||
"Hydrology tile %d,%d planned in %dms: owners=%d resolve=%dms materialize=%dms courses=%d on %s",
|
||||
key.tileX(),
|
||||
key.tileZ(),
|
||||
(finished - started) / 1_000_000L,
|
||||
resolution.ownerCount(),
|
||||
(materializeStarted - started) / 1_000_000L,
|
||||
(finished - materializeStarted) / 1_000_000L,
|
||||
tile.courses().size(),
|
||||
Thread.currentThread().getName()
|
||||
);
|
||||
return tile;
|
||||
}
|
||||
|
||||
public List<HydrologyDiagnosticCandidate> diagnosticCandidates(HydrologyTile tile) {
|
||||
Objects.requireNonNull(tile, "tile");
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>(tile.localDiagnosticCandidates());
|
||||
diagnostics.addAll(regional.diagnosticsIn(regional.ownerBounds(tile.key())));
|
||||
ArrayList<HydrologyDiagnosticCandidate> unique = new ArrayList<>(sourcePlanner.uniqueDiagnostics(diagnostics));
|
||||
unique.sort(Comparator.comparingLong(HydrologyDiagnosticCandidate::id));
|
||||
return List.copyOf(unique);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the lower-rank neighbour drafts an owner starts before its admission demands them to the
|
||||
* tiles the scope accepts; null starts all of them. A neighbour that no admission demands and
|
||||
* nothing plans is a draft computed for nothing, and so are the early neighbours it starts itself.
|
||||
*/
|
||||
void limitEarlyOwners(Predicate<HydrologyTileKey> scope) {
|
||||
earlyOwnerScope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the plans of tiles already planned are found. An owner draft that left the owner cache is
|
||||
* restored from its tile's plan when a neighbour demands it again, instead of being drafted twice.
|
||||
*/
|
||||
void usePlannedTiles(Function<HydrologyTileKey, HydrologyTile> source) {
|
||||
plannedTiles = source;
|
||||
}
|
||||
|
||||
void clearOwnerDrafts() {
|
||||
resolvedOwners.invalidateAll();
|
||||
regional.clear();
|
||||
}
|
||||
|
||||
void reuseResolvedTile(HydrologyTile tile) {
|
||||
Objects.requireNonNull(tile, "tile");
|
||||
if (tile.worldSeed() != worldSeed
|
||||
|| tile.settingsFingerprint() != settings.fingerprint()
|
||||
|| tile.tileSize() != settings.routing().tileSize()) {
|
||||
throw new IllegalArgumentException("Hydrology tile does not match this planner.");
|
||||
}
|
||||
CrossTileResolvedOwner owner = tile.resolvedOwner();
|
||||
if (owner != null) {
|
||||
resolvedOwners.asMap().putIfAbsent(tile.key(), owner);
|
||||
}
|
||||
}
|
||||
|
||||
CrossTileResolution resolveCrossTileOwner(HydrologyTileKey key) {
|
||||
long started = System.nanoTime();
|
||||
CrossTileResolutionContext context = new CrossTileResolutionContext(
|
||||
key,
|
||||
maximumCrossTileDependencyOffset(key),
|
||||
maximumCrossTileDependencyOwners(key)
|
||||
);
|
||||
CrossTileResolvedOwner resolved = resolveCrossTileOwner(key, context, true);
|
||||
return new CrossTileResolution(
|
||||
resolved.draft(),
|
||||
resolved.observedRejections(),
|
||||
context.iterations(),
|
||||
context.ownerCount(),
|
||||
System.nanoTime() - started
|
||||
);
|
||||
}
|
||||
|
||||
CrossTileResolvedOwner resolveCrossTileOwner(
|
||||
HydrologyTileKey key,
|
||||
CrossTileResolutionContext context,
|
||||
boolean retainFootprintCompiler
|
||||
) {
|
||||
CrossTileResolvedOwner local = context.resolved(key);
|
||||
if (local != null) {
|
||||
return local;
|
||||
}
|
||||
CrossTileResolvedOwner cached = resolvedOwners.getIfPresent(key);
|
||||
if (cached == null) {
|
||||
cached = plannedOwner(key);
|
||||
}
|
||||
if (cached != null) {
|
||||
context.remember(key, cached);
|
||||
return cached;
|
||||
}
|
||||
CompletableFuture<CrossTileResolvedOwner> owned = new CompletableFuture<>();
|
||||
CompletableFuture<CrossTileResolvedOwner> existing = resolvingOwners.putIfAbsent(key, owned);
|
||||
if (existing != null) {
|
||||
CrossTileResolvedOwner resolved = awaitResolvedOwner(existing);
|
||||
context.remember(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
if (!context.begin(key)) {
|
||||
resolvingOwners.remove(key, owned);
|
||||
throw new IllegalStateException("Cross-tile owner color dependencies must be acyclic.");
|
||||
}
|
||||
try {
|
||||
int ownerRank = crossTile.ownerColorRank(key);
|
||||
ColorRankedDraftAdmission admission = new ColorRankedDraftAdmission(key, ownerRank, context);
|
||||
HydrologyOwnerDraft draft;
|
||||
try (admission) {
|
||||
try {
|
||||
draft = crossTile.compileOwnerDraft(
|
||||
key,
|
||||
admission,
|
||||
retainFootprintCompiler
|
||||
);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
admission.primaryFailure = failure;
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
CrossTileResolvedOwner resolved = new CrossTileResolvedOwner(draft, admission.observedRejections());
|
||||
context.remember(key, resolved);
|
||||
CrossTileResolvedOwner shared = resolved.withoutFootprintCompiler();
|
||||
resolvedOwners.put(key, shared);
|
||||
owned.complete(shared);
|
||||
return resolved;
|
||||
} catch (Throwable failure) {
|
||||
owned.completeExceptionally(failure);
|
||||
throw propagateOwnerFailure(failure);
|
||||
} finally {
|
||||
context.end(key);
|
||||
resolvingOwners.remove(key, owned);
|
||||
}
|
||||
}
|
||||
|
||||
private CrossTileResolvedOwner plannedOwner(HydrologyTileKey key) {
|
||||
Function<HydrologyTileKey, HydrologyTile> source = plannedTiles;
|
||||
HydrologyTile tile = source == null ? null : source.apply(key);
|
||||
if (tile == null || tile.resolvedOwner() == null) {
|
||||
return null;
|
||||
}
|
||||
reuseResolvedTile(tile);
|
||||
CrossTileResolvedOwner restored = resolvedOwners.getIfPresent(key);
|
||||
return restored == null ? tile.resolvedOwner() : restored;
|
||||
}
|
||||
|
||||
List<CrossTileResolvedOwner> resolveLowerRankOwners(
|
||||
List<HydrologyTileKey> candidateKeys,
|
||||
CrossTileResolutionContext context,
|
||||
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners
|
||||
) {
|
||||
if (candidateKeys.size() < 2 || !IrisPlatforms.isBound()) {
|
||||
ArrayList<CrossTileResolvedOwner> resolved = new ArrayList<>(candidateKeys.size());
|
||||
for (HydrologyTileKey candidateKey : candidateKeys) {
|
||||
CrossTileResolvedOwner local = context.resolved(candidateKey);
|
||||
if (local != null) {
|
||||
resolved.add(local);
|
||||
continue;
|
||||
}
|
||||
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
|
||||
CrossTileResolvedOwner owner = prepared == null
|
||||
? resolveCrossTileOwner(candidateKey, context, false) : prepared.await();
|
||||
context.remember(candidateKey, owner);
|
||||
resolved.add(owner);
|
||||
}
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
// Neighbour drafts use the current pool (or the burst pool from outside any pool).
|
||||
// Waiting workers claim their own tasks without helping unrelated owner drafts.
|
||||
ArrayList<Callable<CrossTileResolvedOwner>> tasks = new ArrayList<>(candidateKeys.size());
|
||||
ArrayList<CrossTileResolvedOwner> resolved = new ArrayList<>(candidateKeys.size());
|
||||
for (HydrologyTileKey candidateKey : candidateKeys) {
|
||||
CrossTileResolvedOwner local = context.resolved(candidateKey);
|
||||
resolved.add(local);
|
||||
if (local != null) {
|
||||
continue;
|
||||
}
|
||||
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
|
||||
tasks.add(prepared == null ? () -> resolveIndependentOwner(candidateKey) : prepared::await);
|
||||
}
|
||||
List<CrossTileResolvedOwner> owners = HydrologyForkJoin.invokeAll(tasks, MultiBurst.burst);
|
||||
int completed = 0;
|
||||
for (int index = 0; index < candidateKeys.size(); index++) {
|
||||
if (resolved.get(index) != null) {
|
||||
continue;
|
||||
}
|
||||
CrossTileResolvedOwner owner = owners.get(completed++);
|
||||
context.remember(candidateKeys.get(index), owner);
|
||||
resolved.set(index, owner);
|
||||
}
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
|
||||
CrossTileResolvedOwner resolveIndependentOwner(HydrologyTileKey key) {
|
||||
CrossTileResolutionContext context = new CrossTileResolutionContext(
|
||||
key,
|
||||
maximumCrossTileDependencyOffset(key),
|
||||
maximumCrossTileDependencyOwners(key)
|
||||
);
|
||||
return resolveCrossTileOwner(key, context, false);
|
||||
}
|
||||
|
||||
static CrossTileResolvedOwner awaitResolvedOwner(
|
||||
CompletableFuture<CrossTileResolvedOwner> future
|
||||
) {
|
||||
try {
|
||||
return future.join();
|
||||
} catch (CompletionException failure) {
|
||||
throw propagateOwnerFailure(failure.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
static RuntimeException propagateOwnerFailure(Throwable failure) {
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
return runtimeException;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
return new IllegalStateException("Hydrology owner planning failed.", failure);
|
||||
}
|
||||
|
||||
int maximumCrossTileDependencyOwners(HydrologyTileKey key) {
|
||||
long offset = maximumCrossTileDependencyOffset(key);
|
||||
long width = Math.addExact(Math.multiplyExact(2L, offset), 1L);
|
||||
return Math.toIntExact(Math.multiplyExact(width, width));
|
||||
}
|
||||
|
||||
long maximumCrossTileDependencyOffset(HydrologyTileKey key) {
|
||||
return Math.multiplyExact(
|
||||
(long) crossTile.ownerColorRank(key),
|
||||
settings.crossTileColorPeriod() - 1L
|
||||
);
|
||||
}
|
||||
|
||||
SourceRoutingContext sourceRoutingContext(HydrologyTileKey key) {
|
||||
SourceRoutingContext cached = routingContexts.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
RoutingContextLoad owned = new RoutingContextLoad(Thread.currentThread(), new CompletableFuture<>());
|
||||
RoutingContextLoad existing = resolvingRoutingContexts.putIfAbsent(key, owned);
|
||||
if (existing != null) {
|
||||
if (existing.owner() == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Hydrology routing context cannot recursively load its own tile.");
|
||||
}
|
||||
try {
|
||||
return existing.future().join();
|
||||
} catch (CompletionException failure) {
|
||||
throw propagateOwnerFailure(failure.getCause());
|
||||
}
|
||||
}
|
||||
try {
|
||||
SourceRoutingContext resolved = routingContexts.getIfPresent(key);
|
||||
if (resolved == null) {
|
||||
resolved = sourcePlanner.compileSourceRoutingContext(key);
|
||||
routingContexts.put(key, resolved);
|
||||
}
|
||||
owned.future().complete(resolved);
|
||||
return resolved;
|
||||
} catch (Throwable failure) {
|
||||
owned.future().completeExceptionally(failure);
|
||||
throw propagateOwnerFailure(failure);
|
||||
} finally {
|
||||
resolvingRoutingContexts.remove(key, owned);
|
||||
}
|
||||
}
|
||||
|
||||
int settingsSeaLevel(HydrologyTerrainSample terrain) {
|
||||
return settings.seaLevel();
|
||||
}
|
||||
|
||||
static HydrologyPoint withY(HydrologyPoint point, int y) {
|
||||
return new HydrologyPoint(point.x(), y, point.z());
|
||||
}
|
||||
|
||||
static int clamp(int value, int minimum, int maximum) {
|
||||
return Math.max(minimum, Math.min(maximum, value));
|
||||
}
|
||||
|
||||
int sampleGeometry(
|
||||
HydrologyGeometrySampler.Field field,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
long stableId,
|
||||
int minimum,
|
||||
int maximum
|
||||
) {
|
||||
HydrologyGeometrySampler.Request request = new HydrologyGeometrySampler.Request(
|
||||
field,
|
||||
profileKey,
|
||||
x,
|
||||
z,
|
||||
stableId,
|
||||
minimum,
|
||||
maximum
|
||||
);
|
||||
PlanningSamples samples = planningSamples.get();
|
||||
if (samples != null) {
|
||||
Integer cached = samples.geometry.get(request);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
int sampled = geometrySampler.sample(request);
|
||||
if (sampled < minimum || sampled > maximum) {
|
||||
throw new IllegalStateException(
|
||||
"Hydrology geometry sampler returned " + sampled + " outside " + minimum + ".." + maximum
|
||||
+ " for " + field + " at " + x + "," + z + "."
|
||||
);
|
||||
}
|
||||
if (samples != null) {
|
||||
samples.geometry.put(request, sampled);
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
|
||||
PlanningSamples samples = planningSamples.get();
|
||||
if (samples == null) {
|
||||
return naturalSampler == null
|
||||
? sampler.sample(blockX, blockZ)
|
||||
: naturalSampler.sampleBasis(blockX, blockZ);
|
||||
}
|
||||
long packed = RiverFootprint.pack(blockX, blockZ);
|
||||
HydrologyTerrainSample cached = samples.basis.get(packed);
|
||||
if (cached != null || samples.basis.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = naturalSampler == null
|
||||
? sampler.sample(blockX, blockZ)
|
||||
: samples.sharedBasis == null ? naturalSampler.sampleBasis(blockX, blockZ)
|
||||
: samples.sharedBasis.sample(blockX, blockZ, naturalSampler);
|
||||
samples.basis.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleLandBasis(int blockX, int blockZ) {
|
||||
HydrologyTerrainSample terrain = sampleBasis(blockX, blockZ);
|
||||
return terrain == null || terrain.ocean() ? null : terrain;
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
|
||||
if (naturalSampler == null) {
|
||||
return sampleBasis(blockX, blockZ);
|
||||
}
|
||||
PlanningSamples samples = planningSamples.get();
|
||||
if (samples == null) {
|
||||
return naturalSampler.sampleBasisWithoutSlope(blockX, blockZ);
|
||||
}
|
||||
long packed = RiverFootprint.pack(blockX, blockZ);
|
||||
HydrologyTerrainSample fullSample = samples.basis.get(packed);
|
||||
if (fullSample != null || samples.basis.containsKey(packed)) {
|
||||
return fullSample;
|
||||
}
|
||||
HydrologyTerrainSample cached = samples.basisWithoutSlope.get(packed);
|
||||
if (cached != null || samples.basisWithoutSlope.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = samples.sharedBasis == null
|
||||
? naturalSampler.sampleBasisWithoutSlope(blockX, blockZ)
|
||||
: samples.sharedBasis.sampleWithoutSlope(blockX, blockZ, naturalSampler);
|
||||
samples.basisWithoutSlope.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleLandBasisWithoutSlope(int blockX, int blockZ) {
|
||||
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(blockX, blockZ);
|
||||
return terrain == null || terrain.ocean() ? null : terrain;
|
||||
}
|
||||
|
||||
double sampleLandHeight(int blockX, int blockZ) {
|
||||
if (naturalSampler == null) {
|
||||
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(blockX, blockZ);
|
||||
return terrain == null ? Double.NaN : terrain.naturalHeight();
|
||||
}
|
||||
PlanningSamples samples = planningSamples.get();
|
||||
if (samples == null) {
|
||||
return naturalSampler.sampleLandHeight(blockX, blockZ);
|
||||
}
|
||||
long packed = RiverFootprint.pack(blockX, blockZ);
|
||||
HydrologyTerrainSample terrain = samples.basis.get(packed);
|
||||
if (terrain != null || samples.basis.containsKey(packed)) {
|
||||
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
|
||||
}
|
||||
terrain = samples.basisWithoutSlope.get(packed);
|
||||
if (terrain != null || samples.basisWithoutSlope.containsKey(packed)) {
|
||||
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
|
||||
}
|
||||
double cached = samples.landHeights.get(packed);
|
||||
if (!Double.isNaN(cached) || samples.landHeights.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
double sampled = samples.sharedLandHeights == null
|
||||
? naturalSampler.sampleLandHeight(blockX, blockZ)
|
||||
: samples.sharedLandHeights.sample(blockX, blockZ, naturalSampler);
|
||||
samples.landHeights.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sampleDetailed(int blockX, int blockZ) {
|
||||
PlanningSamples samples = planningSamples.get();
|
||||
if (samples == null) {
|
||||
return sampler.sample(blockX, blockZ);
|
||||
}
|
||||
long packed = RiverFootprint.pack(blockX, blockZ);
|
||||
HydrologyTerrainSample cached = samples.detailed.get(packed);
|
||||
if (cached != null || samples.detailed.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = sampler.sample(blockX, blockZ);
|
||||
samples.detailed.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
PlanningSamples samples = planningSamples.get();
|
||||
if (samples == null) {
|
||||
return naturalSampler.classifyNatural(blockX, blockZ);
|
||||
}
|
||||
long packed = RiverFootprint.pack(blockX, blockZ);
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification cached = samples.classifications.get(packed);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyRoutingTerrainSampler.NaturalClassification sampled = Objects.requireNonNull(
|
||||
naturalSampler.classifyNatural(blockX, blockZ),
|
||||
"Hydrology natural terrain classifier returned null at " + blockX + "," + blockZ
|
||||
);
|
||||
samples.classifications.put(packed, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
static final class PointRoutingTerrainSampler implements HydrologyRoutingTerrainSampler {
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
|
||||
private PointRoutingTerrainSampler(HydrologyTerrainSampler sampler) {
|
||||
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSharedGridSamples() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
|
||||
int minimumX = request.minimumX();
|
||||
int minimumZ = request.minimumZ();
|
||||
int width = request.width();
|
||||
int spacing = request.spacing();
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[Math.multiplyExact(width, width)];
|
||||
for (int gridZ = 0; gridZ < width; gridZ++) {
|
||||
int z = Math.toIntExact((long) minimumZ + (long) gridZ * spacing);
|
||||
for (int gridX = 0; gridX < width; gridX++) {
|
||||
int x = Math.toIntExact((long) minimumX + (long) gridX * spacing);
|
||||
samples[gridZ * width + gridX] = sampler.sample(x, z);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
HydrologyTerrainSample terrain = sampler.sample(blockX, blockZ);
|
||||
if (terrain == null) {
|
||||
return NaturalClassification.UNAVAILABLE;
|
||||
}
|
||||
return terrain.ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
|
||||
}
|
||||
}
|
||||
|
||||
static final class PlanningSamples {
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basis;
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basisWithoutSlope;
|
||||
private final Long2DoubleOpenHashMap landHeights;
|
||||
private HydrologyLandHeightCache sharedLandHeights;
|
||||
private HydrologyBasisCache sharedBasis;
|
||||
final Long2ObjectOpenHashMap<HydrologyTerrainSample> detailed;
|
||||
private final Long2ObjectOpenHashMap<HydrologyRoutingTerrainSampler.NaturalClassification> classifications;
|
||||
private final HashMap<HydrologyGeometrySampler.Request, Integer> geometry;
|
||||
final HashMap<MinimumLandHeightKey, Integer> minimumLandHeights;
|
||||
final HashMap<Integer, List<HydrologyGridOffset>> radialOffsets;
|
||||
final HashMap<UndergroundSegmentCapKey, Integer> undergroundSegmentCaps;
|
||||
final HashMap<Long, HydrologyPoint> routeAnchors;
|
||||
final HashMap<Long, HydrologyPoint> surfaceRouteAnchors;
|
||||
final HydrologyCaveCourseFilter.CandidateCache caveCandidates;
|
||||
final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
|
||||
|
||||
PlanningSamples() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
PlanningSamples(HydrologyLandHeightCache sharedLandHeights, HydrologyBasisCache sharedBasis) {
|
||||
this.sharedLandHeights = sharedLandHeights;
|
||||
this.sharedBasis = sharedBasis;
|
||||
this.basis = new Long2ObjectOpenHashMap<>();
|
||||
this.basisWithoutSlope = new Long2ObjectOpenHashMap<>();
|
||||
this.landHeights = new Long2DoubleOpenHashMap();
|
||||
this.landHeights.defaultReturnValue(Double.NaN);
|
||||
this.detailed = new Long2ObjectOpenHashMap<>();
|
||||
this.classifications = new Long2ObjectOpenHashMap<>();
|
||||
this.geometry = new HashMap<>();
|
||||
this.minimumLandHeights = new HashMap<>();
|
||||
this.radialOffsets = new HashMap<>();
|
||||
this.undergroundSegmentCaps = new HashMap<>();
|
||||
this.routeAnchors = new HashMap<>();
|
||||
this.surfaceRouteAnchors = new HashMap<>();
|
||||
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
|
||||
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
|
||||
}
|
||||
|
||||
HydrologyBasisCache fallbackBasis(Long2ObjectMap<HydrologyTerrainSample> footprintBasis) {
|
||||
if (sharedBasis == null) {
|
||||
sharedBasis = new HydrologyBasisCache();
|
||||
sharedBasis.seed(basis);
|
||||
sharedBasis.seedWithoutSlope(basisWithoutSlope);
|
||||
sharedBasis.seedWithoutSlope(footprintBasis);
|
||||
}
|
||||
return sharedBasis;
|
||||
}
|
||||
|
||||
HydrologyLandHeightCache fallbackLandHeights() {
|
||||
if (sharedLandHeights == null) {
|
||||
sharedLandHeights = new HydrologyLandHeightCache();
|
||||
sharedLandHeights.seed(landHeights);
|
||||
}
|
||||
return sharedLandHeights;
|
||||
}
|
||||
}
|
||||
|
||||
final class ColorRankedDraftAdmission implements CrossTileDraftAdmission, AutoCloseable {
|
||||
private final HydrologyTileKey ownerKey;
|
||||
private final int ownerRank;
|
||||
private final CrossTileResolutionContext context;
|
||||
private final Set<Long> rejectedCourseIds;
|
||||
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners = new LinkedHashMap<>();
|
||||
private final Set<HydrologyTileKey> demandedOwners = new HashSet<>();
|
||||
private Throwable primaryFailure;
|
||||
private final LinkedHashMap<CrossTileRejectionKey, CrossTileRejectedCourse> observedRejections;
|
||||
|
||||
private ColorRankedDraftAdmission(
|
||||
HydrologyTileKey ownerKey,
|
||||
int ownerRank,
|
||||
CrossTileResolutionContext context
|
||||
) {
|
||||
this.ownerKey = Objects.requireNonNull(ownerKey, "ownerKey");
|
||||
this.ownerRank = ownerRank;
|
||||
this.context = Objects.requireNonNull(context, "context");
|
||||
this.rejectedCourseIds = new HashSet<>();
|
||||
this.observedRejections = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
if (ownerRank == 0 || settings.crossTileColorPeriod() != 2 || !sourcePlanner.hasRoutedSourceSearch()
|
||||
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)
|
||||
|| HydrologyPlanningAdmission.effectiveParallelism(worker.getPool().getParallelism()) <= 2) {
|
||||
return;
|
||||
}
|
||||
int radius = settings.crossTileColorPeriod() - 1;
|
||||
Predicate<HydrologyTileKey> scope = earlyOwnerScope;
|
||||
ArrayList<HydrologyTileKey> candidates = new ArrayList<>();
|
||||
for (long tileZ = (long) ownerKey.tileZ() - radius; tileZ <= (long) ownerKey.tileZ() + radius; tileZ++) {
|
||||
for (long tileX = (long) ownerKey.tileX() - radius; tileX <= (long) ownerKey.tileX() + radius; tileX++) {
|
||||
if (tileX < Integer.MIN_VALUE || tileX > Integer.MAX_VALUE
|
||||
|| tileZ < Integer.MIN_VALUE || tileZ > Integer.MAX_VALUE) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTileKey key = new HydrologyTileKey((int) tileX, (int) tileZ);
|
||||
if (crossTile.ownerColorRank(key) >= ownerRank || scope != null && !scope.test(key)
|
||||
|| preparedOwners.containsKey(key)
|
||||
|| resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
candidates.add(key);
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingInt(crossTile::ownerColorRank).reversed()
|
||||
.thenComparing(HydrologyTileKey::compareTo));
|
||||
for (HydrologyTileKey key : candidates) {
|
||||
if (resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyForkJoin.Task<CrossTileResolvedOwner> task = new HydrologyForkJoin.Task<>(() -> resolveIndependentOwner(key));
|
||||
preparedOwners.put(key, task);
|
||||
try {
|
||||
worker.getPool().execute(task);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
preparedOwners.remove(key);
|
||||
throw failure;
|
||||
}
|
||||
currentDraftProfile().earlyOwners++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Error failure = null;
|
||||
Map<HydrologyTileKey, RuntimeException> unusedFailures = new LinkedHashMap<>();
|
||||
for (Map.Entry<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> entry : preparedOwners.entrySet()) {
|
||||
try {
|
||||
entry.getValue().await();
|
||||
} catch (RuntimeException unusedFailure) {
|
||||
if (!demandedOwners.contains(entry.getKey())) {
|
||||
unusedFailures.put(entry.getKey(), unusedFailure);
|
||||
}
|
||||
} catch (Error failed) {
|
||||
if (failed == primaryFailure) {
|
||||
continue;
|
||||
}
|
||||
if (failure == null) {
|
||||
failure = failed;
|
||||
} else if (failure != failed) {
|
||||
failure.addSuppressed(failed);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<HydrologyTileKey, RuntimeException> entry : unusedFailures.entrySet()) {
|
||||
IrisLogging.reportError("Unused early hydrology owner " + entry.getKey().tileX()
|
||||
+ "," + entry.getKey().tileZ() + " failed to prepare.", entry.getValue());
|
||||
}
|
||||
if (failure != null) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result) {
|
||||
context.recordIteration();
|
||||
HydrologyCaveCourseFilter.Result current = HydrologyCaveCourseFilter.withoutCourses(
|
||||
result,
|
||||
rejectedCourseIds
|
||||
);
|
||||
demandedOwners.addAll(crossTile.conflictOwnerKeys(ownerKey, current));
|
||||
CrossTileBlockers blockers = crossTile.lowerRankBlockers(
|
||||
ownerKey,
|
||||
current,
|
||||
ownerRank,
|
||||
context,
|
||||
preparedOwners
|
||||
);
|
||||
HydrologyCrossTileSurfaceAdmission.Result surfaceAdmission =
|
||||
HydrologyCrossTileSurfaceAdmission.admit(
|
||||
crossTile.surfaceClaims(current),
|
||||
blockers.surfaceClaims()
|
||||
);
|
||||
Map<Long, RiverCourse> currentCourses = crossTile.coursesById(current.courses());
|
||||
HashSet<Long> rejectedSurfaceOutlets = new HashSet<>();
|
||||
HashMap<Long, Long> surfaceWinners = new HashMap<>();
|
||||
for (HydrologyCrossTileSurfaceAdmission.Rejection rejection : surfaceAdmission.rejections()) {
|
||||
RiverCourse course = currentCourses.get(rejection.loser().courseId());
|
||||
if (course == null || course.outletId().isEmpty()) {
|
||||
throw new IllegalStateException("Cross-tile surface admission rejected an unknown current course.");
|
||||
}
|
||||
long outletId = course.outletId().getAsLong();
|
||||
rejectedSurfaceOutlets.add(outletId);
|
||||
surfaceWinners.putIfAbsent(outletId, rejection.winnerCourseId());
|
||||
}
|
||||
HashSet<Long> newRejectedCourseIds = new HashSet<>();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
for (RiverCourse course : current.courses()) {
|
||||
if (course.outletId().isEmpty()
|
||||
|| !rejectedSurfaceOutlets.contains(course.outletId().getAsLong())) {
|
||||
continue;
|
||||
}
|
||||
long winnerCourseId = surfaceWinners.get(course.outletId().getAsLong());
|
||||
rejectCrossTileCourse(course, winnerCourseId, newRejectedCourseIds, diagnostics);
|
||||
}
|
||||
HydrologyCaveCourseFilter.Result surfaceAccepted = HydrologyCaveCourseFilter.withoutCourses(
|
||||
current,
|
||||
newRejectedCourseIds
|
||||
);
|
||||
HydrologyCrossTileCaveAdmission.Result caveAdmission = HydrologyCrossTileCaveAdmission.admit(
|
||||
ownerRank,
|
||||
crossTile.caveClaims(surfaceAccepted),
|
||||
blockers.caveClaims()
|
||||
);
|
||||
if (newRejectedCourseIds.isEmpty() && caveAdmission.rejections().isEmpty()) {
|
||||
return new CrossTilePublicationAdmission(current, List.of(), false);
|
||||
}
|
||||
Map<Long, RiverCourse> surfaceAcceptedCourses = crossTile.coursesById(surfaceAccepted.courses());
|
||||
for (HydrologyCrossTileCaveAdmission.Rejection rejection : caveAdmission.rejections()) {
|
||||
HydrologyCrossTileCaveAdmission.Claim claim = rejection.loser();
|
||||
RiverCourse course = surfaceAcceptedCourses.get(claim.courseId());
|
||||
if (course == null) {
|
||||
throw new IllegalStateException("Cross-tile cave admission rejected an unknown current course.");
|
||||
}
|
||||
rejectCrossTileCourse(course, rejection.winnerSourceId(), newRejectedCourseIds, diagnostics);
|
||||
}
|
||||
return new CrossTilePublicationAdmission(
|
||||
HydrologyCaveCourseFilter.withoutCourses(current, newRejectedCourseIds),
|
||||
diagnostics,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private void rejectCrossTileCourse(
|
||||
RiverCourse course,
|
||||
long winnerCourseId,
|
||||
Set<Long> newRejectedCourseIds,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics
|
||||
) {
|
||||
if (!rejectedCourseIds.add(course.id())) {
|
||||
throw new IllegalStateException("Cross-tile admission repeated a rejected course.");
|
||||
}
|
||||
newRejectedCourseIds.add(course.id());
|
||||
CrossTileRejectionKey rejectionKey = new CrossTileRejectionKey(course.id(), winnerCourseId);
|
||||
if (observedRejections.containsKey(rejectionKey)) {
|
||||
return;
|
||||
}
|
||||
observedRejections.put(
|
||||
rejectionKey,
|
||||
new CrossTileRejectedCourse(course, winnerCourseId)
|
||||
);
|
||||
diagnostics.add(course.type() == RiverCourseType.SURFACE
|
||||
? crossTile.surfaceConflictDiagnostic(course, winnerCourseId)
|
||||
: HydrologyCaveCourseFilter.overlapDiagnostic(course, winnerCourseId));
|
||||
}
|
||||
|
||||
private List<CrossTileRejectedCourse> observedRejections() {
|
||||
return List.copyOf(observedRejections.values());
|
||||
}
|
||||
}
|
||||
|
||||
DraftProfile currentDraftProfile() {
|
||||
DraftProfile profile = draftProfiles.get();
|
||||
return profile == null ? new DraftProfile() : profile;
|
||||
}
|
||||
}
|
||||
-1040
File diff suppressed because it is too large.
Load diff
-136
@@ -1,136 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
final class HydrologyPlanningAdmission {
|
||||
private static final long BYTES_PER_ROOT = 2L * 1024L * 1024L * 1024L;
|
||||
private static final long BYTES_PER_TRIAL = 128L * 1024L * 1024L;
|
||||
private static final int MAXIMUM_TRIAL_LIMIT = 16;
|
||||
private static final int MAXIMUM_ROOTS = rootLimit(
|
||||
Runtime.getRuntime().maxMemory(), Runtime.getRuntime().availableProcessors());
|
||||
private static final int MAXIMUM_TRIALS = trialLimit(Runtime.getRuntime().maxMemory(), MAXIMUM_ROOTS);
|
||||
private static final HydrologyPlanningAdmission ROOTS = new HydrologyPlanningAdmission(MAXIMUM_ROOTS);
|
||||
|
||||
private final Semaphore permits;
|
||||
|
||||
HydrologyPlanningAdmission(int maximumRoots) {
|
||||
if (maximumRoots < 1) {
|
||||
throw new IllegalArgumentException("Maximum hydrology roots must be positive.");
|
||||
}
|
||||
permits = new Semaphore(maximumRoots, true);
|
||||
}
|
||||
|
||||
static Permit acquireRoot(BooleanSupplier cancelled) {
|
||||
return ROOTS.acquire(cancelled);
|
||||
}
|
||||
|
||||
static int maximumRoots() {
|
||||
return MAXIMUM_ROOTS;
|
||||
}
|
||||
|
||||
static int maximumTrials() {
|
||||
return MAXIMUM_TRIALS;
|
||||
}
|
||||
|
||||
static int effectiveParallelism(int poolParallelism) {
|
||||
return effectiveParallelism(poolParallelism, Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
|
||||
static int effectiveParallelism(int poolParallelism, int availableProcessors) {
|
||||
return Math.max(1, Math.min(poolParallelism, availableProcessors));
|
||||
}
|
||||
|
||||
static int trialLimit(long maximumHeap, int maximumRoots) {
|
||||
if (maximumRoots < 1) {
|
||||
throw new IllegalArgumentException("Maximum hydrology roots must be positive.");
|
||||
}
|
||||
return (int) Math.clamp(maximumHeap / maximumRoots / BYTES_PER_TRIAL, 1L, MAXIMUM_TRIAL_LIMIT);
|
||||
}
|
||||
|
||||
static int rootLimit(long maximumHeap, int processors) {
|
||||
return (int) Math.max(1L, Math.min(Math.min(8L, processors), maximumHeap / BYTES_PER_ROOT));
|
||||
}
|
||||
|
||||
Permit acquire(BooleanSupplier cancelled) {
|
||||
AdmissionWait wait = new AdmissionWait(Objects.requireNonNull(cancelled));
|
||||
try {
|
||||
ForkJoinPool.managedBlock(wait);
|
||||
wait.requireActive();
|
||||
return new Permit(permits);
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (wait.acquired) {
|
||||
permits.release();
|
||||
}
|
||||
throw new CancellationException("Hydrology planning admission interrupted.");
|
||||
} catch (RuntimeException | Error failure) {
|
||||
if (wait.acquired) {
|
||||
permits.release();
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Permit implements AutoCloseable {
|
||||
private final Semaphore permits;
|
||||
private final AtomicBoolean released = new AtomicBoolean();
|
||||
|
||||
private Permit(Semaphore permits) {
|
||||
this.permits = permits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (released.compareAndSet(false, true)) {
|
||||
permits.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AdmissionWait implements ForkJoinPool.ManagedBlocker {
|
||||
private final BooleanSupplier cancelled;
|
||||
private boolean acquired;
|
||||
|
||||
private AdmissionWait(BooleanSupplier cancelled) {
|
||||
this.cancelled = cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
requireActive();
|
||||
if (!acquired) {
|
||||
try {
|
||||
acquired = permits.tryAcquire(0L, TimeUnit.NANOSECONDS);
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new CancellationException("Hydrology planning admission interrupted.");
|
||||
}
|
||||
}
|
||||
return acquired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() throws InterruptedException {
|
||||
requireActive();
|
||||
if (!acquired) {
|
||||
acquired = permits.tryAcquire(100L, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return acquired;
|
||||
}
|
||||
|
||||
private void requireActive() {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Hydrology planning admission interrupted.");
|
||||
}
|
||||
if (cancelled.getAsBoolean()) {
|
||||
throw new CancellationException("Hydrology tile cache is closed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
public record HydrologyPoint(int x, int y, int z) {
|
||||
public long packedColumn() {
|
||||
return RiverFootprint.pack(x, z);
|
||||
}
|
||||
|
||||
public long distanceSquared2D(HydrologyPoint other) {
|
||||
long deltaX = (long) x - other.x;
|
||||
long deltaZ = (long) z - other.z;
|
||||
return deltaX * deltaX + deltaZ * deltaZ;
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record HydrologyRegionalCoarsePlan(
|
||||
HydrologySampledGrid grid,
|
||||
List<OutletCandidate> outlets,
|
||||
HydrologyRegionalGraph.Tree tree,
|
||||
List<Integer> sources,
|
||||
List<HydrologyRegionalEnvelope> envelopes,
|
||||
double longest,
|
||||
boolean coastal,
|
||||
int weight
|
||||
) {
|
||||
HydrologyRegionalCoarsePlan {
|
||||
outlets = List.copyOf(outlets);
|
||||
sources = List.copyOf(sources);
|
||||
envelopes = List.copyOf(envelopes);
|
||||
}
|
||||
|
||||
boolean intersects(SurfaceBounds bounds) {
|
||||
if (coastal) {
|
||||
return true;
|
||||
}
|
||||
for (HydrologyRegionalEnvelope envelope : envelopes) {
|
||||
if (envelope.intersects(bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
-246
@@ -1,246 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyRegionalConnectivity {
|
||||
private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final Long2LongOpenHashMap water = new Long2LongOpenHashMap();
|
||||
private final Long2ObjectOpenHashMap<LongArrayList> disjoint = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
void add(List<SurfaceLayerColumn> columns) {
|
||||
for (SurfaceLayerColumn column : columns) {
|
||||
HydrologyColumnLayer layer = column.layer();
|
||||
if (layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) {
|
||||
addFluidColumn(column.x(), column.z(), layer.bedY(), layer.fluidHeadY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add(HydrologyColumnSample column) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) {
|
||||
addFluidColumn(column.x(), column.z(), layer.bedY(), layer.fluidHeadY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addFluidColumn(int x, int z, int bed, int head) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
long interval = levels(bed, head);
|
||||
if (!water.containsKey(key)) {
|
||||
water.put(key, interval);
|
||||
return;
|
||||
}
|
||||
long previous = water.get(key);
|
||||
LongArrayList gaps = disjoint.get(key);
|
||||
if (gaps == null && bed <= (int) previous && (int) (previous >> 32) <= head) {
|
||||
water.put(key, levels(Math.min(bed, (int) (previous >> 32)), Math.max(head, (int) previous)));
|
||||
return;
|
||||
}
|
||||
if (gaps == null) {
|
||||
gaps = new LongArrayList(3);
|
||||
gaps.add(previous);
|
||||
disjoint.put(key, gaps);
|
||||
}
|
||||
gaps.add(interval);
|
||||
gaps.sort(null);
|
||||
int retained = 1;
|
||||
for (int index = 1; index < gaps.size(); index++) {
|
||||
long first = gaps.getLong(retained - 1);
|
||||
long next = gaps.getLong(index);
|
||||
if ((int) (next >> 32) <= (int) first) {
|
||||
gaps.set(retained - 1, levels((int) (first >> 32), Math.max((int) first, (int) next)));
|
||||
} else {
|
||||
gaps.set(retained++, next);
|
||||
}
|
||||
}
|
||||
gaps.size(retained);
|
||||
water.put(key, gaps.getLong(0));
|
||||
if (retained == 1) {
|
||||
disjoint.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
boolean connected(RiverCourse course, HydrologyTerrainSampler sampler, HydrologyPlannerSettings settings) {
|
||||
if (water.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
LongOpenHashSet visited = new LongOpenHashSet(water.size());
|
||||
if (!disjoint.isEmpty()) {
|
||||
if (!connectedIntervals(visited)) {
|
||||
return false;
|
||||
}
|
||||
return reachesTerminal(course, sampler, settings, visited);
|
||||
}
|
||||
LongArrayFIFOQueue queue = new LongArrayFIFOQueue();
|
||||
long start = water.keySet().iterator().nextLong();
|
||||
queue.enqueue(start);
|
||||
visited.add(start);
|
||||
while (!queue.isEmpty()) {
|
||||
long current = queue.dequeueLong();
|
||||
long currentLevels = water.get(current);
|
||||
int currentBed = (int) (currentLevels >> 32);
|
||||
int currentHead = (int) currentLevels;
|
||||
int x = RiverFootprint.unpackX(current);
|
||||
int z = RiverFootprint.unpackZ(current);
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long next = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
if (!water.containsKey(next) || visited.contains(next)) {
|
||||
continue;
|
||||
}
|
||||
long levels = water.get(next);
|
||||
if (Math.max(currentBed, (int) (levels >> 32)) >= Math.min(currentHead, (int) levels)) {
|
||||
continue;
|
||||
}
|
||||
visited.add(next);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
if (visited.size() != water.size()) {
|
||||
return false;
|
||||
}
|
||||
return reachesTerminal(course, sampler, settings, visited);
|
||||
}
|
||||
|
||||
private boolean connectedIntervals(LongOpenHashSet visited) {
|
||||
int count = water.size();
|
||||
for (LongArrayList intervals : disjoint.values()) {
|
||||
count += intervals.size() - 1;
|
||||
}
|
||||
long[] columns = new long[count];
|
||||
long[] intervals = new long[count];
|
||||
Long2IntOpenHashMap starts = new Long2IntOpenHashMap(water.size());
|
||||
starts.defaultReturnValue(-1);
|
||||
int index = 0;
|
||||
for (long column : water.keySet()) {
|
||||
starts.put(column, index);
|
||||
LongArrayList layers = disjoint.get(column);
|
||||
int size = layers == null ? 1 : layers.size();
|
||||
for (int layer = 0; layer < size; layer++) {
|
||||
columns[index] = column;
|
||||
intervals[index++] = layers == null ? water.get(column) : layers.getLong(layer);
|
||||
}
|
||||
}
|
||||
boolean[] reached = new boolean[count];
|
||||
IntArrayFIFOQueue queue = new IntArrayFIFOQueue();
|
||||
queue.enqueue(0);
|
||||
reached[0] = true;
|
||||
visited.add(columns[0]);
|
||||
int reachedCount = 1;
|
||||
while (!queue.isEmpty()) {
|
||||
int current = queue.dequeueInt();
|
||||
int currentBed = (int) (intervals[current] >> 32);
|
||||
int currentHead = (int) intervals[current];
|
||||
int x = RiverFootprint.unpackX(columns[current]);
|
||||
int z = RiverFootprint.unpackZ(columns[current]);
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long nextColumn = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
int start = starts.get(nextColumn);
|
||||
if (start < 0) {
|
||||
continue;
|
||||
}
|
||||
LongArrayList layers = disjoint.get(nextColumn);
|
||||
int end = start + (layers == null ? 1 : layers.size());
|
||||
for (int next = start; next < end; next++) {
|
||||
if (reached[next] || Math.max(currentBed, (int) (intervals[next] >> 32))
|
||||
>= Math.min(currentHead, (int) intervals[next])) {
|
||||
continue;
|
||||
}
|
||||
reached[next] = true;
|
||||
reachedCount++;
|
||||
visited.add(nextColumn);
|
||||
queue.enqueue(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachedCount == count;
|
||||
}
|
||||
|
||||
private boolean reachesTerminal(RiverCourse course, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings, LongOpenHashSet visited) {
|
||||
HydraulicSegment first = course.segments().getFirst();
|
||||
HydraulicSegment last = course.segments().getLast();
|
||||
return reaches(last, false, sampler, settings, visited)
|
||||
&& (first.type() != HydrologyFeatureType.MOUTH || reaches(first, true, sampler, settings, visited));
|
||||
}
|
||||
|
||||
private boolean reaches(HydraulicSegment mouth, boolean upstream, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings, LongOpenHashSet visited) {
|
||||
int seaLevel = settings.seaLevel();
|
||||
if (mouth.type() != HydrologyFeatureType.MOUTH || mouth.upstreamHeadY() != seaLevel
|
||||
|| mouth.downstreamHeadY() != seaLevel) {
|
||||
return false;
|
||||
}
|
||||
HydrologyPoint endpoint = upstream ? mouth.start() : mouth.end();
|
||||
HydrologyTerrainSample terrain = sampler.sample(endpoint.x(), endpoint.z());
|
||||
if (terrain == null || !terrain.ocean() || terrain.naturalHeight() >= seaLevel) {
|
||||
return false;
|
||||
}
|
||||
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forMouth(settings, sampler, mouth);
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(mouth.centerline());
|
||||
int step = upstream ? 1 : -1;
|
||||
int previousX = endpoint.x();
|
||||
int previousZ = endpoint.z();
|
||||
for (int station = upstream ? 0 : centerline.size() - 1; station >= 0 && station < centerline.size(); station += step) {
|
||||
int x = centerline.x()[station];
|
||||
int z = centerline.z()[station];
|
||||
if (x != previousX && z != previousZ) {
|
||||
boolean firstBridge = receiver.receivingWater(previousX, z, seaLevel);
|
||||
boolean secondBridge = receiver.receivingWater(x, previousZ, seaLevel);
|
||||
if (!firstBridge && !secondBridge) {
|
||||
return false;
|
||||
}
|
||||
if (firstBridge && touchesOwnedWater(previousX, z, seaLevel, visited)
|
||||
|| secondBridge && touchesOwnedWater(x, previousZ, seaLevel, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!receiver.receivingWater(x, z, seaLevel)) {
|
||||
return false;
|
||||
}
|
||||
if (touchesOwnedWater(x, z, seaLevel, visited)) {
|
||||
return true;
|
||||
}
|
||||
previousX = x;
|
||||
previousZ = z;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean touchesOwnedWater(int x, int z, int seaLevel, LongOpenHashSet visited) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
long key = RiverFootprint.pack(x + offset[0], z + offset[1]);
|
||||
if (!visited.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
LongArrayList layers = disjoint.get(key);
|
||||
if (layers == null) {
|
||||
if ((int) water.get(key) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (int index = 0; index < layers.size(); index++) {
|
||||
if ((int) layers.getLong(index) == seaLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static long levels(int bed, int head) {
|
||||
return ((long) bed << 32) | (head & 0xffffffffL);
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record HydrologyRegionalCourseGraph(List<DrainageNode> nodes, List<DrainageEdge> edges, int discharge) {
|
||||
private static final long NODE_SALT = 0x5245475055424c49L;
|
||||
|
||||
HydrologyRegionalCourseGraph {
|
||||
nodes = List.copyOf(nodes);
|
||||
edges = List.copyOf(edges);
|
||||
}
|
||||
|
||||
static HydrologyRegionalCourseGraph create(HydrologyTerrainSampler sampler, Options options) {
|
||||
List<HydrologyPoint> path = options.path();
|
||||
double[] distance = new double[path.size()];
|
||||
IntArrayList selected = new IntArrayList();
|
||||
selected.add(0);
|
||||
double next = options.spacing();
|
||||
for (int index = 1; index < path.size(); index++) {
|
||||
HydrologyPoint previous = path.get(index - 1);
|
||||
HydrologyPoint point = path.get(index);
|
||||
distance[index] = distance[index - 1] + StrictMath.hypot(
|
||||
(double) point.x() - previous.x(), (double) point.z() - previous.z());
|
||||
if (distance[index] >= next || index == path.size() - 1) {
|
||||
selected.add(index);
|
||||
next = distance[index] + options.spacing();
|
||||
}
|
||||
}
|
||||
ArrayList<DrainageNode> nodes = new ArrayList<>(selected.size());
|
||||
ArrayList<DrainageEdge> edges = new ArrayList<>(Math.max(0, selected.size() - 1));
|
||||
double length = distance[distance.length - 1];
|
||||
int contribution = 1;
|
||||
for (int index = 0; index < selected.size(); index++) {
|
||||
int station = selected.getInt(index);
|
||||
HydrologyPoint point = path.get(station);
|
||||
HydrologyTerrainSample terrain = Objects.requireNonNull(sampler.sample(point.x(), point.z()),
|
||||
"Accepted regional drainage terrain");
|
||||
DrainageNode node = new DrainageNode(HydrologyHash.mix(options.courseId(), NODE_SALT, index, point.x(), point.z()),
|
||||
point.x(), point.z(), terrain, length - distance[station], options.outletId());
|
||||
nodes.add(node);
|
||||
contribution = Math.max(contribution, options.flow().contributionAt(point.x(), point.z()));
|
||||
if (index > 0) {
|
||||
DrainageNode upstream = nodes.get(index - 1);
|
||||
edges.add(new DrainageEdge(HydrologyHash.mix(options.courseId(), upstream.id(), node.id()),
|
||||
upstream.id(), node.id(), options.outletId(), upstream.potential() - node.potential(),
|
||||
contribution, 0, List.of(upstream.naturalPoint(), node.naturalPoint())));
|
||||
}
|
||||
}
|
||||
return new HydrologyRegionalCourseGraph(nodes, edges, contribution);
|
||||
}
|
||||
|
||||
record Options(long courseId, long outletId, int spacing, List<HydrologyPoint> path, HydrologyRegionalFlow flow) {
|
||||
Options {
|
||||
path = List.copyOf(path);
|
||||
Objects.requireNonNull(flow, "Regional drainage flow");
|
||||
if (spacing < 1 || path.size() < 2) {
|
||||
throw new IllegalArgumentException("Regional drainage requires positive spacing and a complete path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record HydrologyRegionalEnvelope(long minimumX, long minimumZ, long maximumX, long maximumZ) {
|
||||
private static final HydrologyRegionalEnvelope UNBOUNDED = new HydrologyRegionalEnvelope(
|
||||
Long.MIN_VALUE, Long.MIN_VALUE, Long.MAX_VALUE, Long.MAX_VALUE);
|
||||
|
||||
static HydrologyRegionalEnvelope of(List<HydrologyPoint> guide, HydrologyPlannerSettings settings, int footprintRadius) {
|
||||
long minimumX = Long.MAX_VALUE;
|
||||
long minimumZ = Long.MAX_VALUE;
|
||||
long maximumX = Long.MIN_VALUE;
|
||||
long maximumZ = Long.MIN_VALUE;
|
||||
for (HydrologyPoint point : guide) {
|
||||
minimumX = Math.min(minimumX, point.x());
|
||||
minimumZ = Math.min(minimumZ, point.z());
|
||||
maximumX = Math.max(maximumX, point.x());
|
||||
maximumZ = Math.max(maximumZ, point.z());
|
||||
}
|
||||
if (guide.isEmpty()) {
|
||||
return UNBOUNDED;
|
||||
}
|
||||
int spacing = settings.routing().regional().sampleSpacing();
|
||||
double commonPadding = spacing + spacing * settings.geometry().meanders().maximumOffsetRatio()
|
||||
+ 256D + 2D + Math.max(footprintRadius, (long) settings.publicationRadius() + 1L);
|
||||
double paddingX = StrictMath.ceil(commonPadding + (maximumX - minimumX + 2D * spacing) / 8D);
|
||||
double paddingZ = StrictMath.ceil(commonPadding + (maximumZ - minimumZ + 2D * spacing) / 8D);
|
||||
if (!Double.isFinite(paddingX) || !Double.isFinite(paddingZ)
|
||||
|| minimumX - paddingX < Integer.MIN_VALUE || maximumX + paddingX > Integer.MAX_VALUE
|
||||
|| minimumZ - paddingZ < Integer.MIN_VALUE || maximumZ + paddingZ > Integer.MAX_VALUE) {
|
||||
return UNBOUNDED;
|
||||
}
|
||||
return new HydrologyRegionalEnvelope(minimumX - (long) paddingX, minimumZ - (long) paddingZ,
|
||||
maximumX + (long) paddingX, maximumZ + (long) paddingZ);
|
||||
}
|
||||
|
||||
boolean intersects(SurfaceBounds bounds) {
|
||||
return maximumX >= bounds.minimumX() && minimumX <= bounds.maximumX()
|
||||
&& maximumZ >= bounds.minimumZ() && minimumZ <= bounds.maximumZ();
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelPrecondition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class HydrologyRegionalFalls implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private static final int WINDOW_SIZE = 128;
|
||||
private static final int MAXIMUM_ROOF_WINDOWS = 4;
|
||||
private static final int[][] NEIGHBORS = {{0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final RiverCourse course;
|
||||
private final HydrologySurfaceDropRaster drops;
|
||||
private final HydrologyTerrainSampler receiver;
|
||||
private final LongOpenHashSet witnessColumns;
|
||||
private final ArrayList<SurfaceLayerColumn> exposed;
|
||||
private final SurfaceFootprintCompiler surface;
|
||||
private final LinkedHashMap<HydrologyTileKey, Map<Long, HydrologyColumnSample>> roofWindows;
|
||||
|
||||
HydrologyRegionalFalls(HydrologyPlanner planner, RiverCourse course) {
|
||||
this.planner = planner;
|
||||
this.course = course;
|
||||
this.receiver = HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course);
|
||||
this.drops = HydrologySurfaceDropRaster.compile(planner.settings, receiver, planner.geometrySampler, course);
|
||||
this.witnessColumns = new LongOpenHashSet();
|
||||
this.exposed = new ArrayList<>();
|
||||
this.surface = new SurfaceFootprintCompiler(planner.settings, this::sample, planner.geometrySampler);
|
||||
this.roofWindows = new LinkedHashMap<>(MAXIMUM_ROOF_WINDOWS + 1, 1F, true);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
for (int[] offset : NEIGHBORS) {
|
||||
witnessColumns.add(RiverFootprint.pack(column.x() + offset[0], column.z() + offset[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void retain(SurfaceFootprint footprint) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (witnessColumns.contains(RiverFootprint.pack(column.x(), column.z()))) {
|
||||
exposed.add(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<HydrologyCavePlan> validate(HydrologyRegionalNetwork network, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
if (drops.columns().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(planner.settings, receiver, planner.geometrySampler);
|
||||
HydrologyFootprintCompiler.ValidationRaster validation = compiler.compileSurfaceDropValidation(course,
|
||||
new SurfaceFootprint(exposed, 0, null, 0, 0L), drops);
|
||||
HydrologyObservedPlannedSurface planned = new HydrologyObservedPlannedSurface(this);
|
||||
CaveVoxelView view = Objects.requireNonNull(planner.caveViewFactory.create(planned));
|
||||
ArrayList<HydrologyDiagnosticCandidate> failures = new ArrayList<>();
|
||||
HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter(view,
|
||||
new HydrologyCaveCourseFilter.Options(planner.settings.underground().connectToExistingCaves(),
|
||||
planner.settings.outlets().coastalGrotto().maximumVolume(),
|
||||
planner.settings.outlets().inlandGrotto().maximumVolume()), null, null, planned)
|
||||
.filter(network.nodes(), network.edges(), network.outlets(), List.of(course), validation, failures);
|
||||
for (HydrologyDiagnosticCandidate failure : failures) {
|
||||
if (diagnostics.size() >= 64) {
|
||||
break;
|
||||
}
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(failure.id(), HydrologyCandidateKind.REGIONAL_SOURCE,
|
||||
failure.projectedType(), failure.point(), failure.rejection(), failure.detail()));
|
||||
}
|
||||
return result.courses().isEmpty() ? null : result.cavePlans();
|
||||
}
|
||||
|
||||
static HydrologyCavePlan clip(HydrologyCavePlan plan, SurfaceBounds bounds) {
|
||||
LinkedHashMap<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
LinkedHashMap<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
plan.forEachActionIn(bounds.minimumX(), bounds.minimumZ(), Math.addExact(bounds.maximumX(), 1),
|
||||
Math.addExact(bounds.maximumZ(), 1), actions::put);
|
||||
plan.allPreconditionsIn(bounds.minimumX(), bounds.minimumZ(), Math.addExact(bounds.maximumX(), 1),
|
||||
Math.addExact(bounds.maximumZ(), 1), (position, precondition) -> {
|
||||
preconditions.put(position, precondition);
|
||||
return true;
|
||||
});
|
||||
return new HydrologyCavePlan(plan.source(), plan.rejection(), actions, preconditions, plan.arbitrationWinnerSourceId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
HydrologyColumnSample column = column(x, z);
|
||||
return column == null ? naturalHeight : column.terrainHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ownsTerrain(int x, int z) {
|
||||
HydrologyColumnSample column = column(x, z);
|
||||
HydrologyColumnLayer layer = column == null ? null : column.primarySurfaceLayerOrNull();
|
||||
return layer != null && layer.terrainOwned();
|
||||
}
|
||||
|
||||
private HydrologyColumnSample column(int x, int z) {
|
||||
HydrologyTileKey key = HydrologyTileKey.fromBlock(x, z, WINDOW_SIZE);
|
||||
Map<Long, HydrologyColumnSample> columns = roofWindows.get(key);
|
||||
if (columns == null) {
|
||||
SurfaceBounds bounds = new SurfaceBounds(key.minimumBlockX(WINDOW_SIZE), key.minimumBlockZ(WINDOW_SIZE),
|
||||
Math.addExact(key.minimumBlockX(WINDOW_SIZE), WINDOW_SIZE - 1),
|
||||
Math.addExact(key.minimumBlockZ(WINDOW_SIZE), WINDOW_SIZE - 1));
|
||||
LinkedHashMap<Long, HydrologyColumnSample> built = new LinkedHashMap<>();
|
||||
for (SurfaceLayerColumn column : surface.compile(course, bounds).columns()) {
|
||||
HydrologyTerrainSample terrain = column.terrain();
|
||||
HydrologyColumnSample value = new HydrologyColumnSample(column.x(), column.z(), terrain.naturalHeight(),
|
||||
planner.settings.seaLevel(), terrain.ocean(), terrain.parentBiomeKey(), List.of(column.layer()));
|
||||
built.merge(RiverFootprint.pack(column.x(), column.z()), value, HydrologyRegionalPlanner::mergeColumn);
|
||||
}
|
||||
columns = Map.copyOf(built);
|
||||
roofWindows.put(key, columns);
|
||||
if (roofWindows.size() > MAXIMUM_ROOF_WINDOWS) {
|
||||
roofWindows.remove(roofWindows.sequencedKeySet().getFirst());
|
||||
}
|
||||
}
|
||||
return columns.get(RiverFootprint.pack(x, z));
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
return planner.naturalSampler == null ? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class HydrologyRegionalFlow {
|
||||
private final List<HydrologyPoint> points;
|
||||
private final int[] contributions;
|
||||
private final int maximum;
|
||||
|
||||
HydrologyRegionalFlow(List<HydrologyPoint> points, int[] accumulated) {
|
||||
if (points.size() != accumulated.length) {
|
||||
throw new IllegalArgumentException("Regional flow requires one contribution per guide point.");
|
||||
}
|
||||
this.points = List.copyOf(points);
|
||||
this.contributions = new int[accumulated.length];
|
||||
int previous = 1;
|
||||
for (int index = 0; index < accumulated.length; index++) {
|
||||
previous = Math.max(previous, accumulated[index]);
|
||||
contributions[index] = previous;
|
||||
}
|
||||
this.maximum = previous;
|
||||
}
|
||||
|
||||
double fraction(int x, int z) {
|
||||
return StrictMath.sqrt(contribution(x, z) / maximum);
|
||||
}
|
||||
|
||||
int contributionAt(int x, int z) {
|
||||
return Math.max(1, (int) StrictMath.round(contribution(x, z)));
|
||||
}
|
||||
|
||||
private double contribution(int x, int z) {
|
||||
double nearestDistance = Double.POSITIVE_INFINITY;
|
||||
double contribution = contributions[0];
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint next = points.get(index + 1);
|
||||
double deltaX = next.x() - first.x();
|
||||
double deltaZ = next.z() - first.z();
|
||||
double length = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = length <= 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((x - first.x()) * deltaX + (z - first.z()) * deltaZ) / length));
|
||||
double distanceX = x - first.x() - deltaX * progress;
|
||||
double distanceZ = z - first.z() - deltaZ * progress;
|
||||
double distance = distanceX * distanceX + distanceZ * distanceZ;
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
contribution = contributions[index] + (contributions[index + 1] - contributions[index]) * progress;
|
||||
}
|
||||
}
|
||||
return contribution;
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.PriorityQueue;
|
||||
|
||||
final class HydrologyRegionalGraph {
|
||||
private static final int LABELS_PER_GRID_NODE = 64;
|
||||
private static final int MAXIMUM_LABELS = 1_048_576;
|
||||
private static final Comparator<Label> LABEL_ORDER = Comparator.comparingInt((Label label) -> label.state.requiredHead())
|
||||
.thenComparingDouble(label -> label.state.cost()).thenComparingDouble(label -> label.state.length())
|
||||
.thenComparingInt(label -> label.state.index()).thenComparingInt(label -> label.state.root())
|
||||
.thenComparingInt(label -> label.state.id());
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyRegionalHydraulics hydraulics;
|
||||
|
||||
HydrologyRegionalGraph(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
}
|
||||
|
||||
Tree route(HydrologySampledGrid grid, List<OutletCandidate> outlets, boolean coastal) {
|
||||
int count = grid.nodes().size();
|
||||
int maximumLabels = (int) Math.min(MAXIMUM_LABELS, Math.max(count, (long) count * LABELS_PER_GRID_NODE));
|
||||
ArrayList<List<Label>> frontiers = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
frontiers.add(new ArrayList<>());
|
||||
}
|
||||
ArrayList<Label> labels = new ArrayList<>(count);
|
||||
PriorityQueue<Label> pending = new PriorityQueue<>(LABEL_ORDER);
|
||||
for (int outletIndex = 0; outletIndex < outlets.size(); outletIndex++) {
|
||||
int index = outlets.get(outletIndex).landIndex();
|
||||
Label label = new Label(new LabelState(labels.size(), index, planner.settings.seaLevel(),
|
||||
0D, 0D, outletIndex), null);
|
||||
admit(label, frontiers.get(index), labels, pending);
|
||||
}
|
||||
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
|
||||
double possibleInletLength = (inlet.length() + inlet.length() / 2D) * StrictMath.sqrt(2D);
|
||||
while (!pending.isEmpty() && labels.size() < maximumLabels) {
|
||||
Label current = pending.remove();
|
||||
if (!current.active) {
|
||||
continue;
|
||||
}
|
||||
HydrologyGridNode downstream = grid.node(current.state.index());
|
||||
if (current.downstream == null) {
|
||||
HydrologyPoint landward = outlets.get(current.state.root()).outlet().landwardPoint();
|
||||
HydrologyTerrainSample terrain = planner.sampler.sample(landward.x(), landward.z());
|
||||
downstream = new HydrologyGridNode(downstream.index(), downstream.gridX(), downstream.gridZ(),
|
||||
landward.x(), landward.z(), downstream.id(), terrain);
|
||||
}
|
||||
for (HydrologyGridOffset offset : HydrologySourcePlanner.ROUTING_OFFSETS) {
|
||||
if (labels.size() >= maximumLabels) {
|
||||
break;
|
||||
}
|
||||
HydrologyGridNode upstream = grid.nodeAt(downstream.gridX() + offset.x(), downstream.gridZ() + offset.z());
|
||||
if (upstream == null || !allowed(upstream.terrain(), downstream.terrain(), coastal)) {
|
||||
continue;
|
||||
}
|
||||
double distance = StrictMath.hypot(upstream.x() - downstream.x(), upstream.z() - downstream.z());
|
||||
double nextLength = current.state.length() + distance;
|
||||
if (nextLength > planner.settings.routing().maximumRouteLength()) {
|
||||
continue;
|
||||
}
|
||||
int nodeMinimum = possibleInletLength > 0D && nextLength < possibleInletLength
|
||||
? hydraulics.inletMinimumHead(upstream.terrain()) : hydraulics.minimumHead(upstream.terrain());
|
||||
int needed = coastal ? planner.settings.seaLevel() : Math.max(current.state.requiredHead(), nodeMinimum);
|
||||
if (!coastal && needed > hydraulics.maximumHead(upstream.terrain())) {
|
||||
continue;
|
||||
}
|
||||
double candidate = current.state.cost() + edgeCost(upstream, downstream, distance, coastal);
|
||||
Label label = new Label(new LabelState(labels.size(), upstream.index(), needed, candidate,
|
||||
nextLength, current.state.root()), current);
|
||||
admit(label, frontiers.get(upstream.index()), labels, pending);
|
||||
}
|
||||
}
|
||||
Label[] selected = new Label[count];
|
||||
int[] accumulation = new int[labels.size()];
|
||||
for (int index = 0; index < count; index++) {
|
||||
for (Label label : frontiers.get(index)) {
|
||||
if (selected[index] == null || LABEL_ORDER.compare(label, selected[index]) < 0) {
|
||||
selected[index] = label;
|
||||
}
|
||||
}
|
||||
if (selected[index] != null && grid.node(index).terrain().surfaceSourceAllowed()) {
|
||||
accumulation[selected[index].state.id()]++;
|
||||
}
|
||||
}
|
||||
for (int index = labels.size() - 1; index >= 0; index--) {
|
||||
Label label = labels.get(index);
|
||||
if (label.downstream != null) {
|
||||
accumulation[label.downstream.state.id()] += accumulation[label.state.id()];
|
||||
}
|
||||
}
|
||||
int[] heads = new int[labels.size()];
|
||||
int[] required = new int[labels.size()];
|
||||
int[] nominal = new int[labels.size()];
|
||||
int[] ceiling = new int[labels.size()];
|
||||
Arrays.fill(ceiling, Integer.MAX_VALUE);
|
||||
for (Label label : labels) {
|
||||
HydrologyTerrainSample terrain = grid.node(label.state.index()).terrain();
|
||||
if (label.downstream == null) {
|
||||
HydrologyPoint landward = outlets.get(label.state.root()).outlet().landwardPoint();
|
||||
terrain = planner.sampleLandBasisWithoutSlope(landward.x(), landward.z());
|
||||
}
|
||||
int index = label.state.id();
|
||||
int depth = (planner.settings.surface().minimumDepth() + planner.settings.surface().maximumDepth()) / 2;
|
||||
nominal[index] = terrain.naturalHeight() - planner.settings.surface().banks().sink() - depth;
|
||||
int minimum = terrain.naturalHeight() - planner.sourcePlanner.permittedSurfaceIncision(terrain) + depth;
|
||||
required[index] = coastal ? planner.settings.seaLevel() : Math.max(planner.settings.seaLevel(),
|
||||
Math.max(minimum, label.downstream == null ? Integer.MIN_VALUE : required[label.downstream.state.id()]));
|
||||
if (selected[label.state.index()] == label && planner.sourcePlanner.rawSourceEligible(
|
||||
grid.node(label.state.index()).terrain(), planner.settings.surface().sources(), true)) {
|
||||
ceiling[index] = Math.max(nominal[index], required[index]);
|
||||
}
|
||||
}
|
||||
for (int index = labels.size() - 1; index >= 0; index--) {
|
||||
Label label = labels.get(index);
|
||||
if (label.downstream != null) {
|
||||
int parent = label.downstream.state.id();
|
||||
ceiling[parent] = Math.min(ceiling[parent], ceiling[index]);
|
||||
}
|
||||
}
|
||||
for (Label label : labels) {
|
||||
int index = label.state.id();
|
||||
int downstream = label.downstream == null ? Integer.MIN_VALUE : heads[label.downstream.state.id()];
|
||||
heads[index] = coastal ? planner.settings.seaLevel()
|
||||
: Math.max(Math.max(required[index], downstream), Math.min(nominal[index], ceiling[index]));
|
||||
}
|
||||
return new Tree(selected, accumulation, List.copyOf(outlets), heads);
|
||||
}
|
||||
|
||||
List<Integer> path(Tree tree, int source) {
|
||||
ArrayList<Integer> indices = new ArrayList<>();
|
||||
Label current = tree.selected()[source];
|
||||
while (current != null) {
|
||||
indices.add(current.state.index());
|
||||
current = current.downstream;
|
||||
}
|
||||
return List.copyOf(indices);
|
||||
}
|
||||
|
||||
private static void admit(Label candidate, List<Label> frontier, List<Label> labels, PriorityQueue<Label> pending) {
|
||||
for (Label existing : frontier) {
|
||||
if (dominates(existing, candidate)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (int index = frontier.size() - 1; index >= 0; index--) {
|
||||
Label existing = frontier.get(index);
|
||||
if (dominates(candidate, existing)) {
|
||||
existing.active = false;
|
||||
frontier.remove(index);
|
||||
}
|
||||
}
|
||||
frontier.add(candidate);
|
||||
labels.add(candidate);
|
||||
pending.add(candidate);
|
||||
}
|
||||
|
||||
private static boolean dominates(Label first, Label second) {
|
||||
if ((first.downstream == null || second.downstream == null)
|
||||
&& (first.downstream != null || second.downstream != null || first.state.root() != second.state.root())) {
|
||||
return false;
|
||||
}
|
||||
return first.state.requiredHead() <= second.state.requiredHead()
|
||||
&& first.state.cost() <= second.state.cost() && first.state.length() <= second.state.length();
|
||||
}
|
||||
|
||||
private boolean allowed(HydrologyTerrainSample upstream, HydrologyTerrainSample downstream, boolean coastal) {
|
||||
if (upstream.ocean() || upstream.naturalHeight() < planner.settings.seaLevel() || !upstream.transitAllowed() || !upstream.drainsInto(downstream)
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(upstream, downstream)) {
|
||||
return false;
|
||||
}
|
||||
if (coastal) {
|
||||
int incision = upstream.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth();
|
||||
return incision <= planner.settings.routing().regional().maximumCoastalIncision()
|
||||
&& downstream.drainsInto(upstream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private double edgeCost(HydrologyGridNode upstream, HydrologyGridNode downstream, double distance, boolean coastal) {
|
||||
HydrologyPlannerSettings.Routing routing = planner.settings.routing();
|
||||
HydrologyTerrainSample first = upstream.terrain();
|
||||
HydrologyTerrainSample second = downstream.terrain();
|
||||
double rise = Math.max(0D, second.naturalHeight() - first.naturalHeight());
|
||||
double slope = (first.slope() + second.slope()) * 0.5D;
|
||||
double incision = Math.max(0D, first.naturalHeight() - planner.settings.seaLevel());
|
||||
double resistance = coastal ? incision * incision * routing.uphillPenalty() : rise * routing.uphillPenalty();
|
||||
double policy = Math.max(0.01D, (first.routingMultiplier() + second.routingMultiplier()) * 0.5D);
|
||||
return Math.max(0.001D, (distance + resistance + slope * routing.slopePenalty()
|
||||
+ first.routingCost() + second.routingCost()) * policy);
|
||||
}
|
||||
|
||||
record Tree(Label[] selected, int[] accumulation, List<OutletCandidate> outlets, int[] heads) {
|
||||
int retainedLabelCount() {
|
||||
BitSet retained = new BitSet(accumulation.length);
|
||||
for (Label label : selected) {
|
||||
for (Label current = label; current != null; current = current.downstream) {
|
||||
int id = current.state.id();
|
||||
if (retained.get(id)) {
|
||||
break;
|
||||
}
|
||||
retained.set(id);
|
||||
}
|
||||
}
|
||||
return retained.cardinality();
|
||||
}
|
||||
|
||||
int root(int source) {
|
||||
return selected[source] == null ? -1 : selected[source].state.root();
|
||||
}
|
||||
|
||||
double cost(int source) {
|
||||
return selected[source] == null ? Double.POSITIVE_INFINITY : selected[source].state.cost();
|
||||
}
|
||||
|
||||
double length(int source) {
|
||||
return selected[source] == null ? 0D : selected[source].state.length();
|
||||
}
|
||||
|
||||
List<HydrologyPoint> centerline(HydrologySampledGrid grid, int source) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (Label label = selected[source]; label != null; label = label.downstream) {
|
||||
HydrologyGridNode node = grid.node(label.state.index());
|
||||
points.add(new HydrologyPoint(node.x(), heads[label.state.id()], node.z()));
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
int[] contributions(int source) {
|
||||
int count = 0;
|
||||
for (Label label = selected[source]; label != null; label = label.downstream) {
|
||||
count++;
|
||||
}
|
||||
int[] contributions = new int[count];
|
||||
int index = 0;
|
||||
for (Label label = selected[source]; label != null; label = label.downstream) {
|
||||
contributions[index++] = accumulation[label.state.id()];
|
||||
}
|
||||
return contributions;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Label {
|
||||
private final LabelState state;
|
||||
private final Label downstream;
|
||||
private boolean active = true;
|
||||
|
||||
Label(LabelState state, Label downstream) {
|
||||
this.state = state;
|
||||
this.downstream = downstream;
|
||||
}
|
||||
}
|
||||
|
||||
record LabelState(int id, int index, int requiredHead, double cost, double length, int root) {
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
final class HydrologyRegionalHydraulics {
|
||||
private final HydrologyPlannerSettings settings;
|
||||
|
||||
HydrologyRegionalHydraulics(HydrologyPlannerSettings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
int minimumHead(HydrologyTerrainSample terrain) {
|
||||
return minimumHead(terrain, terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision()));
|
||||
}
|
||||
|
||||
int inletMinimumHead(HydrologyTerrainSample terrain) {
|
||||
return minimumHead(terrain, Math.max(terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision()),
|
||||
settings.surface().banks().inlet().maximumIncision()));
|
||||
}
|
||||
|
||||
int maximumHead(HydrologyTerrainSample terrain) {
|
||||
return Math.max(settings.seaLevel(), terrain.naturalHeight() - settings.surface().banks().sink());
|
||||
}
|
||||
|
||||
private int minimumHead(HydrologyTerrainSample terrain, int incision) {
|
||||
int permitted = Math.min(incision, (int) StrictMath.floor(incision * terrain.incisionMultiplier()));
|
||||
int depth = (int) StrictMath.round(Math.max(1D, Math.min(settings.surface().maximumDepth() * 2D,
|
||||
settings.surface().minimumDepth() * terrain.depthMultiplier())));
|
||||
return Math.max(settings.seaLevel(), terrain.naturalHeight() - permitted + depth);
|
||||
}
|
||||
|
||||
}
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class HydrologyRegionalMorphology {
|
||||
private static final int MAXIMUM_STATIONS = 129;
|
||||
private static final double MAXIMUM_LATERAL_REACH = 64D;
|
||||
private static final double GENTLE_GRADE = 1D / 64D;
|
||||
private static final double STEEP_GRADE = 0.25D;
|
||||
private static final double[] LATERAL_OFFSETS = {-1D, -0.5D, 0.5D, 1D};
|
||||
|
||||
private final Location[] locations;
|
||||
private final double[] freedom;
|
||||
|
||||
private HydrologyRegionalMorphology(Location[] locations, double[] freedom) {
|
||||
this.locations = locations;
|
||||
this.freedom = freedom;
|
||||
}
|
||||
|
||||
static HydrologyRegionalMorphology sample(List<HydrologyPoint> guide, HydrologyTerrainSampler sampler,
|
||||
HydrologyPlannerSettings settings) {
|
||||
double length = 0D;
|
||||
for (int index = 1; index < guide.size(); index++) {
|
||||
length += StrictMath.hypot((double) guide.get(index).x() - guide.get(index - 1).x(),
|
||||
(double) guide.get(index).z() - guide.get(index - 1).z());
|
||||
}
|
||||
if (length <= 0D || length > settings.routing().maximumRouteLength()) {
|
||||
return new HydrologyRegionalMorphology(new Location[0], new double[0]);
|
||||
}
|
||||
int intervals = Math.min(MAXIMUM_STATIONS - 1,
|
||||
Math.max(1, (int) StrictMath.ceil(length / settings.routing().regional().sampleSpacing())));
|
||||
Location[] locations = locations(guide, length, intervals);
|
||||
HydrologyTerrainSample[] centers = new HydrologyTerrainSample[locations.length];
|
||||
Map<Long, HydrologyTerrainSample> sampled = new HashMap<>();
|
||||
for (int index = 0; index < locations.length; index++) {
|
||||
Location location = locations[index];
|
||||
centers[index] = sample(sampler, sampled, location.x(), location.z());
|
||||
}
|
||||
double reach = Math.min(MAXIMUM_LATERAL_REACH, Math.max(settings.surface().maximumWidth() * 2D,
|
||||
settings.routing().regional().sampleSpacing() / 8D));
|
||||
double[] freedom = new double[locations.length];
|
||||
for (int index = 0; index < locations.length; index++) {
|
||||
HydrologyTerrainSample center = centers[index];
|
||||
if (!land(center, settings.seaLevel())) {
|
||||
continue;
|
||||
}
|
||||
int before = Math.max(0, index - 1);
|
||||
int after = Math.min(locations.length - 1, index + 1);
|
||||
if (centers[before] == null || centers[after] == null) {
|
||||
continue;
|
||||
}
|
||||
double distance = length / intervals;
|
||||
double grade = Math.max(StrictMath.abs((double) center.naturalHeight() - centers[before].naturalHeight()),
|
||||
StrictMath.abs((double) centers[after].naturalHeight() - center.naturalHeight())) / distance;
|
||||
double gradeFreedom = 1D - smooth((grade - GENTLE_GRADE) / (STEEP_GRADE - GENTLE_GRADE));
|
||||
freedom[index] = gradeFreedom * room(locations[index], center, sampler, sampled, settings, reach);
|
||||
}
|
||||
return new HydrologyRegionalMorphology(locations, smooth(freedom));
|
||||
}
|
||||
|
||||
double freedomAt(double x, double z) {
|
||||
double nearest = Double.POSITIVE_INFINITY;
|
||||
double selected = 0D;
|
||||
for (int index = 0; index + 1 < locations.length; index++) {
|
||||
Location first = locations[index];
|
||||
Location second = locations[index + 1];
|
||||
double deltaX = (double) second.x() - first.x();
|
||||
double deltaZ = (double) second.z() - first.z();
|
||||
double squared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = squared == 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((x - first.x()) * deltaX + (z - first.z()) * deltaZ) / squared));
|
||||
double offsetX = x - first.x() - deltaX * progress;
|
||||
double offsetZ = z - first.z() - deltaZ * progress;
|
||||
double distance = offsetX * offsetX + offsetZ * offsetZ;
|
||||
if (distance < nearest) {
|
||||
nearest = distance;
|
||||
selected = freedom[index] + (freedom[index + 1] - freedom[index]) * smooth(progress);
|
||||
}
|
||||
}
|
||||
return Math.max(0D, Math.min(1D, selected));
|
||||
}
|
||||
|
||||
private static Location[] locations(List<HydrologyPoint> guide, double length, int intervals) {
|
||||
Location[] locations = new Location[intervals + 1];
|
||||
int segment = 0;
|
||||
double traversed = 0D;
|
||||
for (int index = 0; index <= intervals; index++) {
|
||||
double distance = length * index / intervals;
|
||||
HydrologyPoint first = guide.get(segment);
|
||||
HydrologyPoint second = guide.get(segment + 1);
|
||||
double span = StrictMath.hypot((double) second.x() - first.x(), (double) second.z() - first.z());
|
||||
while (segment + 2 < guide.size() && (span == 0D || traversed + span < distance)) {
|
||||
traversed += span;
|
||||
segment++;
|
||||
first = guide.get(segment);
|
||||
second = guide.get(segment + 1);
|
||||
span = StrictMath.hypot((double) second.x() - first.x(), (double) second.z() - first.z());
|
||||
}
|
||||
double progress = span == 0D ? 0D : Math.max(0D, Math.min(1D, (distance - traversed) / span));
|
||||
locations[index] = new Location(
|
||||
(int) StrictMath.round(first.x() + ((double) second.x() - first.x()) * progress),
|
||||
(int) StrictMath.round(first.z() + ((double) second.z() - first.z()) * progress),
|
||||
span == 0D ? 1D : (second.x() - first.x()) / span,
|
||||
span == 0D ? 0D : (second.z() - first.z()) / span);
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
private static double room(Location location, HydrologyTerrainSample center, HydrologyTerrainSampler sampler,
|
||||
Map<Long, HydrologyTerrainSample> sampled, HydrologyPlannerSettings settings, double reach) {
|
||||
int incision = center.surfacePolicy().maximumIncision(settings.surface().maximumIncision());
|
||||
double allowance = Math.max(1D, Math.min(incision, StrictMath.floor(incision * center.incisionMultiplier())));
|
||||
double room = 1D;
|
||||
for (double scale : LATERAL_OFFSETS) {
|
||||
double offset = reach * scale;
|
||||
int x = (int) StrictMath.round(location.x() - location.tangentZ() * offset);
|
||||
int z = (int) StrictMath.round(location.z() + location.tangentX() * offset);
|
||||
HydrologyTerrainSample side = sample(sampler, sampled, x, z);
|
||||
if (!land(side, settings.seaLevel()) || !center.drainsInto(side) || !side.drainsInto(center)
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(center, side)) {
|
||||
return 0D;
|
||||
}
|
||||
double relief = StrictMath.abs((double) side.naturalHeight() - center.naturalHeight());
|
||||
room = Math.min(room, 1D - smooth((relief - allowance / 4D) / allowance));
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
private static boolean land(HydrologyTerrainSample sample, int seaLevel) {
|
||||
return sample != null && !sample.ocean() && sample.naturalHeight() >= seaLevel && sample.transitAllowed();
|
||||
}
|
||||
|
||||
private static HydrologyTerrainSample sample(HydrologyTerrainSampler sampler, Map<Long, HydrologyTerrainSample> sampled,
|
||||
int x, int z) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
if (!sampled.containsKey(key)) {
|
||||
sampled.put(key, sampler.sample(x, z));
|
||||
}
|
||||
return sampled.get(key);
|
||||
}
|
||||
|
||||
private static double[] smooth(double[] values) {
|
||||
double[] smoothed = new double[values.length];
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
double total = 0D;
|
||||
int weight = 0;
|
||||
for (int sample = Math.max(0, index - 2); sample <= Math.min(values.length - 1, index + 2); sample++) {
|
||||
int strength = 3 - Math.abs(sample - index);
|
||||
total += values[sample] * strength;
|
||||
weight += strength;
|
||||
}
|
||||
smoothed[index] = total / weight;
|
||||
}
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
private static double smooth(double value) {
|
||||
double bounded = Math.max(0D, Math.min(1D, value));
|
||||
return bounded * bounded * (3D - 2D * bounded);
|
||||
}
|
||||
|
||||
private record Location(int x, int z, double tangentX, double tangentZ) {
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record HydrologyRegionalNetwork(
|
||||
List<DrainageNode> nodes,
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
List<RiverCourse> courses,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
List<HydrologyCavePlan> cavePlans
|
||||
) {
|
||||
static final HydrologyRegionalNetwork EMPTY = new HydrologyRegionalNetwork(List.of(), List.of(), List.of(), List.of(), List.of(), List.of());
|
||||
|
||||
HydrologyRegionalNetwork {
|
||||
nodes = List.copyOf(nodes);
|
||||
edges = List.copyOf(edges);
|
||||
outlets = List.copyOf(outlets);
|
||||
courses = List.copyOf(courses);
|
||||
diagnostics = List.copyOf(diagnostics);
|
||||
cavePlans = List.copyOf(cavePlans);
|
||||
}
|
||||
|
||||
int stations() {
|
||||
int count = 0;
|
||||
for (RiverCourse course : courses) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
count = Math.addExact(count, segment.centerline().size());
|
||||
}
|
||||
}
|
||||
for (HydrologyCavePlan plan : cavePlans) {
|
||||
count = Math.addExact(count, plan.baselinePreconditions().size());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
-953
@@ -1,953 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCourseBuilder;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCourseResult;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceExcavationMetrics;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceTerminal;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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.OptionalLong;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
final class HydrologyRegionalPlanner {
|
||||
private static final long BASIN_SALT = 0x524547424153494eL;
|
||||
private static final long COURSE_SALT = 0x524547434f555253L;
|
||||
private static final long NODE_SALT = 0x5245474e4f444553L;
|
||||
private static final double AUTHORED_WIDTH_WEIGHT = 0.65D;
|
||||
private static final int MAXIMUM_SOURCE_TRIALS = 24;
|
||||
private static final int SOURCE_TRIAL_WINDOW = 16;
|
||||
private static final int MAXIMUM_DIAGNOSTICS = 64;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyPlannerSettings.Regional settings;
|
||||
private final HydrologyRegionalGraph graph;
|
||||
private final HydrologyRegionalRoute routes;
|
||||
private final Cache<HydrologyTileKey, HydrologyRegionalNetwork> drafts;
|
||||
private final Cache<HydrologyTileKey, HydrologyRegionalCoarsePlan> coarsePlans;
|
||||
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<HydrologyRegionalCoarsePlan>> preparingCoarsePlans = new HashMap<>();
|
||||
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<HydrologyRegionalNetwork>> compilingDrafts = new HashMap<>();
|
||||
private final Object draftPublicationLock = new Object();
|
||||
private long draftEpoch;
|
||||
|
||||
HydrologyRegionalPlanner(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.settings = planner.settings.routing().regional();
|
||||
this.graph = new HydrologyRegionalGraph(planner);
|
||||
this.routes = new HydrologyRegionalRoute(planner);
|
||||
HydrologyCacheBudget cacheBudget = HydrologyCacheBudget.runtime(settings.enabled());
|
||||
this.drafts = Caffeine.newBuilder()
|
||||
.maximumWeight(cacheBudget.regionalDraftBytes())
|
||||
.weigher((HydrologyTileKey key, HydrologyRegionalNetwork network) -> HydrologyCacheWeights.bounded(
|
||||
HydrologyCacheWeights.regionalNetwork(network), cacheBudget.regionalDraftBytes(),
|
||||
settings.maximumCachedBasins(), network.stations(), settings.maximumCachedStations()))
|
||||
.build();
|
||||
this.coarsePlans = Caffeine.newBuilder()
|
||||
.maximumWeight(cacheBudget.regionalCoarseBytes())
|
||||
.weigher((HydrologyTileKey key, HydrologyRegionalCoarsePlan plan) -> HydrologyCacheWeights.bounded(
|
||||
HydrologyCacheWeights.regionalCoarse(plan), cacheBudget.regionalCoarseBytes(),
|
||||
settings.maximumCachedBasins(), plan.weight(), settings.maximumCachedStations()))
|
||||
.build();
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
return settings.enabled() && planner.settings.surface().enabled()
|
||||
&& planner.settings.surface().sources().enabled() && planner.settings.outlets().oceanEnabled()
|
||||
&& settings.minimumLength() <= planner.settings.routing().maximumRouteLength();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
synchronized (draftPublicationLock) {
|
||||
draftEpoch++;
|
||||
compilingDrafts.clear();
|
||||
preparingCoarsePlans.clear();
|
||||
coarsePlans.invalidateAll();
|
||||
drafts.invalidateAll();
|
||||
routes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
return planner.naturalSampler == null ? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
|
||||
HydrologyRegionalNetwork coursesIn(SurfaceBounds bounds) {
|
||||
if (!enabled()) {
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
int basinSize = settings.basinSize(planner.settings.routing());
|
||||
int reach = settings.sampleSpacing() * 2 + footprintRadius();
|
||||
int minimumX = Math.floorDiv(bounds.minimumX() - reach, basinSize);
|
||||
int maximumX = Math.floorDiv(bounds.maximumX() + reach, basinSize);
|
||||
int minimumZ = Math.floorDiv(bounds.minimumZ() - reach, basinSize);
|
||||
int maximumZ = Math.floorDiv(bounds.maximumZ() + reach, basinSize);
|
||||
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
|
||||
ArrayList<Callable<HydrologyRegionalNetwork>> tasks = new ArrayList<>();
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
HydrologyTileKey key = new HydrologyTileKey(x, z);
|
||||
keys.add(key);
|
||||
tasks.add(() -> demandedDraft(key, bounds));
|
||||
}
|
||||
}
|
||||
List<HydrologyRegionalNetwork> loaded = HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
|
||||
ArrayList<HydrologyRegionalNetwork> networks = new ArrayList<>(loaded.size());
|
||||
for (int index = 0; index < loaded.size(); index++) {
|
||||
HydrologyTileKey key = keys.get(index);
|
||||
HydrologyRegionalNetwork draft = loaded.get(index);
|
||||
ArrayList<RiverCourse> accepted = new ArrayList<>();
|
||||
for (RiverCourse course : draft.courses()) {
|
||||
if (intersects(course, bounds, footprintRadius()) && admitted(key, course)) {
|
||||
accepted.add(course);
|
||||
}
|
||||
}
|
||||
if (!accepted.isEmpty()) {
|
||||
HydrologyRegionalNetwork selected = select(draft, accepted);
|
||||
networks.add(new HydrologyRegionalNetwork(selected.nodes(), selected.edges(), selected.outlets(),
|
||||
selected.courses(), List.of(), selected.cavePlans()));
|
||||
}
|
||||
}
|
||||
return merge(networks);
|
||||
}
|
||||
|
||||
List<HydrologyDiagnosticCandidate> diagnosticsIn(SurfaceBounds bounds) {
|
||||
if (!enabled()) {
|
||||
return List.of();
|
||||
}
|
||||
int basinSize = settings.basinSize(planner.settings.routing());
|
||||
HydrologyTileKey primary = HydrologyTileKey.fromBlock(
|
||||
(int) (((long) bounds.minimumX() + bounds.maximumX()) / 2L),
|
||||
(int) (((long) bounds.minimumZ() + bounds.maximumZ()) / 2L), basinSize);
|
||||
return draft(primary).diagnostics();
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork demandedDraft(HydrologyTileKey key, SurfaceBounds bounds) {
|
||||
HydrologyRegionalNetwork cached = drafts.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
PreparedCoarse prepared = prepareCoarse(key);
|
||||
return prepared.plan().intersects(bounds) ? draft(key, prepared) : HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
|
||||
RiverFootprint materialize(HydrologyRegionalNetwork network, SurfaceBounds bounds) {
|
||||
if (network.courses().isEmpty()) {
|
||||
return RiverFootprint.empty();
|
||||
}
|
||||
SurfaceFootprintCompiler compiler = new SurfaceFootprintCompiler(planner.settings,
|
||||
this::sample, planner.geometrySampler);
|
||||
LinkedHashMap<Long, HydrologyColumnSample> columns = new LinkedHashMap<>();
|
||||
for (RiverCourse course : network.courses()) {
|
||||
SurfaceFootprint footprint = compiler.compile(course, bounds);
|
||||
if (!footprint.accepted()) {
|
||||
throw new IllegalStateException("An accepted regional course changed its bounded surface admission: " + course.id());
|
||||
}
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
HydrologyTerrainSample terrain = column.terrain();
|
||||
HydrologyColumnSample sample = new HydrologyColumnSample(column.x(), column.z(), terrain.naturalHeight(),
|
||||
planner.settings.seaLevel(), terrain.ocean(), terrain.parentBiomeKey(), List.of(column.layer()));
|
||||
columns.merge(RiverFootprint.pack(column.x(), column.z()), sample, HydrologyRegionalPlanner::mergeColumn);
|
||||
}
|
||||
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(planner.settings,
|
||||
HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course), planner.geometrySampler, course, bounds);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
columns.merge(RiverFootprint.pack(column.x(), column.z()), column, HydrologyRegionalPlanner::mergeColumn);
|
||||
}
|
||||
}
|
||||
return new RiverFootprint(columns);
|
||||
}
|
||||
|
||||
HydrologyRegionalNetwork draft(HydrologyTileKey key) {
|
||||
HydrologyRegionalNetwork cached = drafts.getIfPresent(key);
|
||||
return cached != null ? cached : draft(key, prepareCoarse(key));
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork draft(HydrologyTileKey key, PreparedCoarse prepared) {
|
||||
HydrologyForkJoin.Task<HydrologyRegionalNetwork> task;
|
||||
synchronized (draftPublicationLock) {
|
||||
boolean current = prepared.epoch() == draftEpoch;
|
||||
HydrologyRegionalNetwork cached = current ? drafts.getIfPresent(key) : null;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
task = current ? compilingDrafts.get(key) : null;
|
||||
if (task == null) {
|
||||
task = new HydrologyForkJoin.Task<>(() -> compileDraft(key, prepared));
|
||||
if (current) {
|
||||
compilingDrafts.put(key, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
return task.await();
|
||||
}
|
||||
|
||||
private PreparedCoarse prepareCoarse(HydrologyTileKey key) {
|
||||
long epoch;
|
||||
HydrologyForkJoin.Task<HydrologyRegionalCoarsePlan> task;
|
||||
synchronized (draftPublicationLock) {
|
||||
epoch = draftEpoch;
|
||||
HydrologyRegionalCoarsePlan cached = coarsePlans.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return new PreparedCoarse(epoch, cached);
|
||||
}
|
||||
task = preparingCoarsePlans.get(key);
|
||||
if (task == null) {
|
||||
task = new HydrologyForkJoin.Task<>(() -> compileCoarse(key, epoch));
|
||||
preparingCoarsePlans.put(key, task);
|
||||
}
|
||||
}
|
||||
return new PreparedCoarse(epoch, task.await());
|
||||
}
|
||||
|
||||
private HydrologyRegionalCoarsePlan compileCoarse(HydrologyTileKey key, long epoch) {
|
||||
try {
|
||||
HydrologyRegionalCoarsePlan plan = coarseSetup(key);
|
||||
synchronized (draftPublicationLock) {
|
||||
if (epoch == draftEpoch) {
|
||||
coarsePlans.put(key, plan);
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
} finally {
|
||||
synchronized (draftPublicationLock) {
|
||||
if (epoch == draftEpoch) {
|
||||
preparingCoarsePlans.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork compileDraft(HydrologyTileKey key, PreparedCoarse prepared) {
|
||||
try {
|
||||
HydrologyRegionalNetwork network = compile(key, prepared.plan());
|
||||
synchronized (draftPublicationLock) {
|
||||
if (prepared.epoch() == draftEpoch) {
|
||||
drafts.put(key, network);
|
||||
}
|
||||
}
|
||||
return network;
|
||||
} finally {
|
||||
synchronized (draftPublicationLock) {
|
||||
if (prepared.epoch() == draftEpoch) {
|
||||
compilingDrafts.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyRegionalCoarsePlan coarseSetup(HydrologyTileKey key) {
|
||||
HydrologySampledGrid grid = sample(key);
|
||||
ArrayList<OutletCandidate> outlets = planner.outletPlanner.regionalOceanOutletCandidates(grid);
|
||||
outlets.removeIf(outlet -> outlet.outlet().type() != HydrologyFeatureType.MOUTH);
|
||||
outlets.sort(Comparator.comparingLong(outlet -> HydrologyHash.mix(planner.worldSeed, BASIN_SALT,
|
||||
key.tileX(), key.tileZ(), outlet.outlet().id())));
|
||||
long basinSeed = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, key.tileX(), key.tileZ());
|
||||
boolean coastal = settings.coastalChannels() && HydrologyHash.unit(basinSeed) < settings.coastalChannelChance();
|
||||
if (outlets.isEmpty() || coastal) {
|
||||
return new HydrologyRegionalCoarsePlan(grid, outlets, null, List.of(), List.of(), 0D, coastal, grid.nodes().size());
|
||||
}
|
||||
return routeCoarse(grid, outlets);
|
||||
}
|
||||
|
||||
private HydrologyRegionalCoarsePlan routeCoarse(HydrologySampledGrid grid, List<OutletCandidate> outlets) {
|
||||
ArrayList<OutletCandidate> roots = spacedOutlets(outlets, settings.maximumTrunks() * 2);
|
||||
HydrologyRegionalGraph.Tree tree = graph.route(grid, roots, false);
|
||||
ArrayList<Integer> sources = new ArrayList<>();
|
||||
ArrayList<HydrologyRegionalEnvelope> envelopes = new ArrayList<>();
|
||||
double longest = 0D;
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
if (!grid.owns(node.x(), node.z()) || tree.root(node.index()) < 0) {
|
||||
continue;
|
||||
}
|
||||
longest = Math.max(longest, tree.length(node.index()));
|
||||
if (tree.length(node.index()) < settings.minimumLength()
|
||||
|| !planner.sourcePlanner.rawSourceEligible(node.terrain(), planner.settings.surface().sources(), true)) {
|
||||
continue;
|
||||
}
|
||||
sources.add(node.index());
|
||||
ArrayList<HydrologyPoint> guide = new ArrayList<>();
|
||||
for (int index : graph.path(tree, node.index())) {
|
||||
guide.add(grid.node(index).naturalPoint());
|
||||
}
|
||||
RiverOutlet outlet = tree.outlets().get(tree.root(node.index())).outlet();
|
||||
guide.add(outlet.landwardPoint());
|
||||
guide.add(outlet.connectionPoint());
|
||||
envelopes.add(HydrologyRegionalEnvelope.of(guide, planner.settings, footprintRadius()));
|
||||
}
|
||||
int weight = Math.addExact(Math.addExact(grid.nodes().size(), tree.selected().length),
|
||||
Math.addExact(Math.ceilDiv(tree.accumulation().length, 16), envelopes.size()));
|
||||
return new HydrologyRegionalCoarsePlan(grid, outlets, tree, sources, envelopes, longest, false, weight);
|
||||
}
|
||||
|
||||
private record PreparedCoarse(long epoch, HydrologyRegionalCoarsePlan plan) {
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork compile(HydrologyTileKey key, HydrologyRegionalCoarsePlan coarse) {
|
||||
HydrologySampledGrid grid = coarse.grid();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyGridNode center = grid.nodes().get(grid.nodes().size() / 2);
|
||||
List<OutletCandidate> outlets = coarse.outlets();
|
||||
if (outlets.isEmpty()) {
|
||||
diagnostic(diagnostics, center.id(), center.naturalPoint(), false, HydrologyCandidateRejection.NO_LEGAL_OUTLET, 0);
|
||||
return new HydrologyRegionalNetwork(List.of(), List.of(), List.of(), List.of(), diagnostics, List.of());
|
||||
}
|
||||
ArrayList<HydrologyRegionalNetwork> accepted = new ArrayList<>();
|
||||
long basinSeed = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, key.tileX(), key.tileZ());
|
||||
if (coarse.coastal()) {
|
||||
ArrayList<HydrologyDiagnosticCandidate> coastalDiagnostics = new ArrayList<>();
|
||||
HydrologyRegionalNetwork coastal = coastalChannel(grid, outlets, basinSeed, coastalDiagnostics);
|
||||
diagnostics.addAll(coastalDiagnostics.subList(0, Math.min(16, coastalDiagnostics.size())));
|
||||
if (!coastal.courses().isEmpty()) {
|
||||
accepted.add(coastal);
|
||||
}
|
||||
}
|
||||
HydrologyRegionalCoarsePlan routed = coarse.tree() == null ? routeCoarse(grid, outlets) : coarse;
|
||||
HydrologyRegionalGraph.Tree tree = routed.tree();
|
||||
List<Integer> sources = routed.sources();
|
||||
if (sources.isEmpty()) {
|
||||
double longest = routed.longest();
|
||||
diagnostic(diagnostics, center.id(), center.naturalPoint(), false,
|
||||
longest == 0D ? HydrologyCandidateRejection.NO_DRAINAGE_PATH
|
||||
: longest < settings.minimumLength() ? HydrologyCandidateRejection.COURSE_TOO_SHORT
|
||||
: HydrologyCandidateRejection.POLICY_EXCLUDED, (int) StrictMath.round(longest));
|
||||
}
|
||||
Set<Long> usedOutlets = new HashSet<>();
|
||||
for (HydrologyRegionalNetwork network : accepted) {
|
||||
for (RiverOutlet outlet : network.outlets()) {
|
||||
usedOutlets.add(outlet.id());
|
||||
}
|
||||
}
|
||||
HydrologyRegionalSourceTrials sourceTrials = new HydrologyRegionalSourceTrials(grid, tree, sources);
|
||||
SourceBuildContext sourceContext = new SourceBuildContext(grid, tree, basinSeed);
|
||||
int trials = 0;
|
||||
while (trials < MAXIMUM_SOURCE_TRIALS && accepted.size() < settings.maximumTrunks()) {
|
||||
int window = Math.min(SOURCE_TRIAL_WINDOW, MAXIMUM_SOURCE_TRIALS - trials);
|
||||
ArrayList<Callable<SourceTrial>> tasks = new ArrayList<>(window);
|
||||
for (int index = 0; index < window; index++) {
|
||||
OptionalInt selected = sourceTrials.next(usedOutlets);
|
||||
if (selected.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
int source = selected.getAsInt();
|
||||
tasks.add(() -> buildSourceTrial(sourceContext, source));
|
||||
}
|
||||
if (tasks.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
ArrayList<SourceTrial> completed = new ArrayList<>(tasks.size());
|
||||
int batchSize = HydrologyPlanningAdmission.maximumTrials();
|
||||
for (int start = 0; start < tasks.size(); start += batchSize) {
|
||||
int end = Math.min(tasks.size(), start + batchSize);
|
||||
completed.addAll(HydrologyForkJoin.invokeAll(tasks.subList(start, end),
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null));
|
||||
}
|
||||
for (SourceTrial trial : completed) {
|
||||
if (accepted.size() >= settings.maximumTrunks()) {
|
||||
break;
|
||||
}
|
||||
if (usedOutlets.contains(trial.outletId())) {
|
||||
continue;
|
||||
}
|
||||
trials++;
|
||||
if (trial.failure() instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (trial.failure() instanceof RuntimeException failure) {
|
||||
throw failure;
|
||||
}
|
||||
int retained = Math.min(MAXIMUM_DIAGNOSTICS - diagnostics.size(), trial.diagnostics().size());
|
||||
diagnostics.addAll(trial.diagnostics().subList(0, retained));
|
||||
HydrologyRegionalNetwork candidate = trial.network();
|
||||
if (!candidate.courses().isEmpty() && !conflicts(candidate.courses().getFirst(), merge(accepted).courses())) {
|
||||
accepted.add(candidate);
|
||||
usedOutlets.add(trial.outletId());
|
||||
}
|
||||
}
|
||||
}
|
||||
HydrologyRegionalNetwork network = merge(accepted);
|
||||
return new HydrologyRegionalNetwork(network.nodes(), network.edges(), network.outlets(), network.courses(), diagnostics, network.cavePlans());
|
||||
}
|
||||
|
||||
private SourceTrial buildSourceTrial(SourceBuildContext context, int source) {
|
||||
long outletId = context.tree().outlets().get(context.tree().root(source)).outlet().id();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyPlanner.PlanningSamples previous = planner.planningSamples.get();
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
HydrologyRegionalNetwork network = build(context.grid(), context.tree(), source, null, context.basinSeed(), diagnostics);
|
||||
return new SourceTrial(outletId, network, List.copyOf(diagnostics), null);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
return new SourceTrial(outletId, null, List.of(), failure);
|
||||
} finally {
|
||||
if (previous == null) {
|
||||
planner.planningSamples.remove();
|
||||
} else {
|
||||
planner.planningSamples.set(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork coastalChannel(HydrologySampledGrid grid, List<OutletCandidate> outlets,
|
||||
long basinSeed, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
int maximum = Math.min(8, outlets.size());
|
||||
int pairs = 0;
|
||||
double longest = 0D;
|
||||
for (int destination = 0; destination < maximum; destination++) {
|
||||
if (!bidirectionalMouth(outlets.get(destination).outlet())) {
|
||||
continue;
|
||||
}
|
||||
HydrologyRegionalGraph.Tree tree = graph.route(grid, List.of(outlets.get(destination)), true);
|
||||
ArrayList<OutletCandidate> origins = new ArrayList<>(outlets);
|
||||
origins.sort(Comparator.comparingDouble((OutletCandidate outlet) -> tree.length(outlet.landIndex())).reversed()
|
||||
.thenComparingLong(outlet -> outlet.outlet().id()));
|
||||
int trials = 0;
|
||||
for (OutletCandidate origin : origins) {
|
||||
if (trials++ >= 8) {
|
||||
break;
|
||||
}
|
||||
int source = origin.landIndex();
|
||||
if (!bidirectionalMouth(origin.outlet())) {
|
||||
continue;
|
||||
}
|
||||
if (grid.owns(grid.node(source).x(), grid.node(source).z())
|
||||
&& opposingCoasts(origin.outlet(), outlets.get(destination).outlet())) {
|
||||
pairs++;
|
||||
longest = Math.max(longest, tree.length(source));
|
||||
}
|
||||
if (!grid.owns(grid.node(source).x(), grid.node(source).z()) || tree.root(source) < 0
|
||||
|| !opposingCoasts(origin.outlet(), outlets.get(destination).outlet())
|
||||
|| tree.length(source) < settings.minimumLength()
|
||||
|| origin.outlet().connectionPoint().distanceSquared2D(outlets.get(destination).outlet().connectionPoint())
|
||||
< (long) settings.minimumLength() * settings.minimumLength() / 4L) {
|
||||
continue;
|
||||
}
|
||||
HydrologyRegionalNetwork network = build(grid, tree, source, origin, basinSeed, diagnostics);
|
||||
if (!network.courses().isEmpty()) {
|
||||
return network;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (diagnostics.isEmpty()) {
|
||||
HydrologyGridNode center = grid.nodes().get(grid.nodes().size() / 2);
|
||||
diagnostic(diagnostics, basinSeed, center.naturalPoint(), true,
|
||||
pairs == 0 ? HydrologyCandidateRejection.NO_LEGAL_OUTLET
|
||||
: longest == 0D ? HydrologyCandidateRejection.NO_DRAINAGE_PATH
|
||||
: HydrologyCandidateRejection.COURSE_TOO_SHORT,
|
||||
(int) StrictMath.round(longest));
|
||||
}
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
|
||||
boolean bidirectionalMouth(RiverOutlet outlet) {
|
||||
HydrologyTerrainSample land = sample(outlet.landwardPoint().x(), outlet.landwardPoint().z());
|
||||
HydrologyTerrainSample ocean = sample(outlet.connectionPoint().x(), outlet.connectionPoint().z());
|
||||
return land != null && ocean != null && land.drainsInto(ocean) && ocean.drainsInto(land);
|
||||
}
|
||||
|
||||
private static boolean opposingCoasts(RiverOutlet first, RiverOutlet second) {
|
||||
double firstX = first.connectionPoint().x() - first.landwardPoint().x();
|
||||
double firstZ = first.connectionPoint().z() - first.landwardPoint().z();
|
||||
double secondX = second.connectionPoint().x() - second.landwardPoint().x();
|
||||
double secondZ = second.connectionPoint().z() - second.landwardPoint().z();
|
||||
return firstX * secondX + firstZ * secondZ < 0D;
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork build(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree,
|
||||
int source, OutletCandidate origin, long basinSeed,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
List<Integer> indices = graph.path(tree, source);
|
||||
OutletCandidate destination = tree.outlets().get(tree.root(source));
|
||||
RiverOutlet outlet = destination.outlet();
|
||||
ArrayList<HydrologyPoint> guide = new ArrayList<>(tree.centerline(grid, source));
|
||||
ArrayList<String> profiles = new ArrayList<>(grid.node(source).terrain().preferredProfileKeys());
|
||||
for (int index : indices) {
|
||||
HydrologyGridNode node = grid.node(index);
|
||||
profiles.retainAll(node.terrain().preferredProfileKeys());
|
||||
}
|
||||
profiles.retainAll(sample(outlet.connectionPoint().x(), outlet.connectionPoint().z()).preferredProfileKeys());
|
||||
if (origin != null) {
|
||||
profiles.retainAll(sample(origin.outlet().connectionPoint().x(), origin.outlet().connectionPoint().z()).preferredProfileKeys());
|
||||
HydrologyPoint start = origin.outlet().landwardPoint();
|
||||
guide.set(0, new HydrologyPoint(start.x(), planner.settings.seaLevel(), start.z()));
|
||||
}
|
||||
if (profiles.isEmpty() || guide.size() < 2) {
|
||||
diagnostic(diagnostics, grid.node(source).id(), grid.node(source).naturalPoint(), origin != null,
|
||||
HydrologyCandidateRejection.POLICY_EXCLUDED, guide.size());
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
HydrologyPoint end = outlet.landwardPoint();
|
||||
guide.set(guide.size() - 1, new HydrologyPoint(end.x(), guide.getLast().y(), end.z()));
|
||||
String profile = profiles.get(HydrologyHash.between(HydrologyHash.mix(basinSeed, source), 0, profiles.size() - 1));
|
||||
BuildContext context = new BuildContext(grid.node(source), origin, basinSeed, outlet, profile,
|
||||
new HydrologyRegionalFlow(guide, tree.contributions(source)),
|
||||
HydrologyRegionalMorphology.sample(guide, this::sample, planner.settings));
|
||||
HydrologyRegionalRoute.Attempt selected = routes.select(guide, profile, origin != null,
|
||||
HydrologyOceanReceiver.forOutlet(planner.settings, this::sample, outlet), candidate -> attempt(context, candidate));
|
||||
if (selected.rejection() != null) {
|
||||
if (diagnostics.size() < MAXIMUM_DIAGNOSTICS) {
|
||||
diagnostics.add(selected.rejection());
|
||||
}
|
||||
} else if (selected.refinement().rejection() != null) {
|
||||
HydrologyRegionalRoute.Refinement rejected = selected.refinement();
|
||||
diagnostic(diagnostics, grid.node(source).id(), rejected.failure(), origin != null, rejected.rejection(), rejected.detail());
|
||||
}
|
||||
return selected.network();
|
||||
}
|
||||
|
||||
private HydrologyRegionalRoute.Attempt attempt(BuildContext context, HydrologyRegionalRoute.Refinement refinement) {
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyRegionalNetwork network = build(context, refinement, diagnostics);
|
||||
if (network.courses().isEmpty() && diagnostics.isEmpty()) {
|
||||
diagnostic(diagnostics, context.source().id(), refinement.points().getFirst(), context.origin() != null,
|
||||
HydrologyCandidateRejection.NO_DRAINAGE_PATH, 0);
|
||||
}
|
||||
return new HydrologyRegionalRoute.Attempt(refinement, network, network.courses().isEmpty() ? diagnostics.getFirst() : null);
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork build(BuildContext context, HydrologyRegionalRoute.Refinement refinement,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
RiverOutlet outlet = context.outlet();
|
||||
OutletCandidate origin = context.origin();
|
||||
String profile = context.profile();
|
||||
List<HydrologyPoint> path = refinement.points();
|
||||
if (refinement.oceanEntry() != null) {
|
||||
HydrologyRegionalRoute.OceanEntry entry = refinement.oceanEntry();
|
||||
outlet = new RiverOutlet(HydrologyHash.mix(outlet.id(), entry.landward().x(), entry.landward().z(),
|
||||
entry.receiving().x(), entry.receiving().z()), HydrologyFeatureType.MOUTH, outlet.drainageNodeId(),
|
||||
entry.landward(), entry.receiving(), planner.settings.seaLevel(), true);
|
||||
}
|
||||
long courseId = HydrologyHash.mix(context.basinSeed(), COURSE_SALT, context.source().id(), outlet.id(), origin == null ? 0L : origin.outlet().id());
|
||||
HydrologyRegionalFlow flow = context.flow();
|
||||
HydrologyGeometrySampler geometry = request -> regionalGeometry(request, context);
|
||||
ArrayList<HydraulicSegment> segments;
|
||||
if (origin == null) {
|
||||
SurfaceCourseResult built = new SurfaceCourseBuilder(planner.settings.surface(),
|
||||
HydrologyOceanReceiver.forOutlet(planner.settings, this::sample, outlet), geometry, planner.settings.seaLevel()).build(
|
||||
planner.worldSeed, courseId, profile, path, SurfaceTerminal.OCEAN_MOUTH,
|
||||
planner.settings.seaLevel(), settings.minimumLength());
|
||||
if (!built.accepted()) {
|
||||
diagnostic(diagnostics, courseId, path.getFirst(), false, built.rejection(), built.rejectionDetail());
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
segments = new ArrayList<>(built.segments());
|
||||
segments.add(mouth(courseId, built.pathEnd(), outlet.connectionPoint(), built.lastWidth(), built.lastDepth(), 1));
|
||||
} else {
|
||||
segments = coastalSegments(courseId, profile, path, origin.outlet(), outlet, geometry);
|
||||
}
|
||||
HydrologyRegionalCourseGraph published = HydrologyRegionalCourseGraph.create(this::sample,
|
||||
new HydrologyRegionalCourseGraph.Options(courseId, outlet.id(), settings.sampleSpacing(), path, flow));
|
||||
List<DrainageNode> nodes = published.nodes();
|
||||
List<DrainageEdge> edges = published.edges();
|
||||
RiverCourse course = new RiverCourse(courseId, RiverCourseType.SURFACE, OptionalLong.of(nodes.getFirst().id()),
|
||||
OptionalLong.of(outlet.id()), profile, published.discharge(), edges, segments);
|
||||
HydrologyRegionalFalls falls = new HydrologyRegionalFalls(planner, course);
|
||||
if (!validFootprints(course, diagnostics, falls)) {
|
||||
return HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
RiverOutlet publishedOutlet = publishedOutlet(outlet, nodes.getLast().id());
|
||||
HydrologyRegionalNetwork network = new HydrologyRegionalNetwork(nodes, edges, origin == null ? List.of(publishedOutlet)
|
||||
: List.of(publishedOutlet(origin.outlet(), nodes.getFirst().id()), publishedOutlet),
|
||||
List.of(course), List.of(), List.of());
|
||||
List<HydrologyCavePlan> plans = falls.validate(network, diagnostics);
|
||||
return plans == null ? HydrologyRegionalNetwork.EMPTY : new HydrologyRegionalNetwork(nodes, edges,
|
||||
network.outlets(), network.courses(), List.of(), plans);
|
||||
}
|
||||
|
||||
private static RiverOutlet publishedOutlet(RiverOutlet outlet, long nodeId) {
|
||||
return new RiverOutlet(outlet.id(), outlet.type(), nodeId, outlet.landwardPoint(), outlet.connectionPoint(),
|
||||
outlet.seaLevel(), outlet.directOcean());
|
||||
}
|
||||
|
||||
private int regionalGeometry(HydrologyGeometrySampler.Request request, BuildContext context) {
|
||||
if (request.field() != HydrologyGeometrySampler.Field.SURFACE_WIDTH) {
|
||||
return planner.geometrySampler.sample(request);
|
||||
}
|
||||
return regionalWidth(request, planner.geometrySampler.sample(request),
|
||||
context.flow().fraction(request.x(), request.z()),
|
||||
context.morphology().freedomAt(request.x(), request.z()));
|
||||
}
|
||||
|
||||
static int regionalWidth(HydrologyGeometrySampler.Request request, int authored,
|
||||
double flow, double freedom) {
|
||||
double fraction = 0.25D + 0.75D * flow;
|
||||
double range = (double) request.maximum() - request.minimum();
|
||||
double sampled = request.minimum() + range * fraction;
|
||||
double width = sampled * (1D - AUTHORED_WIDTH_WEIGHT) + authored * AUTHORED_WIDTH_WEIGHT;
|
||||
double constrained = request.minimum() + (width - request.minimum()) * freedom;
|
||||
return Math.max(request.minimum(), Math.min(request.maximum(), (int) StrictMath.round(constrained)));
|
||||
}
|
||||
|
||||
private ArrayList<HydraulicSegment> coastalSegments(long courseId, String profile, List<HydrologyPoint> path,
|
||||
RiverOutlet origin, RiverOutlet destination, HydrologyGeometrySampler geometry) {
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>();
|
||||
int seaLevel = planner.settings.seaLevel();
|
||||
HydrologyPlannerSettings.Surface surface = planner.settings.surface();
|
||||
HydrologyPoint first = HydrologyPlanner.withY(path.getFirst(), seaLevel);
|
||||
double width = coastalWidth(profile, first, geometry);
|
||||
int depth = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_DEPTH, profile, first.x(), first.z(), surface.minimumDepth(), surface.maximumDepth());
|
||||
segments.add(mouth(courseId, origin.connectionPoint(), first, width, depth, 0));
|
||||
for (int index = 0; index + 1 < path.size(); index++) {
|
||||
HydrologyPoint point = path.get(index);
|
||||
HydrologyPoint next = path.get(index + 1);
|
||||
depth = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_DEPTH, profile, point.x(), point.z(), surface.minimumDepth(), surface.maximumDepth());
|
||||
double nextWidth = coastalWidth(profile, next, geometry);
|
||||
int nextDepth = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_DEPTH, profile, next.x(), next.z(), surface.minimumDepth(), surface.maximumDepth());
|
||||
segments.add(new HydraulicSegment(HydrologyHash.mix(courseId, index, COURSE_SALT), courseId,
|
||||
HydrologyFeatureType.SURFACE_POOL, seaLevel, seaLevel, (int) StrictMath.ceil(Math.max(width, nextWidth)), Math.max(depth, nextDepth), false, false,
|
||||
List.of(HydrologyPlanner.withY(point, seaLevel), HydrologyPlanner.withY(next, seaLevel)),
|
||||
new HydraulicChannelProfile(new double[] {width, nextWidth}, new double[] {depth, nextDepth})));
|
||||
width = nextWidth;
|
||||
depth = nextDepth;
|
||||
}
|
||||
segments.add(mouth(courseId, HydrologyPlanner.withY(path.getLast(), seaLevel), destination.connectionPoint(), width, depth, 1));
|
||||
return segments;
|
||||
}
|
||||
|
||||
private double coastalWidth(String profile, HydrologyPoint point, HydrologyGeometrySampler geometry) {
|
||||
HydrologyPlannerSettings.Surface surface = planner.settings.surface();
|
||||
int authored = geometry.sample(HydrologyGeometrySampler.Field.SURFACE_WIDTH, profile, point.x(), point.z(),
|
||||
surface.minimumWidth(), surface.maximumWidth());
|
||||
return Math.max(surface.minimumWidth(), Math.min(surface.maximumWidth() * 2D,
|
||||
authored * sample(point.x(), point.z()).widthMultiplier()));
|
||||
}
|
||||
|
||||
private HydraulicSegment mouth(long courseId, HydrologyPoint start, HydrologyPoint end, double width, int depth, int side) {
|
||||
int head = planner.settings.seaLevel();
|
||||
return new HydraulicSegment(HydrologyHash.mix(courseId, HydrologyFeatureType.MOUTH.ordinal(), side), courseId,
|
||||
HydrologyFeatureType.MOUTH, head, head, (int) StrictMath.ceil(width), depth, false, false,
|
||||
List.of(HydrologyPlanner.withY(start, head), HydrologyPlanner.withY(end, head)), HydraulicChannelProfile.uniform(width, depth));
|
||||
}
|
||||
|
||||
private boolean validFootprints(RiverCourse course, List<HydrologyDiagnosticCandidate> diagnostics, HydrologyRegionalFalls falls) {
|
||||
double length = 0D;
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
points.addAll(segment.centerline());
|
||||
for (int index = 1; index < segment.centerline().size(); index++) {
|
||||
length += StrictMath.sqrt(segment.centerline().get(index).distanceSquared2D(segment.centerline().get(index - 1)));
|
||||
}
|
||||
}
|
||||
boolean coastal = course.segments().getFirst().type() == HydrologyFeatureType.MOUTH;
|
||||
HydrologyPoint source = course.segments().getFirst().start();
|
||||
if (length < settings.minimumLength() || length > planner.settings.routing().maximumRouteLength()) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, length < settings.minimumLength()
|
||||
? HydrologyCandidateRejection.COURSE_TOO_SHORT : HydrologyCandidateRejection.ROUTE_LIMIT,
|
||||
(int) StrictMath.ceil(length));
|
||||
return false;
|
||||
}
|
||||
SurfaceFootprintCompiler compiler = new SurfaceFootprintCompiler(planner.settings,
|
||||
this::sample, planner.geometrySampler);
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(points);
|
||||
int[] excavation = new int[centerline.size()];
|
||||
HydrologyRegionalConnectivity connectivity = new HydrologyRegionalConnectivity();
|
||||
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course);
|
||||
SurfaceFootprintCompiler.PreparedCourse prepared = compiler.prepare(course);
|
||||
for (SurfaceBounds bounds : courseWindows(course)) {
|
||||
SurfaceFootprint footprint = compiler.compile(prepared, bounds);
|
||||
if (!footprint.accepted()) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, footprint.rejection(), footprint.rejectionDetail());
|
||||
return false;
|
||||
}
|
||||
falls.retain(footprint);
|
||||
connectivity.add(footprint.columns());
|
||||
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(planner.settings,
|
||||
receiver, planner.geometrySampler, course, bounds);
|
||||
SurfaceExcavationMetrics.accumulate(course, centerline, footprint, drops, bounds, excavation);
|
||||
for (HydrologyColumnSample column : drops.columns()) {
|
||||
connectivity.add(column);
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
if (layer.fluidOwned() && !sample(column.x(), column.z()).preferredProfileKeys().contains(course.profileKey())) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.POLICY_EXCLUDED, 0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.layer().fluidOwned() && !column.terrain().preferredProfileKeys().contains(course.profileKey())) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.POLICY_EXCLUDED, column.station());
|
||||
return false;
|
||||
}
|
||||
if (coastal && column.layer().fluidOwned()
|
||||
&& column.terrain().naturalHeight() - column.layer().bedY() > settings.maximumCoastalIncision()) {
|
||||
diagnostic(diagnostics, course.id(), source, true, HydrologyCandidateRejection.SURFACE_BANK_BUDGET,
|
||||
column.terrain().naturalHeight() - column.layer().bedY());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
int volume = SurfaceExcavationMetrics.maximumVolumePerBlock(centerline, excavation);
|
||||
if (volume > planner.settings.surface().banks().erosion().excavation().maximumVolumePerBlock()) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.SURFACE_BANK_BUDGET, volume);
|
||||
return false;
|
||||
}
|
||||
if (!connectivity.connected(course, this::sample, planner.settings)) {
|
||||
diagnostic(diagnostics, course.id(), source, coastal, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<SurfaceBounds> courseWindows(RiverCourse course) {
|
||||
int tileSize = planner.settings.routing().tileSize();
|
||||
int radius = footprintRadius();
|
||||
Set<HydrologyTileKey> keys = new HashSet<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
for (int z = Math.floorDiv(point.z() - radius, tileSize); z <= Math.floorDiv(point.z() + radius, tileSize); z++) {
|
||||
for (int x = Math.floorDiv(point.x() - radius, tileSize); x <= Math.floorDiv(point.x() + radius, tileSize); x++) {
|
||||
keys.add(new HydrologyTileKey(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyTileKey> ordered = new ArrayList<>(keys);
|
||||
ordered.sort(HydrologyTileKey::compareTo);
|
||||
ArrayList<SurfaceBounds> windows = new ArrayList<>(ordered.size());
|
||||
for (HydrologyTileKey key : ordered) {
|
||||
windows.add(tileBounds(key));
|
||||
}
|
||||
return List.copyOf(windows);
|
||||
}
|
||||
|
||||
SurfaceBounds tileBounds(HydrologyTileKey key) {
|
||||
int size = planner.settings.routing().tileSize();
|
||||
return new SurfaceBounds(key.minimumBlockX(size), key.minimumBlockZ(size),
|
||||
Math.addExact(key.minimumBlockX(size), size - 1), Math.addExact(key.minimumBlockZ(size), size - 1));
|
||||
}
|
||||
|
||||
SurfaceBounds ownerBounds(HydrologyTileKey key) {
|
||||
SurfaceBounds tile = tileBounds(key);
|
||||
int reach = planner.settings.publicationRadius() + 1;
|
||||
return new SurfaceBounds(Math.subtractExact(tile.minimumX(), reach), Math.subtractExact(tile.minimumZ(), reach),
|
||||
Math.addExact(tile.maximumX(), reach), Math.addExact(tile.maximumZ(), reach));
|
||||
}
|
||||
|
||||
HydrologyCaveCourseFilter.Result include(HydrologyCaveCourseFilter.Result local, HydrologyRegionalNetwork regional) {
|
||||
if (regional.courses().isEmpty()) {
|
||||
return local;
|
||||
}
|
||||
HydrologyRegionalNetwork combined = merge(List.of(new HydrologyRegionalNetwork(local.nodes(), local.edges(),
|
||||
local.outlets(), local.courses(), List.of(), local.cavePlans()), regional));
|
||||
return new HydrologyCaveCourseFilter.Result(combined.nodes(), combined.edges(), combined.outlets(),
|
||||
combined.courses(), combined.cavePlans());
|
||||
}
|
||||
|
||||
private int footprintRadius() {
|
||||
return (int) StrictMath.ceil(planner.settings.surface().maximumWidth() * 2D
|
||||
+ Math.max(planner.settings.surface().shoreWidth(), planner.settings.widestShoreBiomeWidth())
|
||||
+ Math.min(planner.settings.surface().banks().maximumBlendWidth(),
|
||||
planner.settings.surface().banks().erosion().excavation().maximumWidth()) + 4D);
|
||||
}
|
||||
|
||||
private HydrologySampledGrid sample(HydrologyTileKey key) {
|
||||
int spacing = settings.sampleSpacing();
|
||||
int size = settings.basinSize(planner.settings.routing());
|
||||
int ownerX = key.minimumBlockX(size);
|
||||
int ownerZ = key.minimumBlockZ(size);
|
||||
int minimumX = Math.subtractExact(ownerX, spacing);
|
||||
int minimumZ = Math.subtractExact(ownerZ, spacing);
|
||||
int width = size / spacing + 3;
|
||||
HydrologyTerrainSample[] terrain = planner.routingSampler.sampleGrid(
|
||||
new HydrologyRoutingTerrainSampler.GridRequest(minimumX, minimumZ, width, spacing));
|
||||
ArrayList<HydrologyGridNode> nodes = new ArrayList<>(terrain.length);
|
||||
for (int index = 0; index < terrain.length; index++) {
|
||||
int x = minimumX + index % width * spacing;
|
||||
int z = minimumZ + index / width * spacing;
|
||||
nodes.add(new HydrologyGridNode(index, index % width, index / width, x, z,
|
||||
HydrologyHash.mix(planner.worldSeed, NODE_SALT, key.tileX(), key.tileZ(), x, z), terrain[index]));
|
||||
}
|
||||
return new HydrologySampledGrid(minimumX, minimumZ, ownerX, ownerZ, size, width, spacing, List.copyOf(nodes));
|
||||
}
|
||||
|
||||
private ArrayList<OutletCandidate> spacedOutlets(List<OutletCandidate> candidates, int maximum) {
|
||||
ArrayList<OutletCandidate> selected = new ArrayList<>();
|
||||
long spacing = Math.max(settings.minimumLength() / 2, settings.sampleSpacing() * 2);
|
||||
for (OutletCandidate candidate : candidates) {
|
||||
boolean overlap = false;
|
||||
for (OutletCandidate existing : selected) {
|
||||
if (existing.outlet().landwardPoint().distanceSquared2D(candidate.outlet().landwardPoint()) < spacing * spacing) {
|
||||
overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!overlap) {
|
||||
selected.add(candidate);
|
||||
if (selected.size() == maximum) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private boolean admitted(HydrologyTileKey owner, RiverCourse course) {
|
||||
long rank = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, owner.tileX(), owner.tileZ());
|
||||
int size = settings.basinSize(planner.settings.routing());
|
||||
int reach = settings.sampleSpacing() * 2 + footprintRadius();
|
||||
for (int z = owner.tileZ() - 1; z <= owner.tileZ() + 1; z++) {
|
||||
for (int x = owner.tileX() - 1; x <= owner.tileX() + 1; x++) {
|
||||
HydrologyTileKey other = new HydrologyTileKey(x, z);
|
||||
if (other.equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
SurfaceBounds otherBounds = new SurfaceBounds(other.minimumBlockX(size), other.minimumBlockZ(size),
|
||||
Math.addExact(other.minimumBlockX(size), size - 1), Math.addExact(other.minimumBlockZ(size), size - 1));
|
||||
if (!intersects(course, otherBounds, reach)) {
|
||||
continue;
|
||||
}
|
||||
long otherRank = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, x, z);
|
||||
if (Long.compareUnsigned(otherRank, rank) < 0 && conflicts(course, draft(other).courses())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean conflicts(RiverCourse course, List<RiverCourse> existing) {
|
||||
HydrologyCrossTileSurfaceAdmission.Claim current = claim(course);
|
||||
ArrayList<HydrologyCrossTileSurfaceAdmission.RankedClaim> blockers = new ArrayList<>(existing.size());
|
||||
for (RiverCourse other : existing) {
|
||||
blockers.add(new HydrologyCrossTileSurfaceAdmission.RankedClaim(new HydrologyTileKey(0, 0), 0, claim(other)));
|
||||
}
|
||||
return !HydrologyCrossTileSurfaceAdmission.admit(List.of(current), blockers).rejections().isEmpty();
|
||||
}
|
||||
|
||||
private HydrologyCrossTileSurfaceAdmission.Claim claim(RiverCourse course) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
int width = 1;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
width = Math.max(width, segment.width());
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (points.isEmpty() || points.getLast().distanceSquared2D(point) >= 256L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
HydrologyPoint last = course.segments().getLast().end();
|
||||
if (points.size() < 2 || !points.getLast().equals(last)) {
|
||||
points.add(last);
|
||||
}
|
||||
int bankWidth = planner.settings.surface().banks().erosion().excavation().maximumWidth();
|
||||
int completeWidth = width + 2 * ((int) StrictMath.ceil(planner.settings.surface().shoreWidth()) + bankWidth);
|
||||
return new HydrologyCrossTileSurfaceAdmission.Claim(course.id(), course.outletId().orElse(course.id()), last,
|
||||
true, completeWidth, points, 0);
|
||||
}
|
||||
|
||||
private static boolean intersects(RiverCourse course, SurfaceBounds bounds, int radius) {
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (point.x() >= bounds.minimumX() - radius && point.x() <= bounds.maximumX() + radius
|
||||
&& point.z() >= bounds.minimumZ() - radius && point.z() <= bounds.maximumZ() + radius) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static HydrologyColumnSample mergeColumn(HydrologyColumnSample first, HydrologyColumnSample second) {
|
||||
ArrayList<HydrologyColumnLayer> layers = new ArrayList<>(first.layers());
|
||||
for (HydrologyColumnLayer layer : second.layers()) {
|
||||
if (!layers.contains(layer)) {
|
||||
layers.add(layer);
|
||||
}
|
||||
}
|
||||
return new HydrologyColumnSample(first.x(), first.z(), first.naturalHeight(), first.seaLevel(), first.ocean(),
|
||||
first.parentBiomeKey(), layers);
|
||||
}
|
||||
|
||||
private static HydrologyRegionalNetwork select(HydrologyRegionalNetwork network, List<RiverCourse> courses) {
|
||||
Set<Long> nodeIds = new HashSet<>();
|
||||
Set<Long> outletIds = new HashSet<>();
|
||||
ArrayList<DrainageEdge> edges = new ArrayList<>();
|
||||
for (RiverCourse course : courses) {
|
||||
nodeIds.add(course.sourceNodeId().orElseThrow());
|
||||
outletIds.add(course.outletId().orElseThrow());
|
||||
for (DrainageEdge edge : course.drainageEdges()) {
|
||||
nodeIds.add(edge.upstreamNodeId());
|
||||
nodeIds.add(edge.downstreamNodeId());
|
||||
edges.add(edge);
|
||||
}
|
||||
}
|
||||
ArrayList<DrainageNode> nodes = new ArrayList<>();
|
||||
for (DrainageNode node : network.nodes()) {
|
||||
if (nodeIds.contains(node.id())) {
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
ArrayList<RiverOutlet> outlets = new ArrayList<>();
|
||||
for (RiverOutlet outlet : network.outlets()) {
|
||||
if (outletIds.contains(outlet.id()) || courses.stream().anyMatch(course ->
|
||||
course.segments().getFirst().type() == HydrologyFeatureType.MOUTH
|
||||
&& course.segments().getFirst().start().distanceSquared2D(outlet.connectionPoint()) == 0L)) {
|
||||
outlets.add(outlet);
|
||||
}
|
||||
}
|
||||
Set<Long> courseIds = new HashSet<>();
|
||||
for (RiverCourse course : courses) { courseIds.add(course.id()); }
|
||||
ArrayList<HydrologyCavePlan> plans = new ArrayList<>();
|
||||
for (HydrologyCavePlan plan : network.cavePlans()) {
|
||||
if (courseIds.contains(plan.source().sourceId())) { plans.add(plan); }
|
||||
}
|
||||
return new HydrologyRegionalNetwork(nodes, edges, outlets, courses, List.of(), plans);
|
||||
}
|
||||
|
||||
private static void diagnostic(List<HydrologyDiagnosticCandidate> diagnostics, long id, HydrologyPoint point,
|
||||
boolean coastal, HydrologyCandidateRejection rejection, int detail) {
|
||||
if (diagnostics.size() >= MAXIMUM_DIAGNOSTICS) {
|
||||
return;
|
||||
}
|
||||
HydrologyCandidateKind kind = coastal ? HydrologyCandidateKind.COASTAL_CHANNEL : HydrologyCandidateKind.REGIONAL_SOURCE;
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(HydrologyHash.mix(id, BASIN_SALT, rejection.ordinal()), kind,
|
||||
coastal ? HydrologyFeatureType.MOUTH : HydrologyFeatureType.SURFACE_POOL, point, rejection, detail));
|
||||
}
|
||||
|
||||
private static HydrologyRegionalNetwork merge(List<HydrologyRegionalNetwork> networks) {
|
||||
Map<Long, DrainageNode> nodes = new LinkedHashMap<>();
|
||||
Map<Long, DrainageEdge> edges = new LinkedHashMap<>();
|
||||
Map<Long, RiverOutlet> outlets = new LinkedHashMap<>();
|
||||
Map<Long, RiverCourse> courses = new LinkedHashMap<>();
|
||||
Map<Long, HydrologyDiagnosticCandidate> diagnostics = new LinkedHashMap<>();
|
||||
Map<Long, HydrologyCavePlan> plans = new LinkedHashMap<>();
|
||||
for (HydrologyRegionalNetwork network : networks) {
|
||||
for (DrainageNode node : network.nodes()) { nodes.put(node.id(), node); }
|
||||
for (DrainageEdge edge : network.edges()) { edges.put(edge.id(), edge); }
|
||||
for (RiverOutlet outlet : network.outlets()) { outlets.put(outlet.id(), outlet); }
|
||||
for (RiverCourse course : network.courses()) { courses.put(course.id(), course); }
|
||||
for (HydrologyDiagnosticCandidate candidate : network.diagnostics()) { diagnostics.put(candidate.id(), candidate); }
|
||||
for (HydrologyCavePlan plan : network.cavePlans()) { plans.put(plan.source().sourceId(), plan); }
|
||||
}
|
||||
return new HydrologyRegionalNetwork(new ArrayList<>(nodes.values()), new ArrayList<>(edges.values()),
|
||||
new ArrayList<>(outlets.values()), new ArrayList<>(courses.values()), new ArrayList<>(diagnostics.values()), new ArrayList<>(plans.values()));
|
||||
}
|
||||
|
||||
private record SourceBuildContext(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree, long basinSeed) {
|
||||
}
|
||||
|
||||
private record SourceTrial(long outletId, HydrologyRegionalNetwork network,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics, Throwable failure) {
|
||||
}
|
||||
|
||||
private record BuildContext(HydrologyGridNode source, OutletCandidate origin, long basinSeed, RiverOutlet outlet,
|
||||
String profile, HydrologyRegionalFlow flow, HydrologyRegionalMorphology morphology) {
|
||||
}
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveConflictPolicy;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRegionalProtection {
|
||||
private HydrologyRegionalProtection() {
|
||||
}
|
||||
|
||||
static void rejectFootprints(List<RiverCourse> courses, HydrologyFootprintCompiler compiler,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
HydrologyRegionalNetwork regional = compiler.regionalNetwork;
|
||||
if (regional.cavePlans().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> profiles = profiles(regional);
|
||||
Iterator<RiverCourse> iterator = courses.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
RiverCourse course = iterator.next();
|
||||
RiverFootprint footprint = compiler.compileCourse(course);
|
||||
for (HydrologyCavePlan plan : regional.cavePlans()) {
|
||||
if (changesWitness(course.profileKey(), footprint, profiles.get(plan.source().sourceId()), plan)) {
|
||||
diagnostics.add(diagnostic(course, plan.source().sourceId()));
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HydrologyCaveCourseFilter.Result rejectPlans(HydrologyCaveCourseFilter.Result result,
|
||||
HydrologyRegionalNetwork regional,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
if (regional.cavePlans().isEmpty() || result.cavePlans().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
Map<Long, String> profiles = profiles(regional);
|
||||
Map<Long, RiverCourse> courses = new HashMap<>();
|
||||
for (RiverCourse course : result.courses()) {
|
||||
courses.put(course.id(), course);
|
||||
}
|
||||
Set<Long> rejected = new HashSet<>();
|
||||
for (HydrologyCavePlan local : result.cavePlans()) {
|
||||
RiverCourse course = courses.get(local.source().sourceId());
|
||||
for (HydrologyCavePlan fixed : regional.cavePlans()) {
|
||||
String regionalProfile = profiles.get(fixed.source().sourceId());
|
||||
if (HydrologyCaveConflictPolicy.hasIncompatibleOverlap(course.profileKey(), local.actions(),
|
||||
regionalProfile, fixed.actions()) || changesWitness(course.profileKey(), local, regionalProfile, fixed)) {
|
||||
rejected.add(course.id());
|
||||
diagnostics.add(diagnostic(course, fixed.source().sourceId()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return HydrologyCaveCourseFilter.withoutCourses(result, rejected);
|
||||
}
|
||||
|
||||
static boolean changesWitness(String profile, RiverFootprint footprint, String regionalProfile, HydrologyCavePlan plan) {
|
||||
for (HydrologyColumnSample column : footprint.columns().values()) {
|
||||
if (!plan.allPreconditionsIn(column.x(), column.z(), Math.addExact(column.x(), 1), Math.addExact(column.z(), 1),
|
||||
(position, precondition) -> preservesColumn(profile, column, regionalProfile, plan, position))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean preservesColumn(String profile, HydrologyColumnSample column, String regionalProfile,
|
||||
HydrologyCavePlan plan, CavePosition position) {
|
||||
for (HydrologyColumnLayer layer : column.layers()) {
|
||||
HydrologyCaveAction action = actionAt(column, layer, position.y());
|
||||
if (action != null && (!profile.equals(regionalProfile) || plan.actions().get(position) != action)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static HydrologyCaveAction actionAt(HydrologyColumnSample column, HydrologyColumnLayer layer, int y) {
|
||||
if (y <= layer.bedY() || layer.oceanApron()) {
|
||||
return null;
|
||||
}
|
||||
if (layer.channel() && layer.fluidOwned() && (layer.terrainOwned() || layer.fallingFluid())
|
||||
&& y <= layer.fluidHeadY()) {
|
||||
return layer.fallingFluid() && y < layer.fluidHeadY()
|
||||
? HydrologyCaveAction.FALLING_FLUID : HydrologyCaveAction.WET_SOURCE;
|
||||
}
|
||||
if (!layer.terrainOwned()) {
|
||||
return null;
|
||||
}
|
||||
int ceiling = layer.feature().type().isSurface() ? column.naturalHeight() : layer.ceilingY();
|
||||
return y <= ceiling ? HydrologyCaveAction.DRY_AIR : null;
|
||||
}
|
||||
|
||||
private static boolean changesWitness(String profile, HydrologyCavePlan local, String regionalProfile, HydrologyCavePlan regional) {
|
||||
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : local.actions().entrySet()) {
|
||||
if (entry.getValue() != HydrologyCaveAction.SEAL_GUARD && regional.baselinePreconditions().containsKey(entry.getKey())
|
||||
&& (!profile.equals(regionalProfile) || regional.actions().get(entry.getKey()) != entry.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Map<Long, String> profiles(HydrologyRegionalNetwork regional) {
|
||||
Map<Long, String> profiles = new HashMap<>();
|
||||
for (RiverCourse course : regional.courses()) {
|
||||
profiles.put(course.id(), course.profileKey());
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
private static HydrologyDiagnosticCandidate diagnostic(RiverCourse course, long regionalId) {
|
||||
HydraulicSegment segment = course.segments().getFirst();
|
||||
return new HydrologyDiagnosticCandidate(HydrologyHash.mix(course.id(), regionalId, 0x52454750524f5445L),
|
||||
HydrologyCandidateKind.SOURCE, segment.type(), segment.start(), HydrologyCandidateRejection.CAVE_CONTAINMENT, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,713 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
|
||||
final class HydrologyRegionalRoute {
|
||||
private static final double SAMPLE_SPACING = 4D;
|
||||
private static final double SIMPLEX_COORDINATE_SCALE = 100D;
|
||||
private static final int MAXIMUM_RECEIVER_SAMPLES = 65536;
|
||||
private static final int CORRIDOR_SAMPLE_BATCH = 128;
|
||||
private static final int MAXIMUM_CORRIDOR_WORKERS = 8;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyRegionalTerrain terrain;
|
||||
private final SimplexNoise bends;
|
||||
private final SimplexNoise details;
|
||||
|
||||
HydrologyRegionalRoute(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.terrain = new HydrologyRegionalTerrain(planner);
|
||||
this.bends = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x52454742454e4453L));
|
||||
this.details = new SimplexNoise(HydrologyHash.mix(planner.worldSeed, 0x5245474445544149L));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
terrain.clear();
|
||||
}
|
||||
|
||||
Attempt select(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver,
|
||||
CandidateAdmission admission) {
|
||||
HydrologyRegionalMorphology morphology = HydrologyRegionalMorphology.sample(guide, this.terrain::sample, planner.settings);
|
||||
Refinement topology = validateCorridor(guide, profile, coastal);
|
||||
OceanEntry resolved = topology.rejection() == null ? topology.oceanEntry() : null;
|
||||
if (resolved != null) {
|
||||
guide = topology.points();
|
||||
receiver = HydrologyOceanReceiver.forConnection(planner.settings, this.terrain::sample,
|
||||
resolved.landward(), resolved.receiving());
|
||||
}
|
||||
CandidateSelection selection = new CandidateSelection(admission, resolved);
|
||||
Attempt accepted = selectCurve(guide, profile, coastal, receiver, selection, morphology);
|
||||
return accepted == null ? selection.failure : accepted;
|
||||
}
|
||||
|
||||
private Refinement retainReceivingEntry(Refinement refined, OceanEntry resolved) {
|
||||
if (resolved == null || refined.rejection() != null || refined.oceanEntry() != null) {
|
||||
return refined;
|
||||
}
|
||||
if (refined.points().getLast().distanceSquared2D(resolved.landward()) > 0L) {
|
||||
return new Refinement(List.of(), refined.points().getLast(),
|
||||
HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
double length = 0D;
|
||||
for (int index = 1; index < refined.points().size(); index++) {
|
||||
length += StrictMath.sqrt(refined.points().get(index - 1).distanceSquared2D(refined.points().get(index)));
|
||||
}
|
||||
if (length < planner.settings.routing().regional().minimumLength()) {
|
||||
return new Refinement(List.of(), refined.points().getLast(),
|
||||
HydrologyCandidateRejection.COURSE_TOO_SHORT, (int) StrictMath.floor(length), null);
|
||||
}
|
||||
return new Refinement(refined.points(), null, null, 0, resolved);
|
||||
}
|
||||
|
||||
private Attempt selectCurve(List<HydrologyPoint> guide, String profile, boolean coastal,
|
||||
HydrologyTerrainSampler receiver, CandidateSelection selection, HydrologyRegionalMorphology morphology) {
|
||||
for (double strength : new double[]{1D, 0.5D, 0D}) {
|
||||
List<HydrologyPoint> points = smooth(interpolate(guide, strength, morphology));
|
||||
Attempt accepted = selection.consider(validate(points, profile, coastal, receiver));
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
List<HydrologyPoint> linear = interpolateLinear(guide);
|
||||
Refinement constrained = validate(linear, profile, coastal, receiver);
|
||||
Attempt accepted = selection.consider(constrained);
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
if (constrained.rejection() == HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED) {
|
||||
for (double radius : new double[]{16D, 32D, 64D, 128D, 256D}) {
|
||||
accepted = selection.consider(validateRounded(resample(roundBends(linear, radius)), profile, coastal, receiver));
|
||||
if (accepted != null) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> interpolateLinear(List<HydrologyPoint> guide) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (int index = 0; index + 1 < guide.size(); index++) {
|
||||
HydrologyPoint start = guide.get(index);
|
||||
HydrologyPoint end = guide.get(index + 1);
|
||||
int steps = Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(start.distanceSquared2D(end)) / SAMPLE_SPACING));
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
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 (points.isEmpty() || points.getLast().distanceSquared2D(point) > 0L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private List<CurvePoint> roundBends(List<HydrologyPoint> points, double radius) {
|
||||
double[] stations = new double[points.size()];
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
stations[index] = stations[index - 1] + StrictMath.sqrt(points.get(index - 1).distanceSquared2D(points.get(index)));
|
||||
}
|
||||
double length = stations[stations.length - 1];
|
||||
radius = Math.min(radius, length / 2D);
|
||||
HydrologyPoint start = points.getFirst();
|
||||
HydrologyPoint end = points.getLast();
|
||||
ArrayList<CurvePoint> rounded = new ArrayList<>(points.size());
|
||||
rounded.add(CurvePoint.of(points.getFirst()));
|
||||
int first = 0;
|
||||
for (int index = 1; index + 1 < points.size(); index++) {
|
||||
while (stations[first] < stations[index] - radius) {
|
||||
first++;
|
||||
}
|
||||
double weightedX = 0D;
|
||||
double weightedZ = 0D;
|
||||
double totalWeight = 0D;
|
||||
for (int sample = first; sample < points.size() && stations[sample] < stations[index] + radius; sample++) {
|
||||
double weight = 1D - StrictMath.abs(stations[sample] - stations[index]) / radius;
|
||||
weightedX += points.get(sample).x() * weight;
|
||||
weightedZ += points.get(sample).z() * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
for (int sample = 1; sample < points.size() && stations[sample] + stations[index] < radius; sample++) {
|
||||
double weight = 1D - (stations[sample] + stations[index]) / radius;
|
||||
weightedX += (2D * start.x() - points.get(sample).x()) * weight;
|
||||
weightedZ += (2D * start.z() - points.get(sample).z()) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
for (int sample = points.size() - 2; sample >= 0 && 2D * length - stations[sample] - stations[index] < radius; sample--) {
|
||||
double weight = 1D - (2D * length - stations[sample] - stations[index]) / radius;
|
||||
weightedX += (2D * end.x() - points.get(sample).x()) * weight;
|
||||
weightedZ += (2D * end.z() - points.get(sample).z()) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
HydrologyPoint point = points.get(index);
|
||||
CurvePoint smoothed = new CurvePoint(weightedX / totalWeight, point.y(), weightedZ / totalWeight);
|
||||
if (rounded.getLast().distance(smoothed) > 0D) {
|
||||
rounded.add(smoothed);
|
||||
}
|
||||
}
|
||||
if (rounded.getLast().distance(CurvePoint.of(points.getLast())) > 0D) {
|
||||
rounded.add(CurvePoint.of(points.getLast()));
|
||||
}
|
||||
return List.copyOf(rounded);
|
||||
}
|
||||
|
||||
private List<CurvePoint> resample(List<CurvePoint> points) {
|
||||
ArrayList<CurvePoint> sampled = new ArrayList<>(points.size());
|
||||
sampled.add(points.getFirst());
|
||||
double nextSample = SAMPLE_SPACING;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
CurvePoint first = points.get(index - 1);
|
||||
CurvePoint second = points.get(index);
|
||||
double length = first.distance(second);
|
||||
if (length == 0D) {
|
||||
continue;
|
||||
}
|
||||
while (nextSample <= length) {
|
||||
double progress = nextSample / length;
|
||||
CurvePoint point = new CurvePoint(first.x() + (second.x() - first.x()) * progress,
|
||||
first.y() + (second.y() - first.y()) * progress,
|
||||
first.z() + (second.z() - first.z()) * progress);
|
||||
if (sampled.getLast().distance(point) > 0D) {
|
||||
sampled.add(point);
|
||||
}
|
||||
nextSample += SAMPLE_SPACING;
|
||||
}
|
||||
nextSample -= length;
|
||||
}
|
||||
if (sampled.getLast().distance(points.getLast()) > 0D) {
|
||||
sampled.add(points.getLast());
|
||||
}
|
||||
return List.copyOf(sampled);
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> interpolate(List<HydrologyPoint> guide, double strength, HydrologyRegionalMorphology morphology) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
double spacing = planner.settings.routing().regional().sampleSpacing();
|
||||
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||
double scale = spacing / planner.settings.routing().sampleSpacing();
|
||||
double primaryWavelength = meanders.primaryWavelength() * scale * 5D;
|
||||
double detailWavelength = meanders.detailWavelength() * scale * 8D;
|
||||
for (int index = 0; index + 1 < guide.size(); index++) {
|
||||
HydrologyPoint before = guide.get(Math.max(0, index - 1));
|
||||
HydrologyPoint start = guide.get(index);
|
||||
HydrologyPoint end = guide.get(index + 1);
|
||||
HydrologyPoint after = guide.get(Math.min(guide.size() - 1, index + 2));
|
||||
int steps = Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(start.distanceSquared2D(end)) / SAMPLE_SPACING));
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
double x = spline(before.x(), start.x(), end.x(), after.x(), progress);
|
||||
double z = spline(before.z(), start.z(), end.z(), after.z(), progress);
|
||||
double tangentX = end.x() - before.x() + (after.x() - start.x() - end.x() + before.x()) * progress;
|
||||
double tangentZ = end.z() - before.z() + (after.z() - start.z() - end.z() + before.z()) * progress;
|
||||
double tangentLength = Math.max(1D, StrictMath.hypot(tangentX, tangentZ));
|
||||
double endpointDistance = Math.min(index + progress, guide.size() - 1D - index - progress);
|
||||
double envelope = Math.min(1D, endpointDistance / 2D);
|
||||
double height = start.y() + (end.y() - start.y()) * progress;
|
||||
double primary = bends.noiseSigned(x * SIMPLEX_COORDINATE_SCALE / primaryWavelength,
|
||||
height * SIMPLEX_COORDINATE_SCALE / 96D, z * SIMPLEX_COORDINATE_SCALE / primaryWavelength);
|
||||
double detail = details.noiseSigned(x * SIMPLEX_COORDINATE_SCALE / detailWavelength,
|
||||
height * SIMPLEX_COORDINATE_SCALE / 48D, z * SIMPLEX_COORDINATE_SCALE / detailWavelength);
|
||||
double freedom = morphology.freedomAt(x, z);
|
||||
double broad = primary * meanders.primaryStrength() + detail * meanders.detailStrength() * 0.35D;
|
||||
double tight = detail * (meanders.primaryStrength() + meanders.detailStrength() * 0.35D);
|
||||
double signal = broad * freedom + tight * (1D - freedom);
|
||||
double displacement = Math.max(-1D, Math.min(1D, signal * 2D))
|
||||
* spacing * meanders.maximumOffsetRatio() * envelope * strength * (0.2D + 0.8D * freedom);
|
||||
int pointX = (int) StrictMath.round(x - tangentZ / tangentLength * displacement);
|
||||
int pointZ = (int) StrictMath.round(z + tangentX / tangentLength * displacement);
|
||||
if (points.isEmpty() || points.getLast().x() != pointX || points.getLast().z() != pointZ) {
|
||||
points.add(new HydrologyPoint(pointX, (int) StrictMath.round(height), pointZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private List<HydrologyPoint> smooth(List<HydrologyPoint> points) {
|
||||
for (int pass = 0; pass < planner.settings.geometry().meanders().smoothingPasses(); pass++) {
|
||||
ArrayList<HydrologyPoint> result = new ArrayList<>(points.size());
|
||||
result.add(points.getFirst());
|
||||
for (int index = 1; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint previous = points.get(index - 1);
|
||||
HydrologyPoint point = points.get(index);
|
||||
HydrologyPoint next = points.get(index + 1);
|
||||
HydrologyPoint smoothed = new HydrologyPoint(
|
||||
(int) StrictMath.round((previous.x() + point.x() * 2D + next.x()) / 4D), point.y(),
|
||||
(int) StrictMath.round((previous.z() + point.z() * 2D + next.z()) / 4D));
|
||||
if (smoothed.distanceSquared2D(result.getLast()) > 0L) {
|
||||
result.add(smoothed);
|
||||
}
|
||||
}
|
||||
if (points.getLast().distanceSquared2D(result.getLast()) > 0L) {
|
||||
result.add(points.getLast());
|
||||
}
|
||||
points = result;
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private Refinement validate(List<HydrologyPoint> points, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
if (points.size() < 2) {
|
||||
return new Refinement(List.of(), points.isEmpty() ? new HydrologyPoint(0, 0, 0) : points.getFirst(),
|
||||
HydrologyCandidateRejection.COURSE_TOO_SHORT, points.size(), null);
|
||||
}
|
||||
Refinement corridor = validateHydraulics(validateCorridor(points, profile, coastal), profile, coastal, receiver);
|
||||
if (corridor.rejection() != null) {
|
||||
return corridor;
|
||||
}
|
||||
points = corridor.points();
|
||||
for (int index = 1; index + 1 < points.size(); index++) {
|
||||
double turn = turn(points, index);
|
||||
if (turn > planner.settings.geometry().meanders().maximumTurnDegrees()) {
|
||||
return new Refinement(List.of(), points.get(index), HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED,
|
||||
(int) StrictMath.ceil(turn), null);
|
||||
}
|
||||
}
|
||||
return corridor;
|
||||
}
|
||||
|
||||
private Refinement validateRounded(List<CurvePoint> curve, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>(curve.size());
|
||||
ArrayList<CurvePoint> distinct = new ArrayList<>(curve.size());
|
||||
for (CurvePoint point : curve) {
|
||||
HydrologyPoint rounded = point.rounded();
|
||||
if (points.isEmpty() || points.getLast().distanceSquared2D(rounded) > 0L) {
|
||||
points.add(rounded);
|
||||
distinct.add(point);
|
||||
}
|
||||
}
|
||||
if (points.size() < 2) {
|
||||
return new Refinement(List.of(), curve.getFirst().rounded(), HydrologyCandidateRejection.COURSE_TOO_SHORT, points.size(), null);
|
||||
}
|
||||
Refinement corridor = validateHydraulics(validateCorridor(points, profile, coastal), profile, coastal, receiver);
|
||||
if (corridor.rejection() != null) {
|
||||
return corridor;
|
||||
}
|
||||
if (corridor.oceanEntry() != null) {
|
||||
int last = corridor.points().size() - 1;
|
||||
HydrologyPoint end = corridor.points().getLast();
|
||||
if (points.get(last).distanceSquared2D(end) == 0L) {
|
||||
distinct = new ArrayList<>(distinct.subList(0, last + 1));
|
||||
} else {
|
||||
CurvePoint first = distinct.get(last - 1);
|
||||
CurvePoint second = distinct.get(last);
|
||||
double progress = StrictMath.sqrt(points.get(last - 1).distanceSquared2D(end)
|
||||
/ (double) points.get(last - 1).distanceSquared2D(points.get(last)));
|
||||
distinct = new ArrayList<>(distinct.subList(0, last));
|
||||
distinct.add(new CurvePoint(first.x() + (second.x() - first.x()) * progress,
|
||||
first.y() + (second.y() - first.y()) * progress,
|
||||
first.z() + (second.z() - first.z()) * progress));
|
||||
}
|
||||
}
|
||||
for (int index = 1; index + 1 < distinct.size(); index++) {
|
||||
CurvePoint previous = distinct.get(Math.max(0, index - 4));
|
||||
CurvePoint point = distinct.get(index);
|
||||
CurvePoint next = distinct.get(Math.min(distinct.size() - 1, index + 4));
|
||||
double firstX = point.x() - previous.x();
|
||||
double firstZ = point.z() - previous.z();
|
||||
double nextX = next.x() - point.x();
|
||||
double nextZ = next.z() - point.z();
|
||||
double turn = StrictMath.toDegrees(StrictMath.abs(StrictMath.atan2(firstX * nextZ - firstZ * nextX,
|
||||
firstX * nextX + firstZ * nextZ)));
|
||||
if (turn > planner.settings.geometry().meanders().maximumTurnDegrees()) {
|
||||
return new Refinement(List.of(), point.rounded(), HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED,
|
||||
(int) StrictMath.ceil(turn), null);
|
||||
}
|
||||
}
|
||||
return corridor;
|
||||
}
|
||||
|
||||
private Refinement validateCorridor(List<HydrologyPoint> points, String profile, boolean coastal) {
|
||||
CorridorSamples samples = new CorridorSamples(points);
|
||||
Set<Long> visited = new HashSet<>();
|
||||
HydrologyTerrainSample previousTerrain = null;
|
||||
HydrologyPoint previousPoint = null;
|
||||
double length = 0D;
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
|
||||
for (int step = index == 0 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
||||
HydrologyTerrainSample terrain = samples.next(index, step, x, z);
|
||||
HydrologyPoint failure = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
|
||||
if (!visited.add(RiverFootprint.pack(x, z))) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, 2, null);
|
||||
}
|
||||
double completedLength = length
|
||||
+ (previousPoint == null ? 0D : StrictMath.sqrt(first.distanceSquared2D(previousPoint)));
|
||||
if (terrain != null && terrain.ocean() && previousPoint != null
|
||||
&& receivingEntry(previousTerrain, terrain, profile, coastal, completedLength)) {
|
||||
ArrayList<HydrologyPoint> prefix = new ArrayList<>(points.subList(0, index + 1));
|
||||
if (prefix.getLast().distanceSquared2D(previousPoint) > 0L) {
|
||||
prefix.add(previousPoint);
|
||||
}
|
||||
int seaLevel = planner.settings.seaLevel();
|
||||
OceanEntry entry = new OceanEntry(new HydrologyPoint(previousPoint.x(), seaLevel, previousPoint.z()),
|
||||
new HydrologyPoint(x, seaLevel, z));
|
||||
return new Refinement(List.copyOf(prefix), null, null, 0, entry);
|
||||
}
|
||||
if (terrain != null && !terrain.ocean() && terrain.naturalHeight() < planner.settings.seaLevel()
|
||||
&& previousPoint != null && completedLength >= planner.settings.routing().regional().minimumLength()
|
||||
&& completedLength <= planner.settings.routing().maximumRouteLength()) {
|
||||
return receivingTail(points, new Shore(index, step, previousPoint, previousTerrain), profile, coastal);
|
||||
}
|
||||
double traversedLength = length + StrictMath.sqrt(first.distanceSquared2D(failure));
|
||||
if (traversedLength > planner.settings.routing().maximumRouteLength()) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.ROUTE_LIMIT,
|
||||
(int) StrictMath.ceil(traversedLength), null);
|
||||
}
|
||||
if (terrain == null || !terrain.transitAllowed() || !terrain.preferredProfileKeys().contains(profile)) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.POLICY_EXCLUDED, 0, null);
|
||||
}
|
||||
if (terrain.ocean() || terrain.naturalHeight() < planner.settings.seaLevel()) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, 1, null);
|
||||
}
|
||||
if (previousTerrain != null && (!previousTerrain.drainsInto(terrain)
|
||||
|| coastal && !terrain.drainsInto(previousTerrain))) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.CONFINED_NO_OUTLET, 0, null);
|
||||
}
|
||||
if (coastal && terrain.naturalHeight() - planner.settings.seaLevel()
|
||||
+ planner.settings.surface().minimumDepth()
|
||||
> planner.settings.routing().regional().maximumCoastalIncision()) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_BANK_BUDGET,
|
||||
terrain.naturalHeight() - planner.settings.seaLevel() + planner.settings.surface().minimumDepth(), null);
|
||||
}
|
||||
previousTerrain = terrain;
|
||||
previousPoint = new HydrologyPoint(x, (int) StrictMath.round(first.y() + (second.y() - first.y()) * progress), z);
|
||||
}
|
||||
length += StrictMath.sqrt(first.distanceSquared2D(second));
|
||||
}
|
||||
return new Refinement(points, null, null, 0, null);
|
||||
}
|
||||
|
||||
private Refinement receivingTail(List<HydrologyPoint> points, Shore shore, String profile, boolean coastal) {
|
||||
HydrologyPoint previousPoint = shore.point();
|
||||
HydrologyTerrainSample previous = shore.terrain();
|
||||
HydrologyPoint firstWet = null;
|
||||
int samples = 0;
|
||||
for (int index = shore.segment(); index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint last = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(last.x() - first.x()), Math.abs(last.z() - first.z()));
|
||||
for (int step = index == shore.segment() ? shore.step() : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + (last.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + (last.z() - first.z()) * progress);
|
||||
if (++samples > MAXIMUM_RECEIVER_SAMPLES) {
|
||||
return new Refinement(List.of(), previousPoint, HydrologyCandidateRejection.ROUTE_LIMIT, samples, null);
|
||||
}
|
||||
HydrologyTerrainSample terrain = this.terrain.sample(x, z);
|
||||
HydrologyPoint point = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
|
||||
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, 1, null);
|
||||
}
|
||||
if (!terrain.preferredProfileKeys().contains(profile)) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.POLICY_EXCLUDED, 0, null);
|
||||
}
|
||||
if (!previous.drainsInto(terrain) || coastal && !terrain.drainsInto(previous)) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.CONFINED_NO_OUTLET, 0, null);
|
||||
}
|
||||
if (previousPoint.x() != x && previousPoint.z() != z) {
|
||||
samples += 2;
|
||||
if (samples > MAXIMUM_RECEIVER_SAMPLES
|
||||
|| !wetBridge(previousPoint.x(), z, previous, terrain, profile, coastal)
|
||||
&& !wetBridge(x, previousPoint.z(), previous, terrain, profile, coastal)) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
}
|
||||
if (firstWet == null) {
|
||||
firstWet = point;
|
||||
}
|
||||
if (terrain.ocean()) {
|
||||
return completedReceivingTail(points, shore, firstWet, point, profile, coastal);
|
||||
}
|
||||
previousPoint = point;
|
||||
previous = terrain;
|
||||
}
|
||||
}
|
||||
return new Refinement(List.of(), previousPoint, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
|
||||
private boolean wetBridge(int x, int z, HydrologyTerrainSample previous, HydrologyTerrainSample next,
|
||||
String profile, boolean coastal) {
|
||||
HydrologyTerrainSample bridge = this.terrain.sample(x, z);
|
||||
return bridge != null && bridge.naturalHeight() < planner.settings.seaLevel()
|
||||
&& bridge.preferredProfileKeys().contains(profile) && previous.drainsInto(bridge) && bridge.drainsInto(next)
|
||||
&& (!coastal || next.drainsInto(bridge) && bridge.drainsInto(previous));
|
||||
}
|
||||
|
||||
private Refinement completedReceivingTail(List<HydrologyPoint> points, Shore shore,
|
||||
HydrologyPoint firstWet, HydrologyPoint receiving, String profile, boolean coastal) {
|
||||
int seaLevel = planner.settings.seaLevel();
|
||||
HydrologyPoint landward = new HydrologyPoint(shore.point().x(), seaLevel, shore.point().z());
|
||||
receiving = new HydrologyPoint(receiving.x(), seaLevel, receiving.z());
|
||||
HydrologyTerrainSampler proof = HydrologyOceanReceiver.forConnection(planner.settings,
|
||||
this.terrain::sample, landward, receiving);
|
||||
if (!shore.terrain().outletAllowed() || !proof.receivingWater(firstWet.x(), firstWet.z(), seaLevel)
|
||||
|| !receivingChordAllowed(shore, receiving, profile, coastal)) {
|
||||
return new Refinement(List.of(), firstWet, HydrologyCandidateRejection.SURFACE_MOUTH_DISCONNECTED, 0, null);
|
||||
}
|
||||
ArrayList<HydrologyPoint> prefix = new ArrayList<>(points.subList(0, shore.segment() + 1));
|
||||
if (prefix.getLast().distanceSquared2D(shore.point()) > 0L) {
|
||||
prefix.add(shore.point());
|
||||
}
|
||||
return new Refinement(List.copyOf(prefix), null, null, 0, new OceanEntry(landward, receiving));
|
||||
}
|
||||
|
||||
private boolean receivingChordAllowed(Shore shore, HydrologyPoint end, String profile, boolean coastal) {
|
||||
HydrologyPoint start = shore.point();
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
if (steps > MAXIMUM_RECEIVER_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
HydrologyTerrainSample previous = shore.terrain();
|
||||
HydrologyPoint previousPoint = start;
|
||||
for (int step = 1; 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);
|
||||
HydrologyTerrainSample terrain = this.terrain.sample(x, z);
|
||||
if (terrain == null || terrain.naturalHeight() >= planner.settings.seaLevel()
|
||||
|| !terrain.preferredProfileKeys().contains(profile) || !previous.drainsInto(terrain)
|
||||
|| coastal && !terrain.drainsInto(previous)) {
|
||||
return false;
|
||||
}
|
||||
if (previousPoint.x() != x && previousPoint.z() != z
|
||||
&& !wetBridge(previousPoint.x(), z, previous, terrain, profile, coastal)
|
||||
&& !wetBridge(x, previousPoint.z(), previous, terrain, profile, coastal)) {
|
||||
return false;
|
||||
}
|
||||
previous = terrain;
|
||||
previousPoint = new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Refinement validateHydraulics(Refinement corridor, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
if (corridor.rejection() != null || coastal) {
|
||||
return corridor;
|
||||
}
|
||||
List<HydrologyPoint> points = corridor.points();
|
||||
int count = 1;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
count += Math.max(Math.abs(points.get(index).x() - points.get(index - 1).x()),
|
||||
Math.abs(points.get(index).z() - points.get(index - 1).z()));
|
||||
}
|
||||
HydrologyPlannerSettings.Inlet inlet = planner.settings.surface().banks().inlet();
|
||||
boolean ocean = corridor.oceanEntry() != null || terrain.receivingTerminal(points.getLast(), profile, false, receiver);
|
||||
int reach = ocean ? Math.min(inlet.length(), (int) StrictMath.floor(count * inlet.courseFraction())) : 0;
|
||||
int rampStart = count - reach - reach / 2;
|
||||
int station = 0;
|
||||
for (int index = 0; index + 1 < points.size(); index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
if (first.y() < second.y()) {
|
||||
return new Refinement(List.of(), second, HydrologyCandidateRejection.SURFACE_HEAD_RANGE,
|
||||
second.y() - first.y(), null);
|
||||
}
|
||||
List<HydrologyPoint> raster = planner.segments.rasterLine(first, second);
|
||||
for (int step = index == 0 ? 0 : 1; step < raster.size(); step++) {
|
||||
HydrologyPoint sampled = raster.get(step);
|
||||
double progress = raster.size() < 2 ? 0D : step / (double) (raster.size() - 1);
|
||||
HydrologyPoint point = new HydrologyPoint(sampled.x(),
|
||||
(int) StrictMath.round(first.y() + (second.y() - first.y()) * progress), sampled.z());
|
||||
HydrologyTerrainSample terrain = this.terrain.sample(point.x(), point.z());
|
||||
int incision = terrain.surfacePolicy().maximumIncision(planner.settings.surface().maximumIncision());
|
||||
if (station >= rampStart) {
|
||||
incision = Math.max(incision, inlet.maximumIncision());
|
||||
}
|
||||
int permitted = HydrologySourcePlanner.permittedSurfaceIncision(incision, terrain.incisionMultiplier());
|
||||
int cut = terrain.naturalHeight() - point.y() + planner.settings.surface().minimumDepth();
|
||||
if (cut > permitted) {
|
||||
return new Refinement(List.of(), point, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, cut, null);
|
||||
}
|
||||
station++;
|
||||
}
|
||||
}
|
||||
return corridor;
|
||||
}
|
||||
|
||||
private boolean receivingEntry(HydrologyTerrainSample land, HydrologyTerrainSample ocean,
|
||||
String profile, boolean coastal, double length) {
|
||||
return length >= planner.settings.routing().regional().minimumLength()
|
||||
&& ocean.naturalHeight() < planner.settings.seaLevel()
|
||||
&& ocean.preferredProfileKeys().contains(profile)
|
||||
&& land.outletAllowed() && land.drainsInto(ocean) && (!coastal || ocean.drainsInto(land));
|
||||
}
|
||||
|
||||
private double turn(List<HydrologyPoint> points, int index) {
|
||||
HydrologyPoint previous = points.get(Math.max(0, index - 4));
|
||||
HydrologyPoint point = points.get(index);
|
||||
HydrologyPoint next = points.get(Math.min(points.size() - 1, index + 4));
|
||||
double firstX = point.x() - previous.x();
|
||||
double firstZ = point.z() - previous.z();
|
||||
double nextX = next.x() - point.x();
|
||||
double nextZ = next.z() - point.z();
|
||||
return StrictMath.toDegrees(StrictMath.abs(StrictMath.atan2(firstX * nextZ - firstZ * nextX,
|
||||
firstX * nextX + firstZ * nextZ)));
|
||||
}
|
||||
|
||||
private static double spline(double before, double start, double end, double after, double progress) {
|
||||
return 0.5D * ((2D * start) + (-before + end) * progress
|
||||
+ (2D * before - 5D * start + 4D * end - after) * progress * progress
|
||||
+ (-before + 3D * start - 3D * end + after) * progress * progress * progress);
|
||||
}
|
||||
|
||||
private final class CorridorSamples {
|
||||
private final List<HydrologyPoint> points;
|
||||
private final int workers;
|
||||
private final int[] xs;
|
||||
private final int[] zs;
|
||||
private final HydrologyTerrainSample[] terrain;
|
||||
private final RuntimeException[] failures;
|
||||
private int count;
|
||||
private int cursor;
|
||||
|
||||
private CorridorSamples(List<HydrologyPoint> points) {
|
||||
this.points = points;
|
||||
workers = Thread.currentThread() instanceof ForkJoinWorkerThread worker
|
||||
? Math.min(MAXIMUM_CORRIDOR_WORKERS, worker.getPool().getParallelism()) : 1;
|
||||
xs = workers > 1 ? new int[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
zs = workers > 1 ? new int[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
terrain = workers > 1 ? new HydrologyTerrainSample[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
failures = workers > 1 ? new RuntimeException[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample next(int segment, int step, int x, int z) {
|
||||
if (workers == 1) {
|
||||
return HydrologyRegionalRoute.this.terrain.sample(x, z);
|
||||
}
|
||||
if (cursor == count) {
|
||||
fill(segment, step);
|
||||
}
|
||||
int index = cursor++;
|
||||
if (failures[index] != null) {
|
||||
throw failures[index];
|
||||
}
|
||||
return terrain[index];
|
||||
}
|
||||
|
||||
private void fill(int segment, int firstStep) {
|
||||
count = 0;
|
||||
cursor = 0;
|
||||
Arrays.fill(failures, null);
|
||||
for (int index = segment; index + 1 < points.size() && count < CORRIDOR_SAMPLE_BATCH; index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
|
||||
for (int step = index == segment ? firstStep : 1; step <= steps && count < CORRIDOR_SAMPLE_BATCH; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
xs[count] = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
||||
zs[count++] = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
||||
}
|
||||
}
|
||||
int taskCount = Math.min(workers, Math.ceilDiv(count, 16));
|
||||
if (taskCount < 2) {
|
||||
sampleRange(0, count);
|
||||
return;
|
||||
}
|
||||
int width = Math.ceilDiv(count, taskCount);
|
||||
ArrayList<Callable<Void>> tasks = new ArrayList<>(taskCount);
|
||||
for (int index = 0; index < taskCount; index++) {
|
||||
int start = index * width;
|
||||
int end = Math.min(count, start + width);
|
||||
tasks.add(() -> {
|
||||
sampleRange(start, end);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
HydrologyForkJoin.invokeAll(tasks, null);
|
||||
}
|
||||
|
||||
private void sampleRange(int start, int end) {
|
||||
for (int index = start; index < end; index++) {
|
||||
try {
|
||||
terrain[index] = HydrologyRegionalRoute.this.terrain.sample(xs[index], zs[index]);
|
||||
} catch (RuntimeException failure) {
|
||||
failures[index] = failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CandidateSelection {
|
||||
private final CandidateAdmission admission;
|
||||
private final OceanEntry resolved;
|
||||
private final Set<Refinement> attempted = new HashSet<>();
|
||||
private Attempt failure;
|
||||
private boolean finalFailure;
|
||||
|
||||
private CandidateSelection(CandidateAdmission admission, OceanEntry resolved) {
|
||||
this.admission = admission;
|
||||
this.resolved = resolved;
|
||||
}
|
||||
|
||||
private Attempt consider(Refinement candidate) {
|
||||
candidate = retainReceivingEntry(candidate, resolved);
|
||||
if (candidate.rejection() != null) {
|
||||
if (!finalFailure) {
|
||||
failure = new Attempt(candidate, HydrologyRegionalNetwork.EMPTY, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!attempted.add(candidate)) {
|
||||
return null;
|
||||
}
|
||||
Attempt result = admission.evaluate(candidate);
|
||||
if (result.accepted()) {
|
||||
return result;
|
||||
}
|
||||
failure = result;
|
||||
finalFailure = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
interface CandidateAdmission {
|
||||
Attempt evaluate(Refinement candidate);
|
||||
}
|
||||
|
||||
record Attempt(Refinement refinement, HydrologyRegionalNetwork network, HydrologyDiagnosticCandidate rejection) {
|
||||
boolean accepted() {
|
||||
return refinement.rejection() == null && rejection == null;
|
||||
}
|
||||
}
|
||||
|
||||
private record Shore(int segment, int step, HydrologyPoint point, HydrologyTerrainSample terrain) {
|
||||
}
|
||||
|
||||
private record CurvePoint(double x, double y, double z) {
|
||||
private static CurvePoint of(HydrologyPoint point) {
|
||||
return new CurvePoint(point.x(), point.y(), point.z());
|
||||
}
|
||||
|
||||
private double distance(CurvePoint other) {
|
||||
return StrictMath.hypot(x - other.x(), z - other.z());
|
||||
}
|
||||
|
||||
private HydrologyPoint rounded() {
|
||||
return new HydrologyPoint((int) StrictMath.round(x), (int) StrictMath.round(y), (int) StrictMath.round(z));
|
||||
}
|
||||
}
|
||||
|
||||
record OceanEntry(HydrologyPoint landward, HydrologyPoint receiving) {
|
||||
}
|
||||
|
||||
record Refinement(List<HydrologyPoint> points, HydrologyPoint failure,
|
||||
HydrologyCandidateRejection rejection, int detail, OceanEntry oceanEntry) {
|
||||
}
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRegionalSourceTrials {
|
||||
private static final int LENGTH_BANDS = 4;
|
||||
|
||||
private final HydrologySampledGrid grid;
|
||||
private final List<Catchment> catchments;
|
||||
private int cursor;
|
||||
|
||||
HydrologyRegionalSourceTrials(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree, List<Integer> sources) {
|
||||
this.grid = grid;
|
||||
this.catchments = new ArrayList<>(tree.outlets().size());
|
||||
ArrayList<Integer> sorted = new ArrayList<>(sources);
|
||||
sorted.sort(Comparator.comparingDouble((Integer source) -> tree.length(source)).reversed()
|
||||
.thenComparingLong(source -> grid.node(source).id()));
|
||||
for (int root = 0; root < tree.outlets().size(); root++) {
|
||||
Catchment catchment = new Catchment(tree.outlets().get(root).outlet().id());
|
||||
double minimum = Double.POSITIVE_INFINITY;
|
||||
double maximum = 0D;
|
||||
for (int source : sorted) {
|
||||
if (tree.root(source) == root) {
|
||||
minimum = Math.min(minimum, tree.length(source));
|
||||
maximum = Math.max(maximum, tree.length(source));
|
||||
}
|
||||
}
|
||||
if (maximum == 0D) {
|
||||
continue;
|
||||
}
|
||||
double range = Math.max(1D, maximum - minimum);
|
||||
for (int source : sorted) {
|
||||
if (tree.root(source) == root) {
|
||||
int band = Math.min(LENGTH_BANDS - 1,
|
||||
(int) ((maximum - tree.length(source)) * LENGTH_BANDS / range));
|
||||
catchment.bands.get(band).add(source);
|
||||
}
|
||||
}
|
||||
catchments.add(catchment);
|
||||
}
|
||||
}
|
||||
|
||||
OptionalInt next(Set<Long> usedOutlets) {
|
||||
for (int checked = 0; checked < catchments.size(); checked++) {
|
||||
Catchment catchment = catchments.get(cursor++ % catchments.size());
|
||||
if (usedOutlets.contains(catchment.outlet)) {
|
||||
continue;
|
||||
}
|
||||
for (int band = 0; band < LENGTH_BANDS; band++) {
|
||||
List<Integer> candidates = catchment.bands.get(catchment.cursor++ % LENGTH_BANDS);
|
||||
if (!candidates.isEmpty()) {
|
||||
int source = candidates.remove(farthest(candidates, catchment.attempted));
|
||||
catchment.attempted.add(grid.node(source).naturalPoint());
|
||||
return OptionalInt.of(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
private int farthest(List<Integer> candidates, List<HydrologyPoint> attempted) {
|
||||
int selected = 0;
|
||||
long separation = -1L;
|
||||
for (int index = 0; index < candidates.size(); index++) {
|
||||
HydrologyPoint point = grid.node(candidates.get(index)).naturalPoint();
|
||||
long nearest = Long.MAX_VALUE;
|
||||
for (HydrologyPoint previous : attempted) {
|
||||
nearest = Math.min(nearest, point.distanceSquared2D(previous));
|
||||
}
|
||||
if (nearest > separation) {
|
||||
selected = index;
|
||||
separation = nearest;
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static final class Catchment {
|
||||
private final long outlet;
|
||||
private final List<List<Integer>> bands;
|
||||
private final List<HydrologyPoint> attempted;
|
||||
private int cursor;
|
||||
|
||||
private Catchment(long outlet) {
|
||||
this.outlet = outlet;
|
||||
this.bands = new ArrayList<>(LENGTH_BANDS);
|
||||
this.attempted = new ArrayList<>();
|
||||
for (int band = 0; band < LENGTH_BANDS; band++) {
|
||||
bands.add(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
final class HydrologyRegionalTerrain implements HydrologyTerrainSampler {
|
||||
private static final int MAXIMUM_CACHED_SAMPLES = 4194304;
|
||||
private static final int[][] CARDINAL = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyCacheBudget budget;
|
||||
private volatile Cache<Long, Terrain> terrain;
|
||||
|
||||
HydrologyRegionalTerrain(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.budget = HydrologyCacheBudget.runtime(planner.settings.routing().regional().enabled());
|
||||
this.terrain = cache();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
terrain = cache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sample(int x, int z) {
|
||||
Cache<Long, Terrain> active = terrain;
|
||||
long key = CacheKey.mix(RiverFootprint.pack(x, z));
|
||||
Terrain cached = active.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return cached.sample();
|
||||
}
|
||||
Terrain sampled = new Terrain(planner.naturalSampler == null
|
||||
? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z));
|
||||
Terrain previous = active.asMap().putIfAbsent(key, sampled);
|
||||
return (previous == null ? sampled : previous).sample();
|
||||
}
|
||||
|
||||
boolean receivingTerminal(HydrologyPoint point, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
HydrologyTerrainSample land = sample(point.x(), point.z());
|
||||
if (land == null || land.ocean() || land.naturalHeight() < planner.settings.seaLevel() || !land.outletAllowed()) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < 4; index++) {
|
||||
int[] offset = CARDINAL[index];
|
||||
HydrologyTerrainSample ocean = sample(point.x() + offset[0], point.z() + offset[1]);
|
||||
if (ocean != null && receiver.receivingWater(point.x() + offset[0], point.z() + offset[1], planner.settings.seaLevel())
|
||||
&& ocean.preferredProfileKeys().contains(profile) && land.drainsInto(ocean)
|
||||
&& (!coastal || ocean.drainsInto(land))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Cache<Long, Terrain> cache() {
|
||||
return Caffeine.newBuilder()
|
||||
.maximumWeight(budget.regionalTerrainBytes())
|
||||
.weigher((Long key, Terrain value) -> HydrologyCacheWeights.bounded(
|
||||
HydrologyCacheWeights.terrain(value.sample()), budget.regionalTerrainBytes(),
|
||||
(int) Math.max(65536L, Math.min(MAXIMUM_CACHED_SAMPLES, Runtime.getRuntime().maxMemory() / 8192L))))
|
||||
.build();
|
||||
}
|
||||
|
||||
private record Terrain(HydrologyTerrainSample sample) {
|
||||
}
|
||||
}
|
||||
-231
@@ -1,231 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
final class HydrologyRegionalTributaries {
|
||||
private static final long JUNCTION_SALT = 0x5245474a554e4354L;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
|
||||
HydrologyRegionalTributaries(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
}
|
||||
|
||||
CompiledGraph connect(CompiledGraph graph, List<RiverCourse> courses,
|
||||
HydrologyFootprintCompiler footprints, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
HydrologyRegionalNetwork regional = footprints.regionalNetwork;
|
||||
if (regional.courses().isEmpty()) {
|
||||
return graph;
|
||||
}
|
||||
LinkedHashMap<Long, DrainageNode> nodes = new LinkedHashMap<>();
|
||||
LinkedHashMap<Long, DrainageEdge> edges = new LinkedHashMap<>();
|
||||
LinkedHashMap<Long, RiverOutlet> outlets = new LinkedHashMap<>();
|
||||
for (DrainageNode node : graph.nodes()) { nodes.put(node.id(), node); }
|
||||
for (DrainageEdge edge : graph.edges()) { edges.put(edge.id(), edge); }
|
||||
for (RiverOutlet outlet : graph.outlets()) { outlets.put(outlet.id(), outlet); }
|
||||
Map<Long, DrainageNode> regionalNodes = new HashMap<>();
|
||||
Map<Long, RiverOutlet> regionalOutlets = new HashMap<>();
|
||||
for (DrainageNode node : regional.nodes()) { regionalNodes.put(node.id(), node); }
|
||||
for (RiverOutlet outlet : regional.outlets()) { regionalOutlets.put(outlet.id(), outlet); }
|
||||
ListIterator<RiverCourse> iterator = courses.listIterator();
|
||||
while (iterator.hasNext()) {
|
||||
RiverCourse local = iterator.next();
|
||||
if (local.type() != RiverCourseType.SURFACE || !planner.regional.conflicts(local, regional.courses())) {
|
||||
continue;
|
||||
}
|
||||
DrainageNode source = nodes.get(local.sourceNodeId().orElseThrow());
|
||||
Junction accepted = null;
|
||||
for (RiverCourse stem : regional.courses()) {
|
||||
if (!planner.regional.conflicts(local, List.of(stem)) || source == null) {
|
||||
continue;
|
||||
}
|
||||
RiverOutlet outlet = regionalOutlets.get(stem.outletId().orElseThrow());
|
||||
accepted = build(local, source, new Receiver(stem, outlet, regionalNodes), footprints, diagnostics);
|
||||
if (accepted != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (accepted == null || conflictsOtherStem(accepted.course(), regional.courses())) {
|
||||
iterator.remove();
|
||||
planner.tributaries.addTributaryDiagnostic(local.id(), local.segments().getFirst().start(),
|
||||
HydrologyCandidateRejection.NO_DRAINAGE_PATH, 0, diagnostics);
|
||||
continue;
|
||||
}
|
||||
iterator.set(accepted.course());
|
||||
for (DrainageNode node : accepted.nodes()) { nodes.put(node.id(), node); }
|
||||
for (DrainageEdge edge : accepted.course().drainageEdges()) { edges.put(edge.id(), edge); }
|
||||
outlets.put(accepted.outlet().id(), accepted.outlet());
|
||||
}
|
||||
return new CompiledGraph(List.copyOf(nodes.values()), List.copyOf(edges.values()),
|
||||
List.copyOf(outlets.values()), graph.edgeByUpstream());
|
||||
}
|
||||
|
||||
private Junction build(RiverCourse local, DrainageNode source, Receiver receiver,
|
||||
HydrologyFootprintCompiler footprints, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
if (receiver.outlet() == null || local.drainageEdges().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<HydrologyPoint> points = points(local);
|
||||
ArrayList<DrainageEdge> pairs = new ArrayList<>(Math.max(0, points.size() - 1));
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
pairs.add(local.drainageEdges().getFirst());
|
||||
}
|
||||
HydrologyCoursePath path = new HydrologyCoursePath(points, pairs, local.drainageEdges(), receiver.outlet(),
|
||||
false, false, null);
|
||||
HydrologyGridNode sourceNode = new HydrologyGridNode(0, 0, 0, source.x(), source.z(), source.id(), source.terrain());
|
||||
RiverCourse tributary = planner.tributaries.buildSurfaceTributary(
|
||||
new SurfaceCourseDraft(sourceNode, local.id(), local.profileKey(), path), receiver.course(), diagnostics);
|
||||
if (tributary == null || !confined(tributary)) {
|
||||
return null;
|
||||
}
|
||||
SurfaceFootprint footprint = footprints.surfaceFootprintForPublication(tributary);
|
||||
if (!footprint.accepted() || !joinsWater(tributary, footprint, receiver.course())) {
|
||||
if (!footprint.accepted()) {
|
||||
planner.tributaries.addTributaryDiagnostic(local.id(), local.segments().getFirst().start(),
|
||||
footprint.rejection(), footprint.rejectionDetail(), diagnostics);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return reroot(tributary, source, receiver);
|
||||
}
|
||||
|
||||
private boolean conflictsOtherStem(RiverCourse course, List<RiverCourse> stems) {
|
||||
for (RiverCourse stem : stems) {
|
||||
if (stem.outletId().getAsLong() != course.outletId().getAsLong()
|
||||
&& planner.regional.conflicts(course, List.of(stem))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean joinsWater(RiverCourse tributary, SurfaceFootprint footprint, RiverCourse stem) {
|
||||
HydrologyPoint end = tributary.segments().getLast().end();
|
||||
SurfaceBounds bounds = new SurfaceBounds(end.x(), end.z(), end.x(), end.z());
|
||||
SurfaceFootprint receiving = new SurfaceFootprintCompiler(planner.settings,
|
||||
planner::sampleBasisWithoutSlope, planner.geometrySampler).compile(stem, bounds);
|
||||
HydrologyColumnLayer receiver = null;
|
||||
for (SurfaceLayerColumn column : receiving.columns()) {
|
||||
if (column.layer().fluidOwned()) {
|
||||
receiver = column.layer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!receiving.accepted() || receiver == null) {
|
||||
return false;
|
||||
}
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
HydrologyColumnLayer layer = column.layer();
|
||||
if (column.x() == end.x() && column.z() == end.z() && layer.fluidOwned()) {
|
||||
return layer.fluidHeadY() == receiver.fluidHeadY()
|
||||
&& Math.max(layer.bedY(), receiver.bedY()) < layer.fluidHeadY();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean confined(RiverCourse course) {
|
||||
HydrologyTerrainSample previous = null;
|
||||
List<HydrologyPoint> points = points(course);
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
HydrologyPoint start = points.get(index - 1);
|
||||
HydrologyPoint end = points.get(index);
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
for (int step = index == 1 ? 0 : 1; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||
HydrologyTerrainSample terrain = planner.sampleBasisWithoutSlope(x, z);
|
||||
if (terrain == null || terrain.ocean() || !terrain.transitAllowed()
|
||||
|| !terrain.preferredProfileKeys().contains(course.profileKey())
|
||||
|| previous != null && !previous.drainsInto(terrain)) {
|
||||
return false;
|
||||
}
|
||||
previous = terrain;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Junction reroot(RiverCourse tributary, DrainageNode source, Receiver receiver) {
|
||||
HydrologyPoint end = tributary.segments().getLast().end();
|
||||
DrainageEdge nearest = null;
|
||||
double nearestDistance = Double.POSITIVE_INFINITY;
|
||||
for (DrainageEdge edge : receiver.course().drainageEdges()) {
|
||||
HydrologyPoint start = edge.centerline().getFirst();
|
||||
HydrologyPoint finish = edge.centerline().getLast();
|
||||
double deltaX = finish.x() - start.x();
|
||||
double deltaZ = finish.z() - start.z();
|
||||
double squared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
double progress = squared == 0D ? 0D : Math.max(0D, Math.min(1D,
|
||||
((end.x() - start.x()) * deltaX + (end.z() - start.z()) * deltaZ) / squared));
|
||||
double distance = StrictMath.hypot(end.x() - start.x() - deltaX * progress,
|
||||
end.z() - start.z() - deltaZ * progress);
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
nearest = edge;
|
||||
}
|
||||
}
|
||||
if (nearest == null) {
|
||||
return null;
|
||||
}
|
||||
DrainageNode downstream = receiver.nodes().get(nearest.downstreamNodeId());
|
||||
HydrologyTerrainSample junctionTerrain = planner.sampleBasisWithoutSlope(end.x(), end.z());
|
||||
long outletId = receiver.outlet().id();
|
||||
long sourceId = HydrologyHash.mix(tributary.id(), JUNCTION_SALT, source.id());
|
||||
long junctionId = HydrologyHash.mix(tributary.id(), JUNCTION_SALT, end.x(), end.z());
|
||||
double connectorLength = Math.max(1D, StrictMath.sqrt(end.distanceSquared2D(downstream.naturalPoint())));
|
||||
double sourceLength = Math.max(1D, length(points(tributary)));
|
||||
DrainageNode junction = new DrainageNode(junctionId, end.x(), end.z(), junctionTerrain,
|
||||
downstream.potential() + connectorLength, outletId);
|
||||
DrainageNode origin = new DrainageNode(sourceId, source.x(), source.z(), source.terrain(),
|
||||
junction.potential() + sourceLength, outletId);
|
||||
DrainageEdge branch = new DrainageEdge(HydrologyHash.mix(sourceId, junctionId), sourceId, junctionId,
|
||||
outletId, sourceLength, tributary.discharge(), 0, points(tributary));
|
||||
DrainageEdge connector = new DrainageEdge(HydrologyHash.mix(junctionId, downstream.id()), junctionId, downstream.id(),
|
||||
outletId, connectorLength, tributary.discharge(), 0, List.of(end, downstream.naturalPoint()));
|
||||
RiverCourse connected = new RiverCourse(tributary.id(), tributary.type(), OptionalLong.of(sourceId),
|
||||
OptionalLong.of(outletId), tributary.profileKey(), tributary.discharge(), List.of(branch, connector), tributary.segments());
|
||||
return new Junction(connected, List.of(origin, junction, downstream), receiver.outlet());
|
||||
}
|
||||
|
||||
private static List<HydrologyPoint> points(RiverCourse course) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!segment.type().isSurface()) {
|
||||
break;
|
||||
}
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (points.isEmpty() || point.distanceSquared2D(points.getLast()) > 0L) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(points);
|
||||
}
|
||||
|
||||
private static double length(List<HydrologyPoint> points) {
|
||||
double length = 0D;
|
||||
for (int index = 1; index < points.size(); index++) {
|
||||
length += StrictMath.sqrt(points.get(index).distanceSquared2D(points.get(index - 1)));
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
private record Receiver(RiverCourse course, RiverOutlet outlet, Map<Long, DrainageNode> nodes) {
|
||||
}
|
||||
|
||||
private record Junction(RiverCourse course, List<DrainageNode> nodes, RiverOutlet outlet) {
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record HydrologyRenderSample(int x, int z, List<HydrologyFeatureRef> features) {
|
||||
private static final Comparator<HydrologyFeatureRef> FEATURE_ORDER = Comparator
|
||||
.comparingInt((HydrologyFeatureRef feature) -> feature.type().renderPriority())
|
||||
.thenComparingLong(HydrologyFeatureRef::id);
|
||||
|
||||
public HydrologyRenderSample {
|
||||
Objects.requireNonNull(features, "features");
|
||||
ArrayList<HydrologyFeatureRef> ordered = new ArrayList<>(features);
|
||||
ordered.sort(FEATURE_ORDER);
|
||||
Map<Long, HydrologyFeatureRef> unique = new LinkedHashMap<>();
|
||||
for (HydrologyFeatureRef feature : ordered) {
|
||||
unique.putIfAbsent(feature.id(), feature);
|
||||
}
|
||||
features = List.copyOf(unique.values());
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
return !features.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasFeature(HydrologyFeatureType type) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
for (HydrologyFeatureRef feature : features) {
|
||||
if (feature.type() == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Optional<HydrologyFeatureRef> primaryFeature() {
|
||||
return features.isEmpty() ? Optional.empty() : Optional.of(features.getFirst());
|
||||
}
|
||||
}
|
||||
@@ -1,650 +0,0 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class HydrologyRouteGeometry {
|
||||
private final HydrologyPlanner planner;
|
||||
|
||||
HydrologyRouteGeometry(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
}
|
||||
|
||||
static final long ROUTE_ANCHOR_X_SALT = 0x52414e434858L;
|
||||
static final long ROUTE_ANCHOR_Z_SALT = 0x52414e43485aL;
|
||||
static final long ROUTE_WORM_PRIMARY_SALT = 0x52575052494dL;
|
||||
static final long ROUTE_WORM_DETAIL_SALT = 0x525744455441L;
|
||||
static final double ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO = 0.32D;
|
||||
static final double ROUTE_TANGENT_SCALE = 0.68D;
|
||||
static final int ROUTE_TRANSVERSE_CANDIDATES = 2;
|
||||
static final int ROUTE_FALLBACK_TRANSVERSE_CANDIDATES = 4;
|
||||
static final int MAXIMUM_SURFACE_BORE_STEP = 4;
|
||||
|
||||
HydrologyPoint edgeContinuation(
|
||||
HydrologySampledGrid grid,
|
||||
HydrologyRoutingPlan routing,
|
||||
HydrologyGridNode downstream,
|
||||
RiverOutlet outlet
|
||||
) {
|
||||
int continuationIndex = routing.parent()[downstream.index()];
|
||||
if (continuationIndex >= 0) {
|
||||
return routeAnchor(grid.node(continuationIndex), false);
|
||||
}
|
||||
HydrologyPoint landward = outlet.landwardPoint();
|
||||
if (landward.x() != downstream.x() || landward.z() != downstream.z()) {
|
||||
return landward;
|
||||
}
|
||||
return outlet.connectionPoint();
|
||||
}
|
||||
|
||||
HydrologyPoint routeAnchor(HydrologyGridNode node, boolean surface) {
|
||||
HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get();
|
||||
HashMap<Long, HydrologyPoint> anchors = samples == null ? null
|
||||
: surface ? samples.surfaceRouteAnchors : samples.routeAnchors;
|
||||
if (samples != null) {
|
||||
HydrologyPoint cached = anchors.get(node.id());
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
int spacing = planner.settings.routing().sampleSpacing();
|
||||
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||
double maximumOffsetRatio = Math.min(
|
||||
ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO,
|
||||
meanders.maximumOffsetRatio() * 0.625D
|
||||
);
|
||||
if (maximumOffsetRatio <= 0D) {
|
||||
return node.naturalPoint();
|
||||
}
|
||||
double primaryWavelength = meanders.primaryWavelength() * 5D;
|
||||
double detailWavelength = meanders.detailWavelength() * 8D;
|
||||
double primaryX = planner.routeAnchorX.noiseSigned(
|
||||
node.x() / primaryWavelength,
|
||||
node.z() / primaryWavelength
|
||||
);
|
||||
double primaryZ = planner.routeAnchorZ.noiseSigned(
|
||||
node.x() / primaryWavelength,
|
||||
node.z() / primaryWavelength
|
||||
);
|
||||
double detailX = planner.routeAnchorX.noiseSigned(
|
||||
node.x() / detailWavelength + 31.75D,
|
||||
node.z() / detailWavelength - 19.25D
|
||||
);
|
||||
double detailZ = planner.routeAnchorZ.noiseSigned(
|
||||
node.x() / detailWavelength - 27.5D,
|
||||
node.z() / detailWavelength + 43.5D
|
||||
);
|
||||
double vectorX = primaryX * 0.72D + detailX * 0.28D;
|
||||
double vectorZ = primaryZ * 0.72D + detailZ * 0.28D;
|
||||
double vectorLength = StrictMath.hypot(vectorX, vectorZ);
|
||||
if (vectorLength < 0.1D) {
|
||||
double angle = HydrologyHash.unit(HydrologyHash.mix(planner.worldSeed, node.id(), ROUTE_ANCHOR_X_SALT))
|
||||
* StrictMath.PI * 2D;
|
||||
vectorX = StrictMath.cos(angle);
|
||||
vectorZ = StrictMath.sin(angle);
|
||||
vectorLength = 1D;
|
||||
}
|
||||
double maximumOffset = spacing * maximumOffsetRatio;
|
||||
double displacement = maximumOffset * (0.62D + Math.min(1D, vectorLength) * 0.38D);
|
||||
int desiredX = (int) StrictMath.round(node.x() + vectorX / vectorLength * displacement);
|
||||
int desiredZ = (int) StrictMath.round(node.z() + vectorZ / vectorLength * displacement);
|
||||
int searchRadius = Math.max(planner.settings.routing().refinementSpacing(), spacing / 5);
|
||||
int innerRadius = Math.max(1, searchRadius / 2);
|
||||
int[][] offsets = {
|
||||
{0, 0},
|
||||
{innerRadius, 0}, {-innerRadius, 0}, {0, innerRadius}, {0, -innerRadius},
|
||||
{innerRadius, innerRadius}, {innerRadius, -innerRadius},
|
||||
{-innerRadius, innerRadius}, {-innerRadius, -innerRadius},
|
||||
{searchRadius, 0}, {-searchRadius, 0}, {0, searchRadius}, {0, -searchRadius},
|
||||
{node.x() - desiredX, node.z() - desiredZ}
|
||||
};
|
||||
HydrologyPoint selected = node.naturalPoint();
|
||||
double selectedScore = anchorScore(
|
||||
node,
|
||||
selected.x(),
|
||||
selected.z(),
|
||||
desiredX,
|
||||
desiredZ,
|
||||
maximumOffsetRatio,
|
||||
surface
|
||||
);
|
||||
for (int[] offset : offsets) {
|
||||
int x = desiredX + offset[0];
|
||||
int z = desiredZ + offset[1];
|
||||
double score = anchorScore(node, x, z, desiredX, desiredZ, maximumOffsetRatio, surface);
|
||||
if (score < selectedScore) {
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasis(x, z);
|
||||
selected = new HydrologyPoint(x, terrain.naturalHeight(), z);
|
||||
selectedScore = score;
|
||||
}
|
||||
}
|
||||
if (samples != null) {
|
||||
anchors.put(node.id(), selected);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
double anchorScore(
|
||||
HydrologyGridNode node,
|
||||
int x,
|
||||
int z,
|
||||
int desiredX,
|
||||
int desiredZ,
|
||||
double maximumOffsetRatio,
|
||||
boolean surface
|
||||
) {
|
||||
double distanceFromNode = StrictMath.hypot(x - node.x(), z - node.z());
|
||||
if (distanceFromNode > planner.settings.routing().sampleSpacing() * maximumOffsetRatio) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x, z);
|
||||
if (terrain == null || !terrain.transitAllowed() || !withinConfines(terrain, node.terrain().confinesKey())) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
if (surface) {
|
||||
int maximumDrop = Math.max(0, Math.min(planner.sourcePlanner.permittedSurfaceIncision(node.terrain()),
|
||||
planner.sourcePlanner.permittedSurfaceIncision(terrain))
|
||||
- planner.settings.surface().banks().sink() - planner.settings.surface().minimumDepth());
|
||||
int drop = node.terrain().naturalHeight() - terrain.naturalHeight();
|
||||
if (drop < 0 || drop > maximumDrop
|
||||
|| !HydrologySurfaceProfiles.sharesProfile(node.terrain(), terrain)
|
||||
|| !node.terrain().surfacePolicy().areaKey().equals(terrain.surfacePolicy().areaKey())) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
if (!planner.routePaths.traversableHop(node.naturalPoint(), new HydrologyPoint(x, terrain.naturalHeight(), z))) {
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
terrain = planner.sampleLandBasis(x, z);
|
||||
double distanceFromDesired = StrictMath.hypot(x - desiredX, z - desiredZ);
|
||||
double score = terrain.naturalHeight() * planner.settings.routing().valleyPreference()
|
||||
+ terrain.slope() * planner.settings.routing().slopePenalty()
|
||||
+ terrain.routingCost() * terrain.routingMultiplier()
|
||||
+ distanceFromDesired * 2.4D
|
||||
+ distanceFromNode * 0.08D;
|
||||
long tie = HydrologyHash.mix(planner.worldSeed, node.id(), x, z);
|
||||
return score + HydrologyHash.unit(tie) * 1.0E-6D;
|
||||
}
|
||||
|
||||
List<HydrologyPoint> refineUndergroundEdge(
|
||||
long upstreamId,
|
||||
long downstreamId,
|
||||
HydrologyPoint upstream,
|
||||
HydrologyPoint downstream,
|
||||
HydrologyPoint continuation,
|
||||
int transverseCandidates
|
||||
) {
|
||||
int refinement = planner.settings.routing().refinementSpacing();
|
||||
double deltaX = downstream.x() - upstream.x();
|
||||
double deltaZ = downstream.z() - upstream.z();
|
||||
double distance = StrictMath.hypot(deltaX, deltaZ);
|
||||
int steps = Math.max(1, (int) StrictMath.ceil(distance / refinement));
|
||||
RouteDirection directTangent = planner.routePaths.direction(upstream.x(), upstream.z(), downstream.x(), downstream.z());
|
||||
RouteDirection startTangent = directTangent;
|
||||
RouteDirection endTangent = planner.routePaths.direction(downstream.x(), downstream.z(), continuation.x(), continuation.z());
|
||||
if (endTangent.x() == 0D && endTangent.z() == 0D) {
|
||||
endTangent = startTangent;
|
||||
}
|
||||
HydrologyTerrainSample upstreamTerrain = planner.sampleLandBasis(upstream.x(), upstream.z());
|
||||
HydrologyTerrainSample downstreamTerrain = planner.sampleLandBasis(downstream.x(), downstream.z());
|
||||
int transitionThreshold = planner.settings.hydraulics().waterfallMinimumDrop();
|
||||
boolean elevationTransition = Math.abs(upstream.y() - downstream.y()) >= transitionThreshold
|
||||
|| upstreamTerrain != null && upstreamTerrain.slope() >= transitionThreshold
|
||||
|| downstreamTerrain != null && downstreamTerrain.slope() >= transitionThreshold;
|
||||
if (elevationTransition) {
|
||||
endTangent = directTangent;
|
||||
}
|
||||
double wormBlend = elevationTransition ? 0D : routeWormBlend();
|
||||
double tangentMagnitude = distance * (ROUTE_TANGENT_SCALE + wormBlend * 0.42D);
|
||||
if (wormBlend > 0D) {
|
||||
startTangent = routeWormDirection(upstream, startTangent, wormBlend);
|
||||
endTangent = routeWormDirection(downstream, endTangent, wormBlend);
|
||||
}
|
||||
ArrayList<List<RouteCandidate>> layers = new ArrayList<>(steps + 1);
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
if (step == 0) {
|
||||
layers.add(List.of(routeEndpoint(upstream, startTangent)));
|
||||
continue;
|
||||
}
|
||||
if (step == steps) {
|
||||
layers.add(List.of(routeEndpoint(downstream, endTangent)));
|
||||
continue;
|
||||
}
|
||||
double progress = step / (double) steps;
|
||||
RoutePosition nominal = routePosition(
|
||||
upstream,
|
||||
downstream,
|
||||
startTangent,
|
||||
endTangent,
|
||||
tangentMagnitude,
|
||||
distance,
|
||||
progress,
|
||||
elevationTransition ? 0D : 1D
|
||||
);
|
||||
List<RouteCandidate> candidates = routeCandidates(
|
||||
upstreamId,
|
||||
downstreamId,
|
||||
nominal,
|
||||
progress,
|
||||
refinement,
|
||||
elevationTransition ? 0 : transverseCandidates,
|
||||
upstreamTerrain == null ? null : upstreamTerrain.confinesKey()
|
||||
);
|
||||
if (candidates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
layers.add(candidates);
|
||||
}
|
||||
return solveTerrainRoute(layers, distance / steps);
|
||||
}
|
||||
|
||||
double routeWormBlend() {
|
||||
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||
double strength = meanders.primaryStrength() + meanders.detailStrength() * 0.35D;
|
||||
if (strength <= 0D || meanders.maximumOffsetRatio() <= 0D) {
|
||||
return 0D;
|
||||
}
|
||||
return Math.min(0.18D, meanders.maximumOffsetRatio() * (0.08D + strength * 0.05D));
|
||||
}
|
||||
|
||||
RouteDirection routeWormDirection(HydrologyPoint point, RouteDirection fallback, double blend) {
|
||||
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||
double wavelength = Math.max(
|
||||
planner.settings.routing().sampleSpacing() * 2D,
|
||||
meanders.primaryWavelength() * 2.5D
|
||||
);
|
||||
double fieldX = planner.routeWormPrimary.noiseSigned(point.x() / wavelength, point.z() / wavelength);
|
||||
double fieldZ = planner.routeWormDetail.noiseSigned(
|
||||
point.x() / wavelength + 37.25D,
|
||||
point.z() / wavelength - 21.75D
|
||||
);
|
||||
double fieldLength = StrictMath.hypot(fieldX, fieldZ);
|
||||
if (fieldLength < 0.05D) {
|
||||
return fallback;
|
||||
}
|
||||
fieldX /= fieldLength;
|
||||
fieldZ /= fieldLength;
|
||||
if (fieldX * fallback.x() + fieldZ * fallback.z() < 0D) {
|
||||
fieldX = -fieldX;
|
||||
fieldZ = -fieldZ;
|
||||
}
|
||||
double blendedX = fallback.x() * (1D - blend) + fieldX * blend;
|
||||
double blendedZ = fallback.z() * (1D - blend) + fieldZ * blend;
|
||||
double blendedLength = StrictMath.hypot(blendedX, blendedZ);
|
||||
return blendedLength <= 0D
|
||||
? fallback
|
||||
: new RouteDirection(blendedX / blendedLength, blendedZ / blendedLength);
|
||||
}
|
||||
|
||||
List<HydrologyPoint> constrainedTerrainFallbackEdge(
|
||||
HydrologyPoint upstream,
|
||||
HydrologyPoint downstream
|
||||
) {
|
||||
if (planner.routePaths.crossesTerrainCrevasse(upstream, downstream)) {
|
||||
return List.of();
|
||||
}
|
||||
double deltaX = downstream.x() - upstream.x();
|
||||
double deltaZ = downstream.z() - upstream.z();
|
||||
double distance = StrictMath.hypot(deltaX, deltaZ);
|
||||
if (distance <= 0D) {
|
||||
return List.of();
|
||||
}
|
||||
int refinement = planner.settings.routing().refinementSpacing();
|
||||
double perpendicularX = -deltaZ / distance;
|
||||
double perpendicularZ = deltaX / distance;
|
||||
List<HydrologyPoint> direct = planner.segments.line(upstream, downstream, refinement);
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>(direct.size());
|
||||
for (int index = 0; index < direct.size(); index++) {
|
||||
HydrologyPoint point = direct.get(index);
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
|
||||
if (terrain == null || !terrain.transitAllowed()) {
|
||||
return List.of();
|
||||
}
|
||||
if (index > 0 && index < direct.size() - 1
|
||||
&& hasLateralRouteFreedom(point, perpendicularX, perpendicularZ, refinement)) {
|
||||
return List.of();
|
||||
}
|
||||
points.add(new HydrologyPoint(point.x(), terrain.naturalHeight(), point.z()));
|
||||
}
|
||||
return planner.routePaths.traversableRoute(points) && !planner.routePaths.containsTerrainPit(points)
|
||||
? List.copyOf(points)
|
||||
: List.of();
|
||||
}
|
||||
|
||||
boolean hasLateralRouteFreedom(
|
||||
HydrologyPoint point,
|
||||
double perpendicularX,
|
||||
double perpendicularZ,
|
||||
int refinement
|
||||
) {
|
||||
for (int direction : List.of(-1, 1)) {
|
||||
int x = (int) StrictMath.round(point.x() + perpendicularX * refinement * direction);
|
||||
int z = (int) StrictMath.round(point.z() + perpendicularZ * refinement * direction);
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(x, z);
|
||||
if (terrain != null && terrain.transitAllowed()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RouteCandidate routeEndpoint(
|
||||
HydrologyPoint point,
|
||||
RouteDirection tangent
|
||||
) {
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
|
||||
if (terrain == null || !terrain.transitAllowed()) {
|
||||
return new RouteCandidate(
|
||||
point,
|
||||
point.x(),
|
||||
point.z(),
|
||||
0D,
|
||||
Double.POSITIVE_INFINITY,
|
||||
Double.POSITIVE_INFINITY,
|
||||
tangent,
|
||||
true
|
||||
);
|
||||
}
|
||||
double terrainScore = routeTerrainScore(terrain);
|
||||
return new RouteCandidate(
|
||||
new HydrologyPoint(point.x(), terrain.naturalHeight(), point.z()),
|
||||
point.x(),
|
||||
point.z(),
|
||||
0D,
|
||||
terrainScore,
|
||||
terrainScore,
|
||||
tangent,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
RoutePosition routePosition(
|
||||
HydrologyPoint upstream,
|
||||
HydrologyPoint downstream,
|
||||
RouteDirection startTangent,
|
||||
RouteDirection endTangent,
|
||||
double tangentMagnitude,
|
||||
double distance,
|
||||
double progress,
|
||||
double meanderScale
|
||||
) {
|
||||
double tangentX = planner.routePaths.hermiteDerivative(
|
||||
upstream.x(),
|
||||
downstream.x(),
|
||||
startTangent.x() * tangentMagnitude,
|
||||
endTangent.x() * tangentMagnitude,
|
||||
progress
|
||||
);
|
||||
double tangentZ = planner.routePaths.hermiteDerivative(
|
||||
upstream.z(),
|
||||
downstream.z(),
|
||||
startTangent.z() * tangentMagnitude,
|
||||
endTangent.z() * tangentMagnitude,
|
||||
progress
|
||||
);
|
||||
double tangentLength = StrictMath.hypot(tangentX, tangentZ);
|
||||
RouteDirection tangent = tangentLength <= 0D
|
||||
? startTangent
|
||||
: new RouteDirection(tangentX / tangentLength, tangentZ / tangentLength);
|
||||
double baseX = planner.routePaths.hermite(
|
||||
upstream.x(),
|
||||
downstream.x(),
|
||||
startTangent.x() * tangentMagnitude,
|
||||
endTangent.x() * tangentMagnitude,
|
||||
progress
|
||||
);
|
||||
double baseZ = planner.routePaths.hermite(
|
||||
upstream.z(),
|
||||
downstream.z(),
|
||||
startTangent.z() * tangentMagnitude,
|
||||
endTangent.z() * tangentMagnitude,
|
||||
progress
|
||||
);
|
||||
double envelope = StrictMath.pow(StrictMath.sin(StrictMath.PI * progress), 2D);
|
||||
HydrologyPlannerSettings.Meanders meanders = planner.settings.geometry().meanders();
|
||||
double primary = planner.routeWormPrimary.noiseSigned(
|
||||
baseX / meanders.primaryWavelength(),
|
||||
baseZ / meanders.primaryWavelength()
|
||||
);
|
||||
double detail = planner.routeWormDetail.noiseSigned(
|
||||
baseX / meanders.detailWavelength(),
|
||||
baseZ / meanders.detailWavelength()
|
||||
);
|
||||
double worm = distance * meanders.maximumOffsetRatio() * envelope * meanderScale * (
|
||||
primary * meanders.primaryStrength()
|
||||
+ detail * meanders.detailStrength()
|
||||
);
|
||||
double anchorRatio = Math.min(
|
||||
ROUTE_ANCHOR_MAXIMUM_OFFSET_RATIO,
|
||||
meanders.maximumOffsetRatio() * 0.625D
|
||||
);
|
||||
double transverseRatio = planner.settings.routing().refinementSpacing()
|
||||
/ (double) planner.settings.routing().sampleSpacing();
|
||||
double maximumWorm = distance * Math.max(0D, 0.5D - anchorRatio - transverseRatio);
|
||||
worm = Math.max(-maximumWorm, Math.min(maximumWorm, worm));
|
||||
return new RoutePosition(
|
||||
baseX - tangent.z() * worm,
|
||||
baseZ + tangent.x() * worm,
|
||||
baseX,
|
||||
baseZ,
|
||||
tangent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code confines} is the area the edge's upstream end is confined to, or null: candidates outside it
|
||||
* are not offered, so a refined route between two confined lattice nodes stays inside their area.
|
||||
*/
|
||||
static boolean withinConfines(HydrologyTerrainSample terrain, String confines) {
|
||||
return confines == null || confines.equals(terrain.confinesKey());
|
||||
}
|
||||
|
||||
List<RouteCandidate> routeCandidates(
|
||||
long upstreamId,
|
||||
long downstreamId,
|
||||
RoutePosition nominal,
|
||||
double progress,
|
||||
int refinement,
|
||||
int transverseCandidates,
|
||||
String confines
|
||||
) {
|
||||
double searchEnvelope = StrictMath.sin(StrictMath.PI * progress);
|
||||
LinkedHashMap<Long, RouteCandidate> candidates = new LinkedHashMap<>();
|
||||
for (int offsetIndex = -transverseCandidates;
|
||||
offsetIndex <= transverseCandidates;
|
||||
offsetIndex++) {
|
||||
double offset = offsetIndex * refinement * searchEnvelope;
|
||||
int x = (int) StrictMath.round(nominal.x() - nominal.tangent().z() * offset);
|
||||
int z = (int) StrictMath.round(nominal.z() + nominal.tangent().x() * offset);
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasis(x, z);
|
||||
if (terrain == null || !terrain.transitAllowed() || !withinConfines(terrain, confines)) {
|
||||
continue;
|
||||
}
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
double localScore = routeTerrainScore(terrain)
|
||||
+ StrictMath.abs(offset) * 0.18D
|
||||
+ HydrologyHash.unit(HydrologyHash.mix(planner.worldSeed, upstreamId, downstreamId, x, z)) * 1.0E-6D;
|
||||
RouteCandidate candidate = new RouteCandidate(
|
||||
new HydrologyPoint(x, terrain.naturalHeight(), z),
|
||||
x,
|
||||
z,
|
||||
offset,
|
||||
localScore,
|
||||
routeTerrainScore(terrain),
|
||||
nominal.tangent(),
|
||||
true
|
||||
);
|
||||
RouteCandidate current = candidates.get(packed);
|
||||
if (current == null || candidate.localScore() < current.localScore()) {
|
||||
candidates.put(packed, candidate);
|
||||
}
|
||||
}
|
||||
if (!candidates.isEmpty()) {
|
||||
return List.copyOf(candidates.values());
|
||||
}
|
||||
int fallbackX = (int) StrictMath.round(nominal.baseX());
|
||||
int fallbackZ = (int) StrictMath.round(nominal.baseZ());
|
||||
HydrologyTerrainSample fallbackTerrain = planner.sampleLandBasis(fallbackX, fallbackZ);
|
||||
if (fallbackTerrain != null
|
||||
&& fallbackTerrain.transitAllowed()
|
||||
&& withinConfines(fallbackTerrain, confines)) {
|
||||
long packed = RiverFootprint.pack(fallbackX, fallbackZ);
|
||||
double fallbackOffset = (nominal.baseX() - nominal.x()) * -nominal.tangent().z()
|
||||
+ (nominal.baseZ() - nominal.z()) * nominal.tangent().x();
|
||||
double localScore = routeTerrainScore(fallbackTerrain)
|
||||
+ StrictMath.abs(fallbackOffset) * 0.18D
|
||||
+ HydrologyHash.unit(HydrologyHash.mix(
|
||||
planner.worldSeed,
|
||||
upstreamId,
|
||||
downstreamId,
|
||||
fallbackX,
|
||||
fallbackZ
|
||||
)) * 1.0E-6D;
|
||||
RouteCandidate fallback = new RouteCandidate(
|
||||
new HydrologyPoint(fallbackX, fallbackTerrain.naturalHeight(), fallbackZ),
|
||||
fallbackX,
|
||||
fallbackZ,
|
||||
fallbackOffset,
|
||||
localScore,
|
||||
routeTerrainScore(fallbackTerrain),
|
||||
nominal.tangent(),
|
||||
true
|
||||
);
|
||||
RouteCandidate current = candidates.get(packed);
|
||||
if (current == null || fallback.localScore() < current.localScore()) {
|
||||
candidates.put(packed, fallback);
|
||||
}
|
||||
}
|
||||
return List.copyOf(candidates.values());
|
||||
}
|
||||
|
||||
double routeTerrainScore(HydrologyTerrainSample terrain) {
|
||||
return terrain.naturalHeight() * planner.settings.routing().valleyPreference()
|
||||
+ terrain.slope() * planner.settings.routing().slopePenalty()
|
||||
+ terrain.routingCost() * terrain.routingMultiplier();
|
||||
}
|
||||
|
||||
List<HydrologyPoint> solveTerrainRoute(
|
||||
List<List<RouteCandidate>> layers,
|
||||
double targetStepLength
|
||||
) {
|
||||
ArrayList<List<RouteCandidate>> availableLayers = new ArrayList<>(layers);
|
||||
HashSet<TerrainTransitionKey> rejectedTransitions = new HashSet<>();
|
||||
while (true) {
|
||||
RouteCandidate[] route = selectTerrainRoute(
|
||||
availableLayers,
|
||||
targetStepLength,
|
||||
rejectedTransitions
|
||||
);
|
||||
if (route.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
TerrainTransitionKey unsupportedTransition = unsupportedTerrainTransition(route);
|
||||
if (unsupportedTransition != null) {
|
||||
rejectedTransitions.add(unsupportedTransition);
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
return List.of();
|
||||
}
|
||||
return planner.routePaths.smoothTerrainRoute(points);
|
||||
}
|
||||
}
|
||||
|
||||
RouteCandidate[] selectTerrainRoute(
|
||||
List<List<RouteCandidate>> layers,
|
||||
double targetStepLength,
|
||||
Set<TerrainTransitionKey> rejectedTransitions
|
||||
) {
|
||||
int layerCount = layers.size();
|
||||
double[][] costs = new double[layerCount][];
|
||||
int[][] predecessors = new int[layerCount][];
|
||||
costs[0] = new double[]{layers.getFirst().getFirst().localScore()};
|
||||
predecessors[0] = new int[]{-1};
|
||||
for (int layerIndex = 1; layerIndex < layerCount; layerIndex++) {
|
||||
List<RouteCandidate> currentLayer = layers.get(layerIndex);
|
||||
List<RouteCandidate> previousLayer = layers.get(layerIndex - 1);
|
||||
costs[layerIndex] = new double[currentLayer.size()];
|
||||
predecessors[layerIndex] = new int[currentLayer.size()];
|
||||
Arrays.fill(costs[layerIndex], Double.POSITIVE_INFINITY);
|
||||
Arrays.fill(predecessors[layerIndex], -1);
|
||||
for (int currentIndex = 0; currentIndex < currentLayer.size(); currentIndex++) {
|
||||
RouteCandidate current = currentLayer.get(currentIndex);
|
||||
for (int previousIndex = 0; previousIndex < previousLayer.size(); previousIndex++) {
|
||||
if (!Double.isFinite(costs[layerIndex - 1][previousIndex])) {
|
||||
continue;
|
||||
}
|
||||
RouteCandidate previous = previousLayer.get(previousIndex);
|
||||
TerrainTransitionKey transitionKey = new TerrainTransitionKey(
|
||||
previous.point(),
|
||||
current.point()
|
||||
);
|
||||
if (rejectedTransitions.contains(transitionKey)) {
|
||||
continue;
|
||||
}
|
||||
double transition = planner.routePaths.routeTransitionGeometryCost(previous, current, targetStepLength);
|
||||
if (!Double.isFinite(transition)) {
|
||||
continue;
|
||||
}
|
||||
double cost = costs[layerIndex - 1][previousIndex] + current.localScore() + transition;
|
||||
if (cost < costs[layerIndex][currentIndex]) {
|
||||
costs[layerIndex][currentIndex] = cost;
|
||||
predecessors[layerIndex][currentIndex] = previousIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int selectedIndex = 0;
|
||||
double selectedCost = costs[layerCount - 1][0];
|
||||
for (int candidateIndex = 1; candidateIndex < costs[layerCount - 1].length; candidateIndex++) {
|
||||
if (costs[layerCount - 1][candidateIndex] < selectedCost) {
|
||||
selectedIndex = candidateIndex;
|
||||
selectedCost = costs[layerCount - 1][candidateIndex];
|
||||
}
|
||||
}
|
||||
if (!Double.isFinite(selectedCost)) {
|
||||
return new RouteCandidate[0];
|
||||
}
|
||||
RouteCandidate[] route = new RouteCandidate[layerCount];
|
||||
for (int layerIndex = layerCount - 1; layerIndex >= 0; layerIndex--) {
|
||||
route[layerIndex] = layers.get(layerIndex).get(selectedIndex);
|
||||
selectedIndex = predecessors[layerIndex][selectedIndex];
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
TerrainTransitionKey unsupportedTerrainTransition(RouteCandidate[] route) {
|
||||
for (int routeIndex = 0; routeIndex < route.length - 1; routeIndex++) {
|
||||
RouteCandidate start = route[routeIndex];
|
||||
RouteCandidate end = route[routeIndex + 1];
|
||||
if (!planner.routePaths.traversableTerrainTransition(start.point(), end.point())) {
|
||||
return new TerrainTransitionKey(start.point(), end.point());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
record TerrainTransitionKey(HydrologyPoint start, HydrologyPoint end) {
|
||||
}
|
||||
}
|
||||
Loaded 100 of 710 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user