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

+2
View File
@@ -81,3 +81,5 @@ BUILDS/
/docs/ /docs/
/.perf/ /.perf/
/.codegraph/ /.codegraph/
.superpowers/
@@ -2,8 +2,9 @@ package art.arcane.iris.probe;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.hydrology.HydrologyTileCache; import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.world.pregen.PregenHydrology;
import art.arcane.iris.world.pregen.PregenPerformanceProfile; import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
@@ -31,11 +32,16 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
public final class OfflineRegionGenerator { public final class OfflineRegionGenerator {
private static final int CHUNKS_PER_HYDROLOGY_REGION = WindowKey.SIZE >> 4;
private OfflineRegionGenerator() { private OfflineRegionGenerator() {
} }
@@ -97,14 +103,13 @@ public final class OfflineRegionGenerator {
PregenPerformanceProfile.apply(session.engine()); PregenPerformanceProfile.apply(session.engine());
System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize()
+ " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true"); + " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true");
IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime(); try (HydrologyPregeneration pregeneration = PregenHydrology.open(session.engine());
try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null
: hydrology.preparePregeneration(configuration.hydrologyArea());
HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options( HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options(
session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(), session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(),
configuration.noiseSettings(), spigot)); configuration.noiseSettings(), spigot));
RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(), RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(),
RegionGenerationWindow.Policy.STANDALONE)) { RegionGenerationWindow.Policy.STANDALONE)) {
PregenHydrology.prepareAll(pregeneration, configuration.hydrologyRegions());
HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers); HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers);
Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region"); Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region");
Files.createDirectory(regionDirectory); Files.createDirectory(regionDirectory);
@@ -239,14 +244,27 @@ public final class OfflineRegionGenerator {
} }
} }
HydrologyTileCache.PregenerationArea hydrologyArea() { List<long[]> hydrologyRegions() {
int halo = NativeRegionPlan.terrainHalo(); int halo = NativeRegionPlan.terrainHalo();
long minimumX = ((long) chunkX - halo) * 16L; Set<Long> seen = new HashSet<>();
long minimumZ = ((long) chunkZ - halo) * 16L; List<long[]> regions = new ArrayList<>();
long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L; for (int row = 0; row < regionsZ; row++) {
long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L; for (int column = 0; column < regionsX; column++) {
return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L), int regionX = row % 2 == 0 ? column : regionsX - column - 1;
Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ); int minimumX = Math.floorDiv(chunkX + regionX * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
int maximumX = Math.floorDiv(chunkX + (regionX + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
int minimumZ = Math.floorDiv(chunkZ + row * width - halo, CHUNKS_PER_HYDROLOGY_REGION);
int maximumZ = Math.floorDiv(chunkZ + (row + 1) * width - 1 + halo, CHUNKS_PER_HYDROLOGY_REGION);
for (int hydrologyZ = minimumZ; hydrologyZ <= maximumZ; hydrologyZ++) {
for (int hydrologyX = minimumX; hydrologyX <= maximumX; hydrologyX++) {
if (seen.add(((long) hydrologyX << 32) | (hydrologyZ & 0xFFFFFFFFL))) {
regions.add(new long[]{hydrologyX, hydrologyZ});
}
}
}
}
}
return regions;
} }
static Configuration parse(String[] arguments) throws IOException { static Configuration parse(String[] arguments) throws IOException {
@@ -137,7 +137,7 @@ public class HeadlessChunkGeneratorTest {
} }
Files.writeString(pack.resolve("dimensions/main.json"), """ Files.writeString(pack.resolve("dimensions/main.json"), """
{"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256, {"name":"Native columns","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}} "landChance":1,"carvingEnabled":false}
"""); """);
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}"); Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}"); Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
@@ -1,7 +1,6 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex; import art.arcane.iris.structure.nativegen.NativeStructureVolumeIndex;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume; import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
@@ -68,11 +67,10 @@ public final class HeadlessRegionTerrainTest {
OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration( OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration(
workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration, workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration,
"probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export")); "probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export"));
HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea(); List<long[]> regions = export.hydrologyRegions();
assertEquals(-80L, bounds.minimumBlockX()); assertEquals(2, regions.size());
assertEquals(0L, bounds.minimumBlockZ()); assertArrayEquals(new long[]{-1L, 0L}, regions.get(0));
assertEquals(15L, bounds.maximumBlockX()); assertArrayEquals(new long[]{0L, 0L}, regions.get(1));
assertEquals(111L, bounds.maximumBlockZ());
assertEquals(1, result.targetChunks()); assertEquals(1, result.targetChunks());
assertEquals(529, result.chunks().size()); assertEquals(529, result.chunks().size());
long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count(); long terrain = result.chunks().stream().filter(chunk -> chunk.getPersistedStatus() == ChunkStatus.TERRAIN).count();
@@ -189,7 +187,7 @@ public final class HeadlessRegionTerrainTest {
} }
Files.writeString(pack.resolve("dimensions/main.json"), """ Files.writeString(pack.resolve("dimensions/main.json"), """
{"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256, {"name":"Native region","focus":"swamp","regions":["owned"],"fluidHeight":40,"logicalHeight":256,
"landChance":1,"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}} "landChance":1,"carvingEnabled":false}
"""); """);
Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}"); Files.writeString(pack.resolve("regions/owned.json"), "{\"landBiomes\":[\"swamp\"]}");
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}"); Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
+1 -1
View File
@@ -14,7 +14,7 @@ dependencies {
// log4j-core Filter interface, so its constructor reference needs the type resolvable. // log4j-core Filter interface, so its constructor reference needs the type resolvable.
compileOnly(libs.log4j.core) compileOnly(libs.log4j.core)
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(libs.mockito.core)
testImplementation(libs.paper.api) testImplementation(libs.paper.api)
testImplementation(libs.bstats) testImplementation(libs.bstats)
testImplementation(project(':core')) testImplementation(project(':core'))
@@ -31,8 +31,7 @@ import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.placement.StructureReachability; import art.arcane.iris.structure.placement.StructureReachability;
import art.arcane.iris.world.history.GenerationSemanticQueries; import art.arcane.iris.world.history.GenerationSemanticQueries;
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
@@ -196,9 +195,9 @@ public class CommandFind implements DirectorExecutor {
EngineBukkitOps.gotoPOI(e, type, player(), teleport); EngineBukkitOps.gotoPOI(e, type, player(), teleport);
} }
@Director(description = "Find an accepted hydrology feature", sync = true) @Director(description = "Find a hydrology water feature", sync = true)
public void river( public void river(
@Param(description = "Feature type: surface, waterfall, underground, grotto, mouth, or a deep-fluid id", customHandler = HydrologyTypeHandler.class) @Param(description = "Feature type: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet or cave_pool", customHandler = HydrologyTypeHandler.class)
String type, String type,
@Param(description = "Should you be teleported", defaultValue = "true") @Param(description = "Should you be teleported", defaultValue = "true")
boolean teleport boolean teleport
@@ -212,42 +211,32 @@ public class CommandFind implements DirectorExecutor {
} }
return; return;
} }
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime(); HydrologyFeatureType featureType;
HydrologyFeatureQuery query;
try { try {
query = HydrologyFeatureQuery.parse(type); featureType = HydrologyFeatureType.parse(type);
} catch (IllegalArgumentException error) { } catch (IllegalArgumentException error) {
commandSender.sendMessage(C.RED + error.getMessage()); commandSender.sendMessage(C.RED + error.getMessage());
return; return;
} }
String label = featureType.key().replace('_', ' ');
Location origin = target.getLocation(); Location origin = target.getLocation();
World originWorld = origin.getWorld(); World originWorld = origin.getWorld();
int requestedDistance = runtime == null int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
? 8192 commandSender.sendMessage(C.GRAY + GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
J.a(() -> { J.a(() -> {
try { try {
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver( GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
activeEngine, activeEngine,
query, featureType,
origin.getBlockX(), origin.getBlockX(),
origin.getBlockZ(), origin.getBlockZ(),
maximumDistance, maximumDistance
(int visited) -> sendStructureMessage(target, commandSender,
C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...")
).orElse(null); ).orElse(null);
if (feature == null) { if (feature == null) {
sendStructureMessage(target, commandSender, sendStructureMessage(target, commandSender,
C.YELLOW + "No accepted " + type + " hydrology feature found within " C.YELLOW + GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
+ maximumDistance + " blocks.");
return; return;
} }
String label = type + " hydrology feature";
if (!teleport) { if (!teleport) {
sendStructureMessage(target, commandSender, sendStructureMessage(target, commandSender,
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + "."); C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
@@ -259,8 +248,9 @@ public class CommandFind implements DirectorExecutor {
target, originWorld, commandSender, label, destination, true); target, originWorld, commandSender, label, destination, true);
} catch (Throwable error) { } catch (Throwable error) {
sendStructureMessage(target, commandSender, sendStructureMessage(target, commandSender,
C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName()); C.RED + "Could not search for " + label + ": " + error.getClass().getSimpleName()
Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error); + (error.getMessage() == null ? "" : " - " + error.getMessage()));
Iris.reportError("Could not locate hydrology feature '" + featureType.key() + "'.", error);
} }
}); });
} }
@@ -17,8 +17,7 @@ import art.arcane.iris.platform.bukkit.terrain.IrisSampleLimits;
import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier; import art.arcane.iris.platform.bukkit.terrain.IrisSurfaceClassifier;
import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory; import art.arcane.iris.platform.bukkit.terrain.IrisWorldInfoFactory;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
@@ -108,15 +107,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
try { try {
int surface = engine.getHeight(blockX, blockZ); int surface = engine.getHeight(blockX, blockZ);
HydrologyColumnSample hydrology = hydrologySample(engine, blockX, blockZ); WaterRole role = engine.getComplex().getWaterRoleStream().get(blockX, blockZ);
int fluid = (int) Math.round( int fluid = (int) Math.round(
engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ)); engine.getComplex().getWaterHeadStream().get(blockX, blockZ));
InferredType inferredType = null; InferredType inferredType = null;
if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) { if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) {
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ); IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
inferredType = biome == null ? null : biome.getInferredType(); inferredType = biome == null ? null : biome.getInferredType();
} }
return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology); return IrisSurfaceClassifier.classify(surface, fluid, inferredType, role);
} catch (Throwable error) { } catch (Throwable error) {
reportQueryFault("surfaceKind", world, error); reportQueryFault("surfaceKind", world, error);
return IrisSurfaceKind.UNKNOWN; return IrisSurfaceKind.UNKNOWN;
@@ -266,12 +265,12 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
return false; return false;
} }
HydrologyColumnSample hydrology = wantRiver WaterRole role = wantRiver
? hydrologySample(engine, blockX, blockZ) ? engine.getComplex().getWaterRoleStream().get(blockX, blockZ)
: null; : WaterRole.NONE;
int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0; int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0;
int fluid = wantRiver int fluid = wantRiver
? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ)) ? (int) Math.round(engine.getComplex().getWaterHeadStream().get(blockX, blockZ))
: engine.getDimension().getFluidHeight(); : engine.getDimension().getFluidHeight();
boolean needsBiome = wantBiome boolean needsBiome = wantBiome
|| (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)); || (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid));
@@ -281,29 +280,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
surface, surface,
fluid, fluid,
biome == null ? null : biome.getInferredType(), biome == null ? null : biome.getInferredType(),
hydrology role
) )
: IrisSurfaceKind.UNKNOWN; : IrisSurfaceKind.UNKNOWN;
String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null; String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null;
int natural = wantNaturalHeight int natural = wantNaturalHeight
? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight ? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight
: IrisColumnSample.UNAVAILABLE_HEIGHT; : IrisColumnSample.UNAVAILABLE_HEIGHT;
HydrologyColumnLayer surfaceLayer = hydrology == null
? null
: hydrology.primarySurfaceLayer().orElse(null);
boolean riverPresent = surfaceLayer != null;
IrisRiverState riverState = wantRiverState IrisRiverState riverState = wantRiverState
? riverState(hydrology) ? riverState(role)
: IrisRiverState.NONE; : IrisRiverState.NONE;
double riverDistance = wantRiverDistance && riverPresent double riverDistance = wantRiverDistance
? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ) ? riverDistance(role)
: IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE; : IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
int riverFlow = wantRiverFlow && riverPresent int riverFlow = wantRiverFlow
? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ)) ? riverFlow(role)
: IrisColumnSample.UNAVAILABLE_RIVER_FLOW; : IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
int riverWaterSurfaceY = wantRiverWaterSurface int riverWaterSurfaceY = wantRiverWaterSurface && role.wet()
&& surfaceLayer != null
&& surfaceLayer.connectedFluid()
? fluid + minHeight ? fluid + minHeight
: IrisColumnSample.UNAVAILABLE_HEIGHT; : IrisColumnSample.UNAVAILABLE_HEIGHT;
sink.accept(new IrisColumnSample( sink.accept(new IrisColumnSample(
@@ -327,21 +320,25 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService {
} }
} }
static IrisRiverState riverState(HydrologyColumnSample hydrology) { static IrisRiverState riverState(WaterRole role) {
HydrologyColumnLayer layer = hydrology == null return role.wet() ? IrisRiverState.WET : IrisRiverState.NONE;
? null
: hydrology.primarySurfaceLayer().orElse(null);
if (layer == null || !layer.channel()) {
return IrisRiverState.NONE;
}
return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY;
} }
private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) { static double riverDistance(WaterRole role) {
if (engine.getComplex().getHydrologyRuntime() == null) { return switch (role) {
return null; case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
} case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null); case SHORE, POND_SHORE -> 1D;
case BANK, DAM -> 2D;
};
}
static int riverFlow(WaterRole role) {
return switch (role) {
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
case POND, INLET -> 0;
};
} }
private static Optional<String> key(IrisBiome biome) { private static Optional<String> key(IrisBiome biome) {
@@ -1,8 +1,7 @@
package art.arcane.iris.platform.bukkit.terrain; package art.arcane.iris.platform.bukkit.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind; import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
public final class IrisSurfaceClassifier { public final class IrisSurfaceClassifier {
@@ -29,25 +28,16 @@ public final class IrisSurfaceClassifier {
int engineSurfaceHeight, int engineSurfaceHeight,
int engineFluidHeight, int engineFluidHeight,
InferredType inferredType, InferredType inferredType,
HydrologyColumnSample hydrology WaterRole role
) { ) {
if (engineSurfaceHeight <= 0) { if (engineSurfaceHeight <= 0) {
return IrisSurfaceKind.VOID; return IrisSurfaceKind.VOID;
} }
HydrologyColumnLayer layer = hydrology == null return switch (role) {
? null case NONE -> classify(engineSurfaceHeight, engineFluidHeight, inferredType);
: hydrology.primarySurfaceLayer().orElse(null); case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
if (layer != null) { case BANK, DAM -> IrisSurfaceKind.LAND;
if (layer.shore()) { case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
return IrisSurfaceKind.RIVER_SHORE; };
}
if (layer.channel()) {
return layer.connectedFluid() ? IrisSurfaceKind.RIVER : IrisSurfaceKind.DRY_CHANNEL;
}
if (layer.grading()) {
return IrisSurfaceKind.LAND;
}
}
return classify(engineSurfaceHeight, engineFluidHeight, inferredType);
} }
} }
@@ -1,7 +1,7 @@
package art.arcane.iris.command; package art.arcane.iris.command;
import art.arcane.iris.Iris; import art.arcane.iris.Iris;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.localization.IrisLanguage; import art.arcane.iris.localization.IrisLanguage;
@@ -22,6 +22,7 @@ import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
@@ -45,7 +46,7 @@ public class CommandFindHydrologyTest {
@Test @Test
public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() { public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true); fixture.command.river("river", true);
when(fixture.player.getWorld()).thenReturn(mock(World.class)); when(fixture.player.getWorld()).thenReturn(mock(World.class));
fixture.search.get().run(); fixture.search.get().run();
@@ -58,7 +59,7 @@ public class CommandFindHydrologyTest {
@Test @Test
public void disconnectDuringSearchDoesNotLoadTheDestination() { public void disconnectDuringSearchDoesNotLoadTheDestination() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true); fixture.command.river("river", true);
when(fixture.player.isOnline()).thenReturn(false); when(fixture.player.isOnline()).thenReturn(false);
fixture.search.get().run(); fixture.search.get().run();
@@ -69,7 +70,7 @@ public class CommandFindHydrologyTest {
@Test @Test
public void worldChangeWhileLoadingTheDestinationCancelsTeleport() { public void worldChangeWhileLoadingTheDestinationCancelsTeleport() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true); fixture.command.river("river", true);
fixture.search.get().run(); fixture.search.get().run();
when(fixture.player.getWorld()).thenReturn(mock(World.class)); when(fixture.player.getWorld()).thenReturn(mock(World.class));
fixture.chunk.complete(mock(Chunk.class)); fixture.chunk.complete(mock(Chunk.class));
@@ -82,7 +83,7 @@ public class CommandFindHydrologyTest {
@Test @Test
public void successIsReportedOnlyAfterTeleportCompletes() { public void successIsReportedOnlyAfterTeleportCompletes() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true); fixture.command.river("river", true);
fixture.search.get().run(); fixture.search.get().run();
fixture.chunk.complete(mock(Chunk.class)); fixture.chunk.complete(mock(Chunk.class));
@@ -95,7 +96,7 @@ public class CommandFindHydrologyTest {
@Test @Test
public void rejectedTeleportReportsFailureWithoutSuccess() { public void rejectedTeleportReportsFailureWithoutSuccess() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true); fixture.command.river("river", true);
fixture.search.get().run(); fixture.search.get().run();
fixture.chunk.complete(mock(Chunk.class)); fixture.chunk.complete(mock(Chunk.class));
fixture.teleport.complete(false); fixture.teleport.complete(false);
@@ -109,7 +110,7 @@ public class CommandFindHydrologyTest {
public void failedTeleportReportsTheFullCauseWithoutSuccess() { public void failedTeleportReportsTheFullCauseWithoutSuccess() {
try (Fixture fixture = new Fixture()) { try (Fixture fixture = new Fixture()) {
IllegalStateException failure = new IllegalStateException("destination unloaded"); IllegalStateException failure = new IllegalStateException("destination unloaded");
fixture.command.river("surface", true); fixture.command.river("river", true);
fixture.search.get().run(); fixture.search.get().run();
fixture.chunk.complete(mock(Chunk.class)); fixture.chunk.complete(mock(Chunk.class));
fixture.teleport.completeExceptionally(failure); fixture.teleport.completeExceptionally(failure);
@@ -120,6 +121,72 @@ public class CommandFindHydrologyTest {
} }
} }
@Test
public void everyFeatureTypeIsSearchedOffTheCommandThreadAtTheRuntimeRadius() {
for (HydrologyFeatureType type : HydrologyFeatureType.values()) {
try (Fixture fixture = new Fixture()) {
fixture.command.river(type.key(), false);
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(any(), any(), anyInt(), anyInt(), anyInt()), never());
fixture.search.get().run();
fixture.queries.verify(() -> GenerationSemanticQueries.nearestRiver(fixture.engine, type, 0, 0,
GenerationSemanticQueries.RIVER_SEARCH_DISTANCE));
verify(fixture.sender).sendMessage(contains("Found " + type.key().replace('_', ' ') + " at 32, 63, 32."));
}
}
}
@Test
public void unknownTypeIsRejectedWithTheAcceptedTypesBeforeAnySearch() {
try (Fixture fixture = new Fixture()) {
fixture.command.river("surface", true);
verify(fixture.sender).sendMessage(contains("Use one of: stream, river, major_river, cascade, waterfall, mouth, pond, lake, dam, sea_inlet, cave_pool."));
assertNull(fixture.search.get());
}
}
@Test
public void missingCavePoolIsReportedAsNotFoundInGeneratedChunks() {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
.thenReturn(Optional.empty());
fixture.command.river("cave_pool", true);
fixture.search.get().run();
verify(fixture.sender).sendMessage(contains("Searching generated chunks for the nearest cave pool within 7168 blocks"));
verify(fixture.sender).sendMessage(contains("No cave pool found in generated chunks within 7168 blocks."));
}
}
@Test
public void missingFeatureIsReportedAsNotFound() {
try (Fixture fixture = new Fixture()) {
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
.thenReturn(Optional.empty());
fixture.command.river("sea_inlet", true);
fixture.search.get().run();
verify(fixture.sender).sendMessage(contains("No sea inlet found within 7168 blocks."));
fixture.platform.verify(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true)), never());
}
}
@Test
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() {
try (Fixture fixture = new Fixture()) {
IllegalStateException failure = new IllegalStateException("window planner exploded");
fixture.queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(fixture.engine), any(), eq(0), eq(0), anyInt()))
.thenThrow(failure);
fixture.command.river("lake", true);
fixture.search.get().run();
verify(fixture.sender).sendMessage(contains("Could not search for lake: IllegalStateException - window planner exploded"));
verify(fixture.sender, never()).sendMessage(contains("No lake found"));
fixture.logging.verify(() -> Iris.reportError(any(String.class), eq(failure)));
}
}
private static final class Fixture implements AutoCloseable { private static final class Fixture implements AutoCloseable {
private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS); private final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS);
private final Engine engine = mock(Engine.class); private final Engine engine = mock(Engine.class);
@@ -132,7 +199,7 @@ public class CommandFindHydrologyTest {
private final CompletableFuture<Boolean> teleport = new CompletableFuture<>(); private final CompletableFuture<Boolean> teleport = new CompletableFuture<>();
private final MockedStatic<J> scheduler = mockStatic(J.class); private final MockedStatic<J> scheduler = mockStatic(J.class);
private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class); private final MockedStatic<BukkitPlatform> platform = mockStatic(BukkitPlatform.class);
private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class); private final MockedStatic<GenerationSemanticQueries> queries = mockStatic(GenerationSemanticQueries.class, CALLS_REAL_METHODS);
private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported"); private final MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class, invocation -> "teleported");
private final MockedStatic<Iris> logging = mockStatic(Iris.class); private final MockedStatic<Iris> logging = mockStatic(Iris.class);
@@ -160,10 +227,10 @@ public class CommandFindHydrologyTest {
}); });
platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk); platform.when(() -> BukkitPlatform.chunkAtAsync(any(World.class), anyInt(), anyInt(), eq(true))).thenReturn(chunk);
platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport); platform.when(() -> BukkitPlatform.teleportAsync(eq(player), any(Location.class))).thenReturn(teleport);
queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt(), any())) queries.when(() -> GenerationSemanticQueries.nearestRiver(eq(engine), any(), eq(0), eq(0), anyInt()))
.thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult( .thenReturn(Optional.of(new GenerationSemanticQueries.RiverResult(
GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION, GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION,
HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty()))); HydrologyFeatureType.RIVER, 1L, 32, 63, 32, OptionalLong.empty())));
} }
@Override @Override
@@ -20,26 +20,26 @@ import static org.mockito.Mockito.when;
public class CommandObjectFluidHeightTest { public class CommandObjectFluidHeightTest {
@Test @Test
public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() { public void absoluteObjectPlacementShiftsTheColumnWaterHeadFromNegativeMinY() {
World world = mock(World.class); World world = mock(World.class);
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class);
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ProceduralStream<Double> riverHead = mock(ProceduralStream.class); ProceduralStream<Double> waterHead = mock(ProceduralStream.class);
Map<Block, BlockData> future = new HashMap<>(); Map<Block, BlockData> future = new HashMap<>();
when(engine.getMinHeight()).thenReturn(-64); when(engine.getMinHeight()).thenReturn(-64);
when(engine.getDimension()).thenReturn(dimension); when(engine.getDimension()).thenReturn(dimension);
when(engine.getComplex()).thenReturn(complex); when(engine.getComplex()).thenReturn(complex);
when(dimension.getFluidHeight()).thenReturn(127); when(dimension.getFluidHeight()).thenReturn(127);
when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead); when(complex.getWaterHeadStream()).thenReturn(waterHead);
when(riverHead.get(12, -7)).thenReturn(131D); when(waterHead.get(12, -7)).thenReturn(131D);
IObjectPlacer placer = CommandObject.createPlacer(world, future, engine); IObjectPlacer placer = CommandObject.createPlacer(world, future, engine);
assertEquals(63, placer.getFluidHeight()); assertEquals(63, placer.getFluidHeight());
assertEquals(67, placer.getFluidHeight(12, -7)); assertEquals(67, placer.getFluidHeight(12, -7));
verify(riverHead).get(12, -7); verify(waterHead).get(12, -7);
} }
} }
@@ -9,16 +9,18 @@ import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.BiomeEnvironment; import art.arcane.iris.generation.runtime.BiomeEnvironment;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.platform.generation.PlatformChunkGenerator; import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.history.SavedBiomeUnavailableException; import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample; import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef; import art.arcane.iris.api.terrain.IrisColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType; import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.platform.bukkit.plugin.IrisService; import art.arcane.iris.platform.bukkit.plugin.IrisService;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator; import org.bukkit.generator.ChunkGenerator;
@@ -26,6 +28,7 @@ import org.junit.Test;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -112,10 +115,32 @@ public class IrisTerrainSVCTest {
} }
@Test @Test
public void acceptedHydrologyMapsToThePublicDiagnosticStates() { public void waterRolesMapToThePublicRiverFields() {
assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null)); for (WaterRole role : WaterRole.values()) {
assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true))); assertEquals(role.name(), role.wet() ? IrisRiverState.WET : IrisRiverState.NONE, IrisTerrainSVC.riverState(role));
assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false))); double distance = switch (role) {
case NONE -> IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE;
case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> 0D;
case SHORE, POND_SHORE -> 1D;
case BANK, DAM -> 2D;
};
assertEquals(role.name(), distance, IrisTerrainSVC.riverDistance(role), 0D);
int flow = switch (role) {
case NONE, SHORE, POND_SHORE, BANK, DAM -> IrisColumnSample.UNAVAILABLE_RIVER_FLOW;
case CHANNEL, MOUTH, CASCADE, WATERFALL -> 1;
case POND, INLET -> 0;
};
assertEquals(role.name(), flow, IrisTerrainSVC.riverFlow(role));
}
}
@Test
public void columnSamplesReportTheWaterRoleOfEachColumn() throws Exception {
assertEquals(List.of("RIVER", "WET", "0.0", "1", "6"), sampled(WaterRole.CHANNEL, 70));
assertEquals(List.of("RIVER", "WET", "0.0", "0", "6"), sampled(WaterRole.POND, 70));
assertEquals(List.of("RIVER_SHORE", "NONE", "1.0", "-1", "unavailable"), sampled(WaterRole.SHORE, 50));
assertEquals(List.of("LAND", "NONE", "2.0", "-1", "unavailable"), sampled(WaterRole.BANK, 50));
assertEquals(List.of("LAND", "NONE", "NaN", "-1", "unavailable"), sampled(WaterRole.NONE, 50));
} }
@Test @Test
@@ -219,6 +244,33 @@ public class IrisTerrainSVCTest {
} }
} }
private static List<String> sampled(WaterRole role, int head) throws Exception {
Engine engine = mock(Engine.class);
IrisComplex complex = mock(IrisComplex.class);
IrisBiome biome = mock(IrisBiome.class);
when(biome.getInferredType()).thenReturn(InferredType.LAND);
when(engine.getComplex()).thenReturn(complex);
when(engine.getDimension()).thenReturn(mock(IrisDimension.class));
when(engine.getHeight(0, 0)).thenReturn(60);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getSurfaceBiome(0, 0)).thenReturn(biome);
when(complex.getWaterRoleStream()).thenReturn(ProceduralStream.of((x, z) -> role, Interpolated.of(value -> 0D, value -> role)));
when(complex.getWaterHeadStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> (double) head));
IrisColumnQuery query = IrisColumnQuery.rect(0, 0, 0, 0, 1, EnumSet.of(IrisColumnField.SURFACE_KIND,
IrisColumnField.RIVER_STATE, IrisColumnField.RIVER_DISTANCE, IrisColumnField.RIVER_FLOW,
IrisColumnField.RIVER_WATER_SURFACE_Y));
List<IrisColumnSample> samples = new ArrayList<>();
try (MockedStatic<IrisSettings> settings = mockStatic(IrisSettings.class)) {
settings.when(IrisSettings::get).thenReturn(new IrisSettings());
assertTrue(enabledService().sampleColumns(world(engine), query, samples::add));
}
assertEquals(1, samples.size());
IrisColumnSample sample = samples.get(0);
return List.of(sample.surfaceKind().name(), sample.riverState().name(), String.valueOf(sample.riverDistance()),
String.valueOf(sample.riverFlow()),
sample.riverWaterSurfaceY() == IrisColumnSample.UNAVAILABLE_HEIGHT ? "unavailable" : String.valueOf(sample.riverWaterSurfaceY()));
}
private static World world(Engine engine) { private static World world(Engine engine) {
World world = mock(World.class); World world = mock(World.class);
ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class)); ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class));
@@ -235,41 +287,4 @@ public class IrisTerrainSVCTest {
((AtomicBoolean) enabled.get(service)).set(true); ((AtomicBoolean) enabled.get(service)).set(true);
return service; return service;
} }
private static HydrologyColumnSample river(boolean connectedFluid) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
1L,
HydrologyFeatureType.SURFACE_POOL,
2L,
3L,
0,
63,
0,
1,
0,
false
);
HydrologyColumnLayer layer = new HydrologyColumnLayer(
feature,
60,
63,
63,
true,
false,
false,
connectedFluid,
false,
false,
true,
connectedFluid,
false,
"water",
"river",
"mouth",
"shore",
"dry",
"cave"
);
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
}
} }
@@ -1,15 +1,10 @@
package art.arcane.iris.platform.bukkit.terrain; package art.arcane.iris.platform.bukkit.terrain;
import art.arcane.iris.api.terrain.IrisSurfaceKind; import art.arcane.iris.api.terrain.IrisSurfaceKind;
import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
import art.arcane.iris.generation.hydrology.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
import org.junit.Test; import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -69,98 +64,28 @@ public class IrisSurfaceClassifierTest {
} }
@Test @Test
public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() { public void waterRolesOverrideGenericOceanAndLandKinds() {
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify( for (WaterRole role : WaterRole.values()) {
60, IrisSurfaceKind expected = switch (role) {
63, case NONE -> IrisSurfaceKind.OCEAN;
InferredType.SEA, case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE;
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true) case BANK, DAM -> IrisSurfaceKind.LAND;
)); case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER;
assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify( };
60, assertEquals(role.name(), expected, IrisSurfaceClassifier.classify(60, 63, InferredType.SEA, role));
63, }
InferredType.SEA,
hydrology(HydrologyFeatureType.MOUTH, true, false, true)
));
assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify(
64,
63,
InferredType.SHORE,
hydrology(HydrologyFeatureType.SURFACE_POOL, false, true, false)
));
assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify(
60,
60,
InferredType.LAND,
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, false)
));
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(
60,
60,
InferredType.LAND,
hydrology(HydrologyFeatureType.SURFACE_POOL, false, false, false)
));
} }
@Test @Test
public void voidClassificationWinsOverAcceptedRiverGeometry() { public void voidClassificationWinsOverWaterRoles() {
assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify( for (WaterRole role : WaterRole.values()) {
0, assertEquals(role.name(), IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, 63, InferredType.LAND, role));
63, }
InferredType.LAND,
hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true)
));
} }
@Test @Test
public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() { public void columnsWithoutAWaterRoleKeepTheGenericKinds() {
assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify( assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(64, 63, InferredType.LAND, WaterRole.NONE));
64, assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(64, 63, InferredType.SHORE, WaterRole.NONE));
63,
InferredType.LAND,
null
));
}
private static HydrologyColumnSample hydrology(
HydrologyFeatureType type,
boolean channel,
boolean shore,
boolean connectedFluid
) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(
1L,
type,
2L,
3L,
0,
63,
0,
1,
0,
false
);
HydrologyColumnLayer layer = new HydrologyColumnLayer(
feature,
60,
63,
63,
channel,
shore,
!channel && !shore,
connectedFluid,
false,
false,
true,
connectedFluid && channel,
false,
"water",
"river",
"mouth",
"shore",
"dry",
"cave"
);
return new HydrologyColumnSample(0, 0, 70, 63, false, "parent", List.of(layer));
} }
} }
+3 -1
View File
@@ -153,7 +153,7 @@ dependencies {
String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation' String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation'
add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}") add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}")
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(libs.mockito.core)
testImplementation(testFixtures('art.arcane:core')) testImplementation(testFixtures('art.arcane:core'))
testImplementation(libs.spigot) { testImplementation(libs.spigot) {
transitive = false transitive = false
@@ -236,6 +236,8 @@ tasks.named('compileJava', JavaCompile).configure {
options.release.set((int) irisModTarget.java) options.release.set((int) irisModTarget.java)
} }
apply from: file('../../gradle/mockito-agent.gradle')
tasks.named('test').configure { tasks.named('test').configure {
} }
+3 -1
View File
@@ -178,7 +178,7 @@ dependencies {
? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}") ? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}")
: minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}")) : minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}"))
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(libs.mockito.core)
testImplementation(testFixtures('art.arcane:core')) testImplementation(testFixtures('art.arcane:core'))
testImplementation(libs.spigot) { testImplementation(libs.spigot) {
transitive = false transitive = false
@@ -272,6 +272,8 @@ tasks.named('compileJava', JavaCompile).configure {
options.release.set((int) irisModTarget.java) options.release.set((int) irisModTarget.java)
} }
apply from: file('../../gradle/mockito-agent.gradle')
tasks.named('test').configure { tasks.named('test').configure {
} }
@@ -25,7 +25,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.iris.structure.placement.IrisStructureLocator;
import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy;
import art.arcane.iris.structure.placement.StructureReachability; import art.arcane.iris.structure.placement.StructureReachability;
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.iris.world.history.GenerationFindCatalog; import art.arcane.iris.world.history.GenerationFindCatalog;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
@@ -61,7 +61,7 @@ final class ModdedCommandSuggestions {
static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder); static final SuggestionProvider<NativeCommandSource> OBJECT_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder); static final SuggestionProvider<NativeCommandSource> STRUCTURE_KEYS = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder); static final SuggestionProvider<NativeCommandSource> POI_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder);
static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder); static final SuggestionProvider<NativeCommandSource> HYDROLOGY_TYPES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(HydrologyFeatureType.keys(), builder);
static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder); static final SuggestionProvider<NativeCommandSource> PACK_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestPackNames(context, builder);
static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder); static final SuggestionProvider<NativeCommandSource> DIMENSION_NAMES = (CommandContext<NativeCommandSource> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
@@ -87,24 +87,6 @@ final class ModdedCommandSuggestions {
return builder.buildFuture(); return builder.buildFuture();
} }
private static CompletableFuture<Suggestions> suggestHydrologyTypes(
CommandContext<NativeCommandSource> context,
SuggestionsBuilder builder
) {
ModdedCommandFeedback.tab(context.getSource());
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().world());
if (engine != null && engine.getComplex().getHydrologyRuntime() != null) {
return NativeCommandRegistration.suggest(
engine.getComplex().getHydrologyRuntime().featureQueryKeys(), builder);
}
} catch (Throwable error) {
warnTabFailure("hydrology types", context.getSource(), error);
}
return NativeCommandRegistration.suggest(
HydrologyFeatureQuery.suggestions(List.of()), builder);
}
static Set<String> reachableBiomeKeys(Engine engine) { static Set<String> reachableBiomeKeys(Engine engine) {
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine)); return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
} }
@@ -39,8 +39,7 @@ import art.arcane.iris.structure.placement.StructureReachability;
import art.arcane.iris.generation.runtime.WrongEngineBroException; import art.arcane.iris.generation.runtime.WrongEngineBroException;
import art.arcane.iris.world.history.GenerationSemanticQueries; import art.arcane.iris.world.history.GenerationSemanticQueries;
import art.arcane.iris.world.history.GenerationFindCatalog; import art.arcane.iris.world.history.GenerationFindCatalog;
import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
@@ -143,39 +142,31 @@ final class ModdedLocateCommands {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris."); IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
return 0; return 0;
} }
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime(); HydrologyFeatureType featureType;
HydrologyFeatureQuery query;
try { try {
query = HydrologyFeatureQuery.parse(type); featureType = HydrologyFeatureType.parse(type);
} catch (IllegalArgumentException error) { } catch (IllegalArgumentException error) {
IrisModdedCommands.fail(source, error.getMessage()); IrisModdedCommands.fail(source, error.getMessage());
return 0; return 0;
} }
String label = featureType.key().replace('_', ' ');
int originX = player.blockX(); int originX = player.blockX();
int originZ = player.blockZ(); int originZ = player.blockZ();
int requestedDistance = runtime == null int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
NativeModdedServer server = source.server(); NativeModdedServer server = source.server();
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "..."); IrisModdedCommands.ok(source, GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance));
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver( MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
engine, query, originX, originZ, maximumDistance, engine, featureType, originX, originZ, maximumDistance).orElse(null))
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> { .whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
if (error != null) { if (error != null) {
ModdedIrisLog.error("Hydrology locate failed for {}", type, error); Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
IrisModdedCommands.fail(source, ModdedIrisLog.error("Hydrology locate failed for {}", featureType.key(), cause);
"Could not locate " + type + " hydrology: " + error.getClass().getSimpleName()); IrisModdedCommands.fail(source, "Could not search for " + label + ": " + cause.getClass().getSimpleName()
+ (cause.getMessage() == null ? "" : " - " + cause.getMessage()));
return; return;
} }
if (feature == null) { if (feature == null) {
IrisModdedCommands.fail(source, IrisModdedCommands.fail(source, GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance));
"No accepted " + type + " hydrology feature found within "
+ maximumDistance + " blocks.");
return; return;
} }
teleportToStructure( teleportToStructure(
@@ -185,7 +176,7 @@ final class ModdedLocateCommands {
feature.x(), feature.x(),
feature.y() + 2, feature.y() + 2,
feature.z(), feature.z(),
type + " hydrology feature" label
); );
})); }));
return 1; return 1;
@@ -23,6 +23,7 @@ import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime;
import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad; import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.MantleHeapPressure; import art.arcane.iris.world.pregen.MantleHeapPressure;
import art.arcane.iris.world.pregen.PregenHydrology;
import art.arcane.iris.world.pregen.PregenListener; import art.arcane.iris.world.pregen.PregenListener;
import art.arcane.iris.world.pregen.PregenMantleBackpressure; import art.arcane.iris.world.pregen.PregenMantleBackpressure;
import art.arcane.iris.world.pregen.PregeneratorMethod; import art.arcane.iris.world.pregen.PregeneratorMethod;
@@ -30,6 +31,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.ModdedGenPool; import art.arcane.iris.modded.ModdedGenPool;
import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.Mantle;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -71,6 +73,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
private final PregenMantleBackpressure backpressure; private final PregenMantleBackpressure backpressure;
private final PauseWhenEmptyGuard pauseGuard; private final PauseWhenEmptyGuard pauseGuard;
private final LongSupplier clock; private final LongSupplier clock;
private final AtomicBoolean closing = new AtomicBoolean();
private final PregenHydrology hydrology;
public ModdedPregenMethod(Configuration configuration) { public ModdedPregenMethod(Configuration configuration) {
this(configuration, System::nanoTime); this(configuration, System::nanoTime);
@@ -80,6 +84,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
this.clock = Objects.requireNonNull(clock); this.clock = Objects.requireNonNull(clock);
runtime = configuration.runtime(); runtime = configuration.runtime();
engine = configuration.engine(); engine = configuration.engine();
hydrology = new PregenHydrology(() -> engine);
sync = configuration.sync(); sync = configuration.sync();
pauseGuard = new PauseWhenEmptyGuard(runtime); pauseGuard = new PauseWhenEmptyGuard(runtime);
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
@@ -121,6 +126,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
@Override @Override
public void close() { public void close() {
closing.set(true);
hydrology.close();
try { try {
awaitPendingChunks(); awaitPendingChunks();
ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}", ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}",
@@ -341,6 +348,13 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
return false; return false;
} }
@Override
public void onRegionsAhead(List<long[]> regions) {
if (!closing.get()) {
hydrology.prepare(regions);
}
}
@Override @Override
public String getMethod(int x, int z) { public String getMethod(int x, int z) {
return "Modded"; return "Modded";
@@ -0,0 +1,149 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.history.GenerationSemanticQueries;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeStructureQueries;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedLocateRiverTest {
private static final int DISTANCE = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE;
@Test
public void unknownTypeFailsBeforeSearching() {
Fixture fixture = new Fixture();
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
assertEquals(0, ModdedLocateCommands.gotoRiver(fixture.source, "ocean_trench"));
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()));
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), never());
verify(fixture.server, never()).execute(any());
verify(fixture.runtime, never()).nearestFeature(anyInt(), anyInt(), any(), anyInt(), any());
}
}
@Test
public void missingFeatureReportsNotFound() throws Exception {
Fixture fixture = new Fixture();
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any())).thenReturn(Optional.empty());
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "river"));
fixture.finishOnServerThread();
commands.verify(() -> IrisModdedCommands.fail(fixture.source, "No river found within " + DISTANCE + " blocks."));
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
}
}
@Test
public void plannerFailureIsReportedAsAnErrorNotAsNotFound() throws Exception {
Fixture fixture = new Fixture();
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
.thenThrow(new IllegalStateException("window planner exploded"));
try (MockedStatic<IrisModdedCommands> commands = fixture.commands()) {
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "major_river"));
fixture.finishOnServerThread();
commands.verify(() -> IrisModdedCommands.fail(fixture.source,
"Could not search for major river: IllegalStateException - window planner exploded"));
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), eq("No major river found within " + DISTANCE + " blocks.")),
never());
verify(fixture.player, never()).teleport(any(), anyDouble(), anyDouble(), anyDouble());
}
}
@Test
public void foundFeatureTeleportsAboveItFromThePlayersPosition() throws Exception {
Fixture fixture = new Fixture();
when(fixture.runtime.nearestFeature(anyInt(), anyInt(), any(), anyInt(), any()))
.thenReturn(Optional.of(new HydrologyFeatureRef(9L, HydrologyFeatureType.WATERFALL, 400, 130, -250)));
when(fixture.player.teleport(fixture.world, 400.5D, 68D, -249.5D)).thenReturn(true);
try (MockedStatic<IrisModdedCommands> commands = fixture.commands();
MockedConstruction<NativeStructureQueries> queries = mockConstruction(NativeStructureQueries.class)) {
assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "waterfall"));
fixture.finishOnServerThread();
ArgumentCaptor<HydrologyFeatureQuery> query = ArgumentCaptor.forClass(HydrologyFeatureQuery.class);
verify(fixture.runtime).nearestFeature(eq(120), eq(-75), query.capture(), eq(DISTANCE), any());
assertEquals(HydrologyFeatureQuery.of(HydrologyFeatureType.WATERFALL), query.getValue());
verify(queries.constructed().getFirst()).loadChunk(400 >> 4, -250 >> 4);
verify(fixture.player).teleport(fixture.world, 400.5D, 68D, -249.5D);
commands.verify(() -> IrisModdedCommands.ok(eq(fixture.source), anyString()), times(2));
commands.verify(() -> IrisModdedCommands.fail(eq(fixture.source), anyString()), never());
}
}
private static final class Fixture {
private final NativeCommandSource source = mock(NativeCommandSource.class);
private final NativeWorld world = mock(NativeWorld.class);
private final NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
private final NativeModdedServer server = mock(NativeModdedServer.class);
private final Engine engine = mock(Engine.class);
private final HydrologyRuntime runtime = mock(HydrologyRuntime.class);
private final CompletableFuture<Runnable> completion = new CompletableFuture<>();
private Fixture() {
IrisComplex complex = mock(IrisComplex.class);
IrisDimension dimension = mock(IrisDimension.class);
when(source.player()).thenReturn(player);
when(source.world()).thenReturn(world);
when(source.server()).thenReturn(server);
when(player.blockX()).thenReturn(120);
when(player.blockZ()).thenReturn(-75);
when(player.connected()).thenReturn(true);
when(player.inWorld(world)).thenReturn(true);
when(world.minHeight()).thenReturn(-64);
when(world.maxHeight()).thenReturn(320);
when(engine.getComplex()).thenReturn(complex);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getMinHeight()).thenReturn(-64);
when(complex.getHydrologyRuntime()).thenReturn(runtime);
when(runtime.enabled()).thenReturn(true);
doAnswer(invocation -> {
completion.complete(invocation.getArgument(0));
return null;
}).when(server).execute(any());
}
private MockedStatic<IrisModdedCommands> commands() {
MockedStatic<IrisModdedCommands> commands = mockStatic(IrisModdedCommands.class);
commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine);
return commands;
}
private void finishOnServerThread() throws Exception {
completion.get(10L, TimeUnit.SECONDS).run();
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.modded.command; package art.arcane.iris.modded.command;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.modded.ModdedGenPool; import art.arcane.iris.modded.ModdedGenPool;
import art.arcane.iris.world.pregen.MantleHeapPressure; import art.arcane.iris.world.pregen.MantleHeapPressure;
@@ -11,8 +12,10 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -31,9 +34,11 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@@ -89,6 +94,25 @@ public class ModdedPregenMethodLifecycleTest {
verify(runtime, never()).execute(any()); verify(runtime, never()).execute(any());
} }
@Test
public void closeWithdrawsQueuedHydrologyPrefetchAndRefusesLaterAnnouncements() {
HydrologyPregeneration epoch = mock(HydrologyPregeneration.class);
when(engine.getComplex().getHydrologyRuntime().openPregeneration()).thenReturn(epoch);
when(runtime.isServerThread()).thenReturn(true);
assertTrue(method.deferFinalSaveToServerThread());
List<long[]> ahead = List.of(new long[]{0L, 0L}, new long[]{1L, 0L});
method.onRegionsAhead(ahead);
method.close();
method.onRegionsAhead(ahead);
InOrder order = inOrder(epoch);
order.verify(epoch).prepare(ahead);
order.verify(epoch).close();
verify(epoch, times(1)).prepare(anyList());
method.completeDeferredFinalSave();
}
@Test @Test
public void asynchronousCallerCannotClaimTheShutdownSave() { public void asynchronousCallerCannotClaimTheShutdownSave() {
assertFalse(method.deferFinalSaveToServerThread()); assertFalse(method.deferFinalSaveToServerThread());
+3 -1
View File
@@ -141,7 +141,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath,
dependencies { dependencies {
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(libs.mockito.core)
testImplementation(testFixtures('art.arcane:core')) testImplementation(testFixtures('art.arcane:core'))
testImplementation(libs.spigot) { testImplementation(libs.spigot) {
transitive = false transitive = false
@@ -261,6 +261,8 @@ tasks.named('compileJava', JavaCompile).configure {
options.release.set((int) irisModTarget.java) options.release.set((int) irisModTarget.java)
} }
apply from: file('../../gradle/mockito-agent.gradle')
tasks.named('test').configure { tasks.named('test').configure {
useJUnitPlatform() useJUnitPlatform()
classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output) classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output)
+4 -1
View File
@@ -127,7 +127,7 @@ nmsBindings.each { String key ->
compileOnly(rootProject.libs.annotations) compileOnly(rootProject.libs.annotations)
compileOnly(rootProject.libs.byteBuddy.core) compileOnly(rootProject.libs.byteBuddy.core)
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(rootProject.libs.mockito.core)
if (key == 'v26_3_R1') { if (key == 'v26_3_R1') {
testImplementation(project(':probe')) testImplementation(project(':probe'))
} }
@@ -586,11 +586,14 @@ allprojects {
layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}")) layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}"))
} }
apply from: rootProject.file('gradle/mockito-agent.gradle')
tasks.withType(Test).configureEach { tasks.withType(Test).configureEach {
int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks) int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks)
: project.path == ':probe' ? Math.min(4, testForks) : testForks : project.path == ':probe' ? Math.min(4, testForks) : testForks
maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks) maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks)
jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2') jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2')
timeout = java.time.Duration.ofMinutes(60)
} }
java { java {
+4 -3
View File
@@ -163,11 +163,11 @@ dependencies {
slim(libs.jaxen) slim(libs.jaxen)
testFixturesImplementation('junit:junit:4.13.2') testFixturesImplementation('junit:junit:4.13.2')
testFixturesImplementation('org.mockito:mockito-core:5.23.0') testFixturesImplementation(libs.mockito.core)
testFixturesCompileOnly(libs.paper.api) testFixturesCompileOnly(libs.paper.api)
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation(libs.mockito.core)
testImplementation(libs.paper.api) testImplementation(libs.paper.api)
testImplementation(libs.log4j.core) testImplementation(libs.log4j.core)
testRuntimeOnly(libs.paper.api) testRuntimeOnly(libs.paper.api)
@@ -177,7 +177,8 @@ dependencies {
} }
tasks.named('test').configure { tasks.named('test').configure {
maxHeapSize = '1g' timeout = java.time.Duration.ofMinutes(60)
maxHeapSize = '2g'
jvmArgs('-XX:+ExitOnOutOfMemoryError') jvmArgs('-XX:+ExitOnOutOfMemoryError')
systemProperty('iris.generation.counters', 'true') systemProperty('iris.generation.counters', 'true')
} }
@@ -1,25 +1,14 @@
package art.arcane.iris.command.specialhandlers; 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.command.DirectorParameterHandler;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; import art.arcane.volmlib.util.director.exceptions.DirectorParsingException;
import java.util.List;
public final class HydrologyTypeHandler implements DirectorParameterHandler<String> { public final class HydrologyTypeHandler implements DirectorParameterHandler<String> {
@Override @Override
public KList<String> getPossibilities() { public KList<String> getPossibilities() {
Engine activeEngine = engine(); return new KList<>(HydrologyFeatureType.keys());
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());
} }
@Override @Override
@@ -29,16 +18,11 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
@Override @Override
public String parse(String input, boolean force) throws DirectorParsingException { public String parse(String input, boolean force) throws DirectorParsingException {
String normalized = input == null ? "" : input.trim(); try {
if (normalized.isEmpty()) { return HydrologyFeatureType.parse(input).key();
throw new DirectorParsingException("Hydrology feature type must not be blank."); } catch (IllegalArgumentException error) {
throw new DirectorParsingException(error.getMessage());
} }
for (String possibility : getPossibilities(normalized)) {
if (possibility.equalsIgnoreCase(normalized)) {
return possibility;
}
}
return normalized;
} }
@Override @Override
@@ -48,6 +32,6 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler<Stri
@Override @Override
public String getRandomDefault() { 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.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreBands; import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.IrisProceduralObjects; 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.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle; import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
@@ -161,8 +161,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
private int lockLayersMax = 7; private int lockLayersMax = 7;
@Description("Profile-driven 3D cave configuration") @Description("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile(); private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Description("Biome-level river placement, routing, profile, biome, and geometry policy. Omit to inherit region and dimension settings.") @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 IrisRiverPolicy riverPolicy = null; private IrisHydrologyPolicy hydrologyPolicy = null;
@MinNumber(1) @MinNumber(1)
@MaxNumber(512) @MaxNumber(512)
@Description("The rarity of this biome (integer)") @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.spi.IrisPlatforms;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.EngineMetrics; 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.FloatingIslandBoundarySampler;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot; import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
@@ -25,6 +25,7 @@ public class ChunkContext {
private FloatingBiomeOverlay floatingBiomes; private FloatingBiomeOverlay floatingBiomes;
private CaveTerrainSnapshot caveTerrain; private CaveTerrainSnapshot caveTerrain;
private TransitionTerrainColumn[] transitionTerrain; private TransitionTerrainColumn[] transitionTerrain;
private volatile ChunkWaterRaster waterRaster;
private final int x; private final int x;
private final int z; private final int z;
private final IrisComplex complex; private final IrisComplex complex;
@@ -89,10 +90,6 @@ public class ChunkContext {
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan; PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
boolean capturePrefillMetric = metrics != null; boolean capturePrefillMetric = metrics != null;
long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L; 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())) { if (shouldPrefillAsync(resolvedPlan, complex.isPlatformMainThread())) {
prefillAsync(resolvedPlan); prefillAsync(resolvedPlan);
} else { } else {
@@ -115,6 +112,9 @@ public class ChunkContext {
} }
private void prefillSurface(PrefillPlan prefillPlan) { private void prefillSurface(PrefillPlan prefillPlan) {
if (prefillPlan.height || prefillPlan.biome) {
waterRaster();
}
if (prefillPlan.height) { if (prefillPlan.height) {
height.fillRounded(roundedHeight); height.fillRounded(roundedHeight);
} }
@@ -209,6 +209,18 @@ public class ChunkContext {
return complex; 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() { public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() {
return floatingIslandBoundarySampler; return floatingIslandBoundarySampler;
} }
@@ -542,7 +542,7 @@ final class DecoratorCore {
return stack; return stack;
} }
private static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) { static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) {
return decorator.isForcePlace() return decorator.isForcePlace()
|| (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface)) || (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface))
&& (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(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.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; 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.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk; 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) { private static boolean plannedWater(Engine engine, int x, int y, int z) {
IrisComplex complex = engine.getComplex(); 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 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) { if (y <= terrainHeight || y > fluidHeight) {
return false; return false;
} }
if (complex.getWaterRoleStream().get(x, z).wet()) {
return supportsAdjacent(complex.resolveWaterFluid(x, z));
}
IrisBiome biome = complex.getTrueBiomeStream().get(x, z); IrisBiome biome = complex.getTrueBiomeStream().get(x, z);
KList<NativeBlockState> layers = biome.generateSeaLayers(x, z, KList<NativeBlockState> layers = biome.generateSeaLayers(x, z,
new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData()); new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData());
int depth = fluidHeight - y; 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;
}
}
Loaded 100 of 710 files, more files were not shown because too many files have changed in this diff. Show more