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:
Brian Neumann-Fopiano committed 2026-10-25 15:28:26 -04:00
1 parent 2d211cf106
commit db0f98d103
710 files changed
+41791 -91883

No files matched your search

+4 -3
View File
@@ -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')
}
@@ -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;
};
}
}
@@ -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);
}
@@ -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"));
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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
) {
}
@@ -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
}
@@ -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;
}
@@ -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);
}
}
@@ -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
) {
}
@@ -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();
}
}
}
@@ -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"));
}
}
}
@@ -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");
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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;
};
}
}
@@ -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();
}
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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.");
}
}
}
}
@@ -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) {
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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) {
}
}
@@ -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) {
}
}
@@ -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) {
}
}
@@ -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