diff --git a/.gitignore b/.gitignore index e6a873c31..1c5e411bd 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,5 @@ BUILDS/ /docs/ /.perf/ /.codegraph/ + +.superpowers/ diff --git a/adapters/bukkit/nms/v26_3_R1/src/offline/java/art/arcane/iris/probe/OfflineRegionGenerator.java b/adapters/bukkit/nms/v26_3_R1/src/offline/java/art/arcane/iris/probe/OfflineRegionGenerator.java index 488ddb781..f786eed97 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/offline/java/art/arcane/iris/probe/OfflineRegionGenerator.java +++ b/adapters/bukkit/nms/v26_3_R1/src/offline/java/art/arcane/iris/probe/OfflineRegionGenerator.java @@ -2,8 +2,9 @@ package art.arcane.iris.probe; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.generation.concurrent.MultiBurst; -import art.arcane.iris.generation.hydrology.HydrologyTileCache; -import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; +import art.arcane.iris.generation.hydrology.engine.HydrologyPregeneration; +import art.arcane.iris.generation.hydrology.plan.WindowKey; +import art.arcane.iris.world.pregen.PregenHydrology; import art.arcane.iris.world.pregen.PregenPerformanceProfile; import net.minecraft.core.registries.Registries; @@ -31,11 +32,16 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.HashSet; import java.util.HexFormat; import java.util.List; import java.util.Locale; +import java.util.Set; public final class OfflineRegionGenerator { + private static final int CHUNKS_PER_HYDROLOGY_REGION = WindowKey.SIZE >> 4; + private OfflineRegionGenerator() { } @@ -97,14 +103,13 @@ public final class OfflineRegionGenerator { PregenPerformanceProfile.apply(session.engine()); System.out.println("generationProfile=pregen noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " burstParallelism=" + MultiBurst.burst.parallelism() + " multicore=true"); - IrisHydrologyRuntime hydrology = session.engine().getComplex().getHydrologyRuntime(); - try (HydrologyTileCache.PregenerationScope hydrologyScope = hydrology == null ? null - : hydrology.preparePregeneration(configuration.hydrologyArea()); + try (HydrologyPregeneration pregeneration = PregenHydrology.open(session.engine()); HeadlessTerrainContext context = HeadlessTerrainContext.create(new HeadlessTerrainContext.Options( session.engine(), runtime, staged.resolve("templates"), configuration.levelKey(), configuration.noiseSettings(), spigot)); RegionGenerationWindow workers = new RegionGenerationWindow(configuration.parallelism(), RegionGenerationWindow.Policy.STANDALONE)) { + PregenHydrology.prepareAll(pregeneration, configuration.hydrologyRegions()); HeadlessRegionTerrain.Session rolling = new HeadlessRegionTerrain.Session(workers); Path regionDirectory = session.engine().getTarget().getWorld().worldFolder().toPath().resolve("region"); Files.createDirectory(regionDirectory); @@ -239,14 +244,27 @@ public final class OfflineRegionGenerator { } } - HydrologyTileCache.PregenerationArea hydrologyArea() { + List hydrologyRegions() { int halo = NativeRegionPlan.terrainHalo(); - long minimumX = ((long) chunkX - halo) * 16L; - long minimumZ = ((long) chunkZ - halo) * 16L; - long maximumX = ((long) chunkX + (long) width * regionsX + halo) * 16L - 1L; - long maximumZ = ((long) chunkZ + (long) width * regionsZ + halo) * 16L - 1L; - return new HydrologyTileCache.PregenerationArea(Math.toIntExact((minimumX + maximumX) / 2L), - Math.toIntExact((minimumZ + maximumZ) / 2L), minimumX, minimumZ, maximumX, maximumZ); + Set seen = new HashSet<>(); + List regions = new ArrayList<>(); + for (int row = 0; row < regionsZ; row++) { + for (int column = 0; column < regionsX; column++) { + int regionX = row % 2 == 0 ? column : regionsX - column - 1; + 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 { diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java index 038279345..d4034db19 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java @@ -137,7 +137,7 @@ public class HeadlessChunkGeneratorTest { } Files.writeString(pack.resolve("dimensions/main.json"), """ {"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("generators/flat.json"), "{\"composite\":[]}"); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java index ea155b6b9..9994a75a5 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java @@ -1,7 +1,6 @@ package art.arcane.iris.probe; 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.util.collection.KList; import net.minecraft.nbt.CompoundTag; @@ -68,11 +67,10 @@ public final class HeadlessRegionTerrainTest { OfflineRegionGenerator.Configuration export = new OfflineRegionGenerator.Configuration( workspace.pack(), "main", 69420L, -3, 2, 1, 2, 3, 4, configuration, "probe", Level.OVERWORLD, NoiseGeneratorSettings.OVERWORLD, root.resolve("export")); - HydrologyTileCache.PregenerationArea bounds = export.hydrologyArea(); - assertEquals(-80L, bounds.minimumBlockX()); - assertEquals(0L, bounds.minimumBlockZ()); - assertEquals(15L, bounds.maximumBlockX()); - assertEquals(111L, bounds.maximumBlockZ()); + List regions = export.hydrologyRegions(); + assertEquals(2, regions.size()); + assertArrayEquals(new long[]{-1L, 0L}, regions.get(0)); + assertArrayEquals(new long[]{0L, 0L}, regions.get(1)); assertEquals(1, result.targetChunks()); assertEquals(529, result.chunks().size()); 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"), """ {"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("generators/flat.json"), "{\"composite\":[]}"); diff --git a/adapters/bukkit/plugin/build.gradle b/adapters/bukkit/plugin/build.gradle index e3f1b98ed..d711f56e4 100644 --- a/adapters/bukkit/plugin/build.gradle +++ b/adapters/bukkit/plugin/build.gradle @@ -14,7 +14,7 @@ dependencies { // log4j-core Filter interface, so its constructor reference needs the type resolvable. compileOnly(libs.log4j.core) testImplementation('junit:junit:4.13.2') - testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.mockito.core) testImplementation(libs.paper.api) testImplementation(libs.bstats) testImplementation(project(':core')) diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandFind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandFind.java index 8cbc5f74f..54c03e8ea 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandFind.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandFind.java @@ -31,8 +31,7 @@ import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; import art.arcane.iris.structure.placement.StructureReachability; import art.arcane.iris.world.history.GenerationSemanticQueries; -import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; -import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; +import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; import art.arcane.iris.generation.terrain.IrisRegion; @@ -196,9 +195,9 @@ public class CommandFind implements DirectorExecutor { 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( - @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, @Param(description = "Should you be teleported", defaultValue = "true") boolean teleport @@ -212,42 +211,32 @@ public class CommandFind implements DirectorExecutor { } return; } - IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime(); - HydrologyFeatureQuery query; + HydrologyFeatureType featureType; try { - query = HydrologyFeatureQuery.parse(type); + featureType = HydrologyFeatureType.parse(type); } catch (IllegalArgumentException error) { commandSender.sendMessage(C.RED + error.getMessage()); return; } + String label = featureType.key().replace('_', ' '); Location origin = target.getLocation(); World originWorld = origin.getWorld(); - int requestedDistance = runtime == null - ? 8192 - : 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 + "..."); + int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE; + commandSender.sendMessage(C.GRAY + GenerationSemanticQueries.riverSearchMessage(featureType, maximumDistance)); J.a(() -> { try { GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver( activeEngine, - query, + featureType, origin.getBlockX(), origin.getBlockZ(), - maximumDistance, - (int visited) -> sendStructureMessage(target, commandSender, - C.GRAY + "Searched " + visited + " hydrology tiles for " + type + "...") + maximumDistance ).orElse(null); if (feature == null) { sendStructureMessage(target, commandSender, - C.YELLOW + "No accepted " + type + " hydrology feature found within " - + maximumDistance + " blocks."); + C.YELLOW + GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance)); return; } - String label = type + " hydrology feature"; if (!teleport) { sendStructureMessage(target, commandSender, 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); } catch (Throwable error) { sendStructureMessage(target, commandSender, - C.RED + "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName()); - Iris.reportError("Could not locate accepted hydrology feature '" + type + "'.", error); + C.RED + "Could not search for " + label + ": " + error.getClass().getSimpleName() + + (error.getMessage() == null ? "" : " - " + error.getMessage())); + Iris.reportError("Could not locate hydrology feature '" + featureType.key() + "'.", error); } }); } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVC.java index 2106bb380..44449af97 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVC.java @@ -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.IrisWorldInfoFactory; 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.hydrology.raster.WaterRole; import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.terrain.IrisRegion; @@ -108,15 +107,15 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { try { 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( - engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ)); + engine.getComplex().getWaterHeadStream().get(blockX, blockZ)); InferredType inferredType = null; if (IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)) { IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ); inferredType = biome == null ? null : biome.getInferredType(); } - return IrisSurfaceClassifier.classify(surface, fluid, inferredType, hydrology); + return IrisSurfaceClassifier.classify(surface, fluid, inferredType, role); } catch (Throwable error) { reportQueryFault("surfaceKind", world, error); return IrisSurfaceKind.UNKNOWN; @@ -266,12 +265,12 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { return false; } - HydrologyColumnSample hydrology = wantRiver - ? hydrologySample(engine, blockX, blockZ) - : null; + WaterRole role = wantRiver + ? engine.getComplex().getWaterRoleStream().get(blockX, blockZ) + : WaterRole.NONE; int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0; int fluid = wantRiver - ? (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ)) + ? (int) Math.round(engine.getComplex().getWaterHeadStream().get(blockX, blockZ)) : engine.getDimension().getFluidHeight(); boolean needsBiome = wantBiome || (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)); @@ -281,29 +280,23 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { surface, fluid, biome == null ? null : biome.getInferredType(), - hydrology + role ) : IrisSurfaceKind.UNKNOWN; String biomeKey = wantBiome && biome != null ? biome.getLoadKey() : null; int natural = wantNaturalHeight ? (int) Math.round(engine.getComplex().getNaturalHeightStream().get(blockX, blockZ)) + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT; - HydrologyColumnLayer surfaceLayer = hydrology == null - ? null - : hydrology.primarySurfaceLayer().orElse(null); - boolean riverPresent = surfaceLayer != null; IrisRiverState riverState = wantRiverState - ? riverState(hydrology) + ? riverState(role) : IrisRiverState.NONE; - double riverDistance = wantRiverDistance && riverPresent - ? engine.getComplex().getRiverDistanceStream().get(blockX, blockZ) + double riverDistance = wantRiverDistance + ? riverDistance(role) : IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE; - int riverFlow = wantRiverFlow && riverPresent - ? (int) Math.round(engine.getComplex().getRiverFlowStream().get(blockX, blockZ)) + int riverFlow = wantRiverFlow + ? riverFlow(role) : IrisColumnSample.UNAVAILABLE_RIVER_FLOW; - int riverWaterSurfaceY = wantRiverWaterSurface - && surfaceLayer != null - && surfaceLayer.connectedFluid() + int riverWaterSurfaceY = wantRiverWaterSurface && role.wet() ? fluid + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT; sink.accept(new IrisColumnSample( @@ -327,21 +320,25 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { } } - static IrisRiverState riverState(HydrologyColumnSample hydrology) { - HydrologyColumnLayer layer = hydrology == null - ? null - : hydrology.primarySurfaceLayer().orElse(null); - if (layer == null || !layer.channel()) { - return IrisRiverState.NONE; - } - return layer.connectedFluid() ? IrisRiverState.WET : IrisRiverState.DRY; + static IrisRiverState riverState(WaterRole role) { + return role.wet() ? IrisRiverState.WET : IrisRiverState.NONE; } - private static HydrologyColumnSample hydrologySample(Engine engine, int blockX, int blockZ) { - if (engine.getComplex().getHydrologyRuntime() == null) { - return null; - } - return engine.getComplex().getHydrologyRuntime().sample(blockX, blockZ).orElse(null); + static double riverDistance(WaterRole role) { + return 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; + }; + } + + 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 key(IrisBiome biome) { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifier.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifier.java index 9a2a40642..f54ffa704 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifier.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifier.java @@ -1,8 +1,7 @@ package art.arcane.iris.platform.bukkit.terrain; import art.arcane.iris.api.terrain.IrisSurfaceKind; -import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; -import art.arcane.iris.generation.hydrology.HydrologyColumnSample; +import art.arcane.iris.generation.hydrology.raster.WaterRole; import art.arcane.iris.generation.terrain.InferredType; public final class IrisSurfaceClassifier { @@ -29,25 +28,16 @@ public final class IrisSurfaceClassifier { int engineSurfaceHeight, int engineFluidHeight, InferredType inferredType, - HydrologyColumnSample hydrology + WaterRole role ) { if (engineSurfaceHeight <= 0) { return IrisSurfaceKind.VOID; } - HydrologyColumnLayer layer = hydrology == null - ? null - : hydrology.primarySurfaceLayer().orElse(null); - if (layer != null) { - if (layer.shore()) { - 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); + return switch (role) { + case NONE -> classify(engineSurfaceHeight, engineFluidHeight, inferredType); + case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE; + case BANK, DAM -> IrisSurfaceKind.LAND; + case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER; + }; } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandFindHydrologyTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandFindHydrologyTest.java index 8e5d2fce3..1c507db10 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandFindHydrologyTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandFindHydrologyTest.java @@ -1,7 +1,7 @@ package art.arcane.iris.command; 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.IrisComplex; import art.arcane.iris.localization.IrisLanguage; @@ -22,6 +22,7 @@ import java.util.OptionalLong; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -45,7 +46,7 @@ public class CommandFindHydrologyTest { @Test public void worldChangeDuringSearchDoesNotLoadOrTeleportIntoTheNewWorld() { try (Fixture fixture = new Fixture()) { - fixture.command.river("surface", true); + fixture.command.river("river", true); when(fixture.player.getWorld()).thenReturn(mock(World.class)); fixture.search.get().run(); @@ -58,7 +59,7 @@ public class CommandFindHydrologyTest { @Test public void disconnectDuringSearchDoesNotLoadTheDestination() { try (Fixture fixture = new Fixture()) { - fixture.command.river("surface", true); + fixture.command.river("river", true); when(fixture.player.isOnline()).thenReturn(false); fixture.search.get().run(); @@ -69,7 +70,7 @@ public class CommandFindHydrologyTest { @Test public void worldChangeWhileLoadingTheDestinationCancelsTeleport() { try (Fixture fixture = new Fixture()) { - fixture.command.river("surface", true); + fixture.command.river("river", true); fixture.search.get().run(); when(fixture.player.getWorld()).thenReturn(mock(World.class)); fixture.chunk.complete(mock(Chunk.class)); @@ -82,7 +83,7 @@ public class CommandFindHydrologyTest { @Test public void successIsReportedOnlyAfterTeleportCompletes() { try (Fixture fixture = new Fixture()) { - fixture.command.river("surface", true); + fixture.command.river("river", true); fixture.search.get().run(); fixture.chunk.complete(mock(Chunk.class)); @@ -95,7 +96,7 @@ public class CommandFindHydrologyTest { @Test public void rejectedTeleportReportsFailureWithoutSuccess() { try (Fixture fixture = new Fixture()) { - fixture.command.river("surface", true); + fixture.command.river("river", true); fixture.search.get().run(); fixture.chunk.complete(mock(Chunk.class)); fixture.teleport.complete(false); @@ -109,7 +110,7 @@ public class CommandFindHydrologyTest { public void failedTeleportReportsTheFullCauseWithoutSuccess() { try (Fixture fixture = new Fixture()) { IllegalStateException failure = new IllegalStateException("destination unloaded"); - fixture.command.river("surface", true); + fixture.command.river("river", true); fixture.search.get().run(); fixture.chunk.complete(mock(Chunk.class)); 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 final CommandFind command = mock(CommandFind.class, CALLS_REAL_METHODS); private final Engine engine = mock(Engine.class); @@ -132,7 +199,7 @@ public class CommandFindHydrologyTest { private final CompletableFuture teleport = new CompletableFuture<>(); private final MockedStatic scheduler = mockStatic(J.class); private final MockedStatic platform = mockStatic(BukkitPlatform.class); - private final MockedStatic queries = mockStatic(GenerationSemanticQueries.class); + private final MockedStatic queries = mockStatic(GenerationSemanticQueries.class, CALLS_REAL_METHODS); private final MockedStatic language = mockStatic(IrisLanguage.class, invocation -> "teleported"); private final MockedStatic 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.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( GenerationSemanticQueries.RiverSource.ACTIVE_PREDICTION, - HydrologyFeatureType.RIFFLE, Optional.empty(), 1L, 32, 63, 32, OptionalLong.empty()))); + HydrologyFeatureType.RIVER, 1L, 32, 63, 32, OptionalLong.empty()))); } @Override diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandObjectFluidHeightTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandObjectFluidHeightTest.java index 500dd2147..bd0e1585e 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandObjectFluidHeightTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandObjectFluidHeightTest.java @@ -20,26 +20,26 @@ import static org.mockito.Mockito.when; public class CommandObjectFluidHeightTest { @Test - public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() { + public void absoluteObjectPlacementShiftsTheColumnWaterHeadFromNegativeMinY() { World world = mock(World.class); Engine engine = mock(Engine.class); IrisDimension dimension = mock(IrisDimension.class); IrisComplex complex = mock(IrisComplex.class); @SuppressWarnings("unchecked") - ProceduralStream riverHead = mock(ProceduralStream.class); + ProceduralStream waterHead = mock(ProceduralStream.class); Map future = new HashMap<>(); when(engine.getMinHeight()).thenReturn(-64); when(engine.getDimension()).thenReturn(dimension); when(engine.getComplex()).thenReturn(complex); when(dimension.getFluidHeight()).thenReturn(127); - when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead); - when(riverHead.get(12, -7)).thenReturn(131D); + when(complex.getWaterHeadStream()).thenReturn(waterHead); + when(waterHead.get(12, -7)).thenReturn(131D); IObjectPlacer placer = CommandObject.createPlacer(world, future, engine); assertEquals(63, placer.getFluidHeight()); assertEquals(67, placer.getFluidHeight(12, -7)); - verify(riverHead).get(12, -7); + verify(waterHead).get(12, -7); } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVCTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVCTest.java index c9a654003..09783052f 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVCTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/api/IrisTerrainSVCTest.java @@ -9,16 +9,18 @@ import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.runtime.BiomeEnvironment; 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.IrisRegion; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.platform.generation.PlatformChunkGenerator; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.world.history.SavedBiomeUnavailableException; -import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; -import art.arcane.iris.generation.hydrology.HydrologyColumnSample; -import art.arcane.iris.generation.hydrology.HydrologyFeatureRef; -import art.arcane.iris.generation.hydrology.HydrologyFeatureType; +import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; +import art.arcane.iris.api.terrain.IrisColumnSample; +import art.arcane.iris.generation.hydrology.raster.WaterRole; import art.arcane.iris.platform.bukkit.plugin.IrisService; import org.bukkit.World; import org.bukkit.generator.ChunkGenerator; @@ -26,6 +28,7 @@ import org.junit.Test; import org.mockito.MockedStatic; import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Optional; @@ -112,10 +115,32 @@ public class IrisTerrainSVCTest { } @Test - public void acceptedHydrologyMapsToThePublicDiagnosticStates() { - assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(null)); - assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(true))); - assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(false))); + public void waterRolesMapToThePublicRiverFields() { + for (WaterRole role : WaterRole.values()) { + assertEquals(role.name(), role.wet() ? IrisRiverState.WET : IrisRiverState.NONE, IrisTerrainSVC.riverState(role)); + 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 @@ -219,6 +244,33 @@ public class IrisTerrainSVCTest { } } + private static List 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 samples = new ArrayList<>(); + try (MockedStatic 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) { World world = mock(World.class); ChunkGenerator generator = mock(ChunkGenerator.class, withSettings().extraInterfaces(PlatformChunkGenerator.class)); @@ -235,41 +287,4 @@ public class IrisTerrainSVCTest { ((AtomicBoolean) enabled.get(service)).set(true); 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)); - } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifierTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifierTest.java index 8536b792b..85b943be3 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifierTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/bukkit/terrain/IrisSurfaceClassifierTest.java @@ -1,15 +1,10 @@ package art.arcane.iris.platform.bukkit.terrain; import art.arcane.iris.api.terrain.IrisSurfaceKind; -import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; -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.hydrology.raster.WaterRole; import art.arcane.iris.generation.terrain.InferredType; import org.junit.Test; -import java.util.List; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -69,98 +64,28 @@ public class IrisSurfaceClassifierTest { } @Test - public void acceptedSurfaceRolesOverrideGenericOceanAndLandKinds() { - assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify( - 60, - 63, - InferredType.SEA, - hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true) - )); - assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify( - 60, - 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) - )); + public void waterRolesOverrideGenericOceanAndLandKinds() { + for (WaterRole role : WaterRole.values()) { + IrisSurfaceKind expected = switch (role) { + case NONE -> IrisSurfaceKind.OCEAN; + case SHORE, POND_SHORE -> IrisSurfaceKind.RIVER_SHORE; + case BANK, DAM -> IrisSurfaceKind.LAND; + case CHANNEL, POND, MOUTH, CASCADE, WATERFALL, INLET -> IrisSurfaceKind.RIVER; + }; + assertEquals(role.name(), expected, IrisSurfaceClassifier.classify(60, 63, InferredType.SEA, role)); + } } @Test - public void voidClassificationWinsOverAcceptedRiverGeometry() { - assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify( - 0, - 63, - InferredType.LAND, - hydrology(HydrologyFeatureType.SURFACE_POOL, true, false, true) - )); + public void voidClassificationWinsOverWaterRoles() { + for (WaterRole role : WaterRole.values()) { + assertEquals(role.name(), IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify(0, 63, InferredType.LAND, role)); + } } @Test - public void absentRejectedCandidatesDoNotCreatePublicRiverKinds() { - assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify( - 64, - 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)); + public void columnsWithoutAWaterRoleKeepTheGenericKinds() { + assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify(64, 63, InferredType.LAND, WaterRole.NONE)); + assertEquals(IrisSurfaceKind.SHORE, IrisSurfaceClassifier.classify(64, 63, InferredType.SHORE, WaterRole.NONE)); } } diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index 648adc762..2ee7fbcfa 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -153,7 +153,7 @@ dependencies { String modConfiguration = irisModTarget.fabricRemap ? 'modImplementation' : 'implementation' add(modConfiguration, "net.fabricmc:fabric-loader:${fabricLoaderVersion}") testImplementation('junit:junit:4.13.2') - testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.mockito.core) testImplementation(testFixtures('art.arcane:core')) testImplementation(libs.spigot) { transitive = false @@ -236,6 +236,8 @@ tasks.named('compileJava', JavaCompile).configure { options.release.set((int) irisModTarget.java) } +apply from: file('../../gradle/mockito-agent.gradle') + tasks.named('test').configure { } diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle index d69d925bb..727059081 100644 --- a/adapters/forge/build.gradle +++ b/adapters/forge/build.gradle @@ -178,7 +178,7 @@ dependencies { ? minecraft.dependency("net.minecraftforge:forge:${forgeVersion}") : minecraft.dependency("iris-${buildLane}", "net.minecraftforge:forge:${forgeVersion}")) testImplementation('junit:junit:4.13.2') - testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.mockito.core) testImplementation(testFixtures('art.arcane:core')) testImplementation(libs.spigot) { transitive = false @@ -272,6 +272,8 @@ tasks.named('compileJava', JavaCompile).configure { options.release.set((int) irisModTarget.java) } +apply from: file('../../gradle/mockito-agent.gradle') + tasks.named('test').configure { } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java index 5e7424711..fa56f2cba 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandSuggestions.java @@ -25,7 +25,7 @@ import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.structure.placement.IrisStructureLocator; import art.arcane.iris.structure.nativegen.NativeStructureGenerationPolicy; 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.generation.biome.IrisBiome; import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; @@ -61,7 +61,7 @@ final class ModdedCommandSuggestions { static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder); static final SuggestionProvider STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder); static final SuggestionProvider POI_TYPES = (CommandContext context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(List.of("buried_treasure"), builder); - static final SuggestionProvider HYDROLOGY_TYPES = (CommandContext context, SuggestionsBuilder builder) -> suggestHydrologyTypes(context, builder); + static final SuggestionProvider HYDROLOGY_TYPES = (CommandContext context, SuggestionsBuilder builder) -> NativeCommandRegistration.suggest(HydrologyFeatureType.keys(), builder); static final SuggestionProvider PACK_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestPackNames(context, builder); static final SuggestionProvider DIMENSION_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder); @@ -87,24 +87,6 @@ final class ModdedCommandSuggestions { return builder.buildFuture(); } - private static CompletableFuture suggestHydrologyTypes( - CommandContext 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 reachableBiomeKeys(Engine engine) { return reachableBiomeKeys(GenerationFindCatalog.biomes(engine)); } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java index fa600f153..077f9597d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java @@ -39,8 +39,7 @@ import art.arcane.iris.structure.placement.StructureReachability; import art.arcane.iris.generation.runtime.WrongEngineBroException; import art.arcane.iris.world.history.GenerationSemanticQueries; import art.arcane.iris.world.history.GenerationFindCatalog; -import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; -import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; +import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.structure.nativegen.IrisNativeStructureDecision; import art.arcane.iris.generation.terrain.IrisRegion; @@ -143,39 +142,31 @@ final class ModdedLocateCommands { IrisModdedCommands.fail(source, "This dimension is not generated by Iris."); return 0; } - IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime(); - HydrologyFeatureQuery query; + HydrologyFeatureType featureType; try { - query = HydrologyFeatureQuery.parse(type); + featureType = HydrologyFeatureType.parse(type); } catch (IllegalArgumentException error) { IrisModdedCommands.fail(source, error.getMessage()); return 0; } + String label = featureType.key().replace('_', ' '); int originX = player.blockX(); int originZ = player.blockZ(); - int requestedDistance = runtime == null - ? 8192 - : Math.min(8192, runtime.settings().routing().tileSize() * 15); - int maximumDistance = runtime == null - ? requestedDistance - : runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance); + int maximumDistance = GenerationSemanticQueries.RIVER_SEARCH_DISTANCE; 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( - engine, query, originX, originZ, maximumDistance, - (int visited) -> server.execute(() -> IrisModdedCommands.ok(source, - "Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null)) + engine, featureType, originX, originZ, maximumDistance).orElse(null)) .whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> { if (error != null) { - ModdedIrisLog.error("Hydrology locate failed for {}", type, error); - IrisModdedCommands.fail(source, - "Could not locate " + type + " hydrology: " + error.getClass().getSimpleName()); + Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error; + ModdedIrisLog.error("Hydrology locate failed for {}", featureType.key(), cause); + IrisModdedCommands.fail(source, "Could not search for " + label + ": " + cause.getClass().getSimpleName() + + (cause.getMessage() == null ? "" : " - " + cause.getMessage())); return; } if (feature == null) { - IrisModdedCommands.fail(source, - "No accepted " + type + " hydrology feature found within " - + maximumDistance + " blocks."); + IrisModdedCommands.fail(source, GenerationSemanticQueries.riverNotFoundMessage(featureType, maximumDistance)); return; } teleportToStructure( @@ -185,7 +176,7 @@ final class ModdedLocateCommands { feature.x(), feature.y() + 2, feature.z(), - type + " hydrology feature" + label ); })); return 1; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java index 9d1df42f9..46080f526 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java @@ -23,6 +23,7 @@ import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime; import art.arcane.volmlib.nativelib.terrain.NativePregenRuntime.ChunkLoad; import art.arcane.iris.configuration.IrisSettings; 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.PregenMantleBackpressure; 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.volmlib.util.mantle.runtime.Mantle; +import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -71,6 +73,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod { private final PregenMantleBackpressure backpressure; private final PauseWhenEmptyGuard pauseGuard; private final LongSupplier clock; + private final AtomicBoolean closing = new AtomicBoolean(); + private final PregenHydrology hydrology; public ModdedPregenMethod(Configuration configuration) { this(configuration, System::nanoTime); @@ -80,6 +84,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod { this.clock = Objects.requireNonNull(clock); runtime = configuration.runtime(); engine = configuration.engine(); + hydrology = new PregenHydrology(() -> engine); sync = configuration.sync(); pauseGuard = new PauseWhenEmptyGuard(runtime); IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); @@ -121,6 +126,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod { @Override public void close() { + closing.set(true); + hydrology.close(); try { awaitPendingChunks(); ModdedIrisLog.info("Iris modded pregen closing: dim={} completed={} pending={} peakInFlight={} finalLimit={}", @@ -341,6 +348,13 @@ public final class ModdedPregenMethod implements PregeneratorMethod { return false; } + @Override + public void onRegionsAhead(List regions) { + if (!closing.get()) { + hydrology.prepare(regions); + } + } + @Override public String getMethod(int x, int z) { return "Modded"; diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedLocateRiverTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedLocateRiverTest.java new file mode 100644 index 000000000..afd149bb0 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedLocateRiverTest.java @@ -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 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 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 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 commands = fixture.commands(); + MockedConstruction queries = mockConstruction(NativeStructureQueries.class)) { + assertEquals(1, ModdedLocateCommands.gotoRiver(fixture.source, "waterfall")); + fixture.finishOnServerThread(); + + ArgumentCaptor 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 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 commands() { + MockedStatic commands = mockStatic(IrisModdedCommands.class); + commands.when(() -> IrisModdedCommands.engineFor(world)).thenReturn(engine); + return commands; + } + + private void finishOnServerThread() throws Exception { + completion.get(10L, TimeUnit.SECONDS).run(); + } + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedPregenMethodLifecycleTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedPregenMethodLifecycleTest.java index 5e6447dac..a35f5fa61 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedPregenMethodLifecycleTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedPregenMethodLifecycleTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.modded.command; 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.modded.ModdedGenPool; import art.arcane.iris.world.pregen.MantleHeapPressure; @@ -11,8 +12,10 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.MockedStatic; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -31,9 +34,11 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; @@ -89,6 +94,25 @@ public class ModdedPregenMethodLifecycleTest { 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 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 public void asynchronousCallerCannotClaimTheShutdownSave() { assertFalse(method.deferFinalSaveToServerThread()); diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle index 2314edb3a..6087291ed 100644 --- a/adapters/neoforge/build.gradle +++ b/adapters/neoforge/build.gradle @@ -141,7 +141,7 @@ configurations.testRuntimeClasspath.extendsFrom(configurations.runtimeClasspath, dependencies { testImplementation('junit:junit:4.13.2') - testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.mockito.core) testImplementation(testFixtures('art.arcane:core')) testImplementation(libs.spigot) { transitive = false @@ -261,6 +261,8 @@ tasks.named('compileJava', JavaCompile).configure { options.release.set((int) irisModTarget.java) } +apply from: file('../../gradle/mockito-agent.gradle') + tasks.named('test').configure { useJUnitPlatform() classpath = classpath.minus(developmentModLibraries).plus(sourceSets.modLibraries.output) diff --git a/build.gradle b/build.gradle index 96718e4c1..fca8d7fc2 100644 --- a/build.gradle +++ b/build.gradle @@ -127,7 +127,7 @@ nmsBindings.each { String key -> compileOnly(rootProject.libs.annotations) compileOnly(rootProject.libs.byteBuddy.core) testImplementation('junit:junit:4.13.2') - testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(rootProject.libs.mockito.core) if (key == 'v26_3_R1') { testImplementation(project(':probe')) } @@ -586,11 +586,14 @@ allprojects { layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}")) } + apply from: rootProject.file('gradle/mockito-agent.gradle') + tasks.withType(Test).configureEach { int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks) : project.path == ':probe' ? Math.min(4, testForks) : testForks maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks) jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2') + timeout = java.time.Duration.ofMinutes(60) } java { diff --git a/core/build.gradle b/core/build.gradle index e18d4d249..3c7f9e75d 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -163,11 +163,11 @@ dependencies { slim(libs.jaxen) testFixturesImplementation('junit:junit:4.13.2') - testFixturesImplementation('org.mockito:mockito-core:5.23.0') + testFixturesImplementation(libs.mockito.core) testFixturesCompileOnly(libs.paper.api) testImplementation('junit:junit:4.13.2') - testImplementation('org.mockito:mockito-core:5.23.0') + testImplementation(libs.mockito.core) testImplementation(libs.paper.api) testImplementation(libs.log4j.core) testRuntimeOnly(libs.paper.api) @@ -177,7 +177,8 @@ dependencies { } tasks.named('test').configure { - maxHeapSize = '1g' + timeout = java.time.Duration.ofMinutes(60) + maxHeapSize = '2g' jvmArgs('-XX:+ExitOnOutOfMemoryError') systemProperty('iris.generation.counters', 'true') } diff --git a/core/src/main/java/art/arcane/iris/command/specialhandlers/HydrologyTypeHandler.java b/core/src/main/java/art/arcane/iris/command/specialhandlers/HydrologyTypeHandler.java index 5c27b448e..2f2c81975 100644 --- a/core/src/main/java/art/arcane/iris/command/specialhandlers/HydrologyTypeHandler.java +++ b/core/src/main/java/art/arcane/iris/command/specialhandlers/HydrologyTypeHandler.java @@ -1,25 +1,14 @@ package art.arcane.iris.command.specialhandlers; -import art.arcane.iris.generation.runtime.Engine; -import art.arcane.iris.generation.hydrology.HydrologyFeatureQuery; -import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.command.DirectorParameterHandler; +import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; -import java.util.List; - public final class HydrologyTypeHandler implements DirectorParameterHandler { @Override public KList getPossibilities() { - Engine activeEngine = engine(); - if (activeEngine == null) { - return new KList<>(HydrologyFeatureQuery.suggestions(List.of())); - } - IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime(); - return new KList<>(runtime == null - ? HydrologyFeatureQuery.suggestions(List.of()) - : runtime.featureQueryKeys()); + return new KList<>(HydrologyFeatureType.keys()); } @Override @@ -29,16 +18,11 @@ public final class HydrologyTypeHandler implements DirectorParameterHandler> 4, z >> 4); + if (!raster.provisional()) { + waterRaster = raster; + } + return raster; + } + public FloatingIslandBoundarySampler getFloatingIslandBoundarySampler() { return floatingIslandBoundarySampler; } diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java b/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java index e57e58ebe..8cee6de8e 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java @@ -542,7 +542,7 @@ final class DecoratorCore { return stack; } - private static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) { + static boolean allowsSurface(IrisDecorator decorator, NativeBlockState surface, IrisData data) { return decorator.isForcePlace() || (decorator.getWhitelist() == null || matchesPalette(decorator.getWhitelistArray(data), surface)) && (decorator.getBlacklist() == null || !matchesPalette(decorator.getBlacklistArray(data), surface)); diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/HydrologyVegetation.java b/core/src/main/java/art/arcane/iris/generation/decoration/HydrologyVegetation.java new file mode 100644 index 000000000..1ade171c5 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/decoration/HydrologyVegetation.java @@ -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 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 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 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 decorators, IrisData data, String path) { + HydrologySettings.requireDecorators(decorators, path); + if (decorators == null || decorators.isEmpty()) { + return EMPTY; + } + List floor = new ArrayList<>(); + List surface = new ArrayList<>(); + List 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 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 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; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisSugarCane.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisSugarCane.java index 2962be290..d22d0bcfa 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisSugarCane.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisSugarCane.java @@ -2,8 +2,6 @@ package art.arcane.iris.generation.decoration; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.Engine; -import art.arcane.iris.generation.hydrology.HydrologyColumnLayer; -import art.arcane.iris.generation.hydrology.HydrologyColumnSample; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; @@ -71,21 +69,18 @@ final class IrisSugarCane { private static boolean plannedWater(Engine engine, int x, int y, int z) { IrisComplex complex = engine.getComplex(); - HydrologyColumnSample sample = complex.sampleHydrologyColumn(x, z); - HydrologyColumnLayer fluid = sample == null ? null : sample.primarySurfaceFluidLayerOrNull(); - if (fluid != null) { - return y > fluid.bedY() && y <= fluid.fluidHeadY() - && supportsAdjacent(complex.resolveHydrologyFluid(fluid.profileKey(), x, z)); - } int terrainHeight = (int) Math.round(complex.getHeightStream().getDouble(x, z)); - int fluidHeight = (int) Math.round(complex.getRiverWaterSurfaceStream().getDouble(x, z)); + int fluidHeight = (int) Math.round(complex.getWaterHeadStream().getDouble(x, z)); if (y <= terrainHeight || y > fluidHeight) { return false; } + if (complex.getWaterRoleStream().get(x, z).wet()) { + return supportsAdjacent(complex.resolveWaterFluid(x, z)); + } IrisBiome biome = complex.getTrueBiomeStream().get(x, z); KList layers = biome.generateSeaLayers(x, z, new RNG(engine.getSeedManager().getTerrain()), fluidHeight - terrainHeight, engine.getData()); int depth = fluidHeight - y; - return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSurfaceFluid(x, z)); + return supportsAdjacent(layers.hasIndex(depth) ? layers.get(depth) : complex.resolveSeaFluid(x, z)); } } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateBounds.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateBounds.java deleted file mode 100644 index 84e60545b..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateBounds.java +++ /dev/null @@ -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(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateBuilder.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateBuilder.java deleted file mode 100644 index 282637a8c..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateBuilder.java +++ /dev/null @@ -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 actions; - private final LinkedHashSet openings; - private final CavePositionIndex openingIndex; - private final List 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 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; - }; - } - -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateSpanBuilder.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateSpanBuilder.java deleted file mode 100644 index f422adc14..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCandidateSpanBuilder.java +++ /dev/null @@ -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 actions; - private final LinkedHashMap openings; - private final LinkedHashMap coastalGrottoActions; - private final LinkedHashMap inlandGrottoActions; - private final List 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 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 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 actionSpans = excludedActions == null ? List.of() : excludedActions.spans(); - List 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 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 spans) { - long count = 0L; - for (CaveSpanSet spanSet : spans.values()) { - count += spanSet.positionCount(); - } - return count; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCourseGraph.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCourseGraph.java deleted file mode 100644 index fc1b20168..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveCourseGraph.java +++ /dev/null @@ -1,10 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; - -record CaveCourseGraph( - List nodes, - List edges, - List outlets -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSpanSet.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSpanSet.java deleted file mode 100644 index 857c0b570..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSpanSet.java +++ /dev/null @@ -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 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 spans() { - if (normalized != null) { - return normalized; - } - sortPending(); - ArrayList 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 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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSurfaceComposition.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSurfaceComposition.java deleted file mode 100644 index 34f48e481..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSurfaceComposition.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSurfaceOpening.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSurfaceOpening.java deleted file mode 100644 index 6e85f895b..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveSurfaceOpening.java +++ /dev/null @@ -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 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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveYSpan.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CaveYSpan.java deleted file mode 100644 index 76a079066..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CaveYSpan.java +++ /dev/null @@ -1,4 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record CaveYSpan(int minimumY, int maximumY) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CompiledGraph.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CompiledGraph.java deleted file mode 100644 index f10e831c4..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CompiledGraph.java +++ /dev/null @@ -1,12 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; -import java.util.Map; - -record CompiledGraph( - List nodes, - List edges, - List outlets, - Map edgeByUpstream -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CourseEdgeKey.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CourseEdgeKey.java deleted file mode 100644 index c8ab680ff..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CourseEdgeKey.java +++ /dev/null @@ -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"); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileBlockers.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileBlockers.java deleted file mode 100644 index ee6c1562d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileBlockers.java +++ /dev/null @@ -1,14 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; -import java.util.Objects; - -record CrossTileBlockers( - List caveClaims, - List surfaceClaims -) { - CrossTileBlockers { - caveClaims = List.copyOf(Objects.requireNonNull(caveClaims, "caveClaims")); - surfaceClaims = List.copyOf(Objects.requireNonNull(surfaceClaims, "surfaceClaims")); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileDraftAdmission.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileDraftAdmission.java deleted file mode 100644 index 5a6176387..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileDraftAdmission.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -interface CrossTileDraftAdmission { - void prepare(); - - CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result); -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTilePublicationAdmission.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTilePublicationAdmission.java deleted file mode 100644 index 01e568fc6..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTilePublicationAdmission.java +++ /dev/null @@ -1,15 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; -import java.util.Objects; - -record CrossTilePublicationAdmission( - HydrologyCaveCourseFilter.Result result, - List diagnostics, - boolean rejectedCourses -) { - CrossTilePublicationAdmission { - Objects.requireNonNull(result, "result"); - diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileRejectedCourse.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileRejectedCourse.java deleted file mode 100644 index fc4ea5c4b..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileRejectedCourse.java +++ /dev/null @@ -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"); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileRejectionKey.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileRejectionKey.java deleted file mode 100644 index f33ceefd4..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileRejectionKey.java +++ /dev/null @@ -1,4 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record CrossTileRejectionKey(long courseId, long winnerCourseId) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolution.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolution.java deleted file mode 100644 index b9f498337..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolution.java +++ /dev/null @@ -1,17 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; -import java.util.Objects; - -record CrossTileResolution( - HydrologyOwnerDraft draft, - List observedRejections, - int iterations, - int ownerCount, - long resolutionNanos -) { - CrossTileResolution { - Objects.requireNonNull(draft, "draft"); - observedRejections = List.copyOf(Objects.requireNonNull(observedRejections, "observedRejections")); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolutionContext.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolutionContext.java deleted file mode 100644 index d74ac8d93..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolutionContext.java +++ /dev/null @@ -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 resolved; - private final Set 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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolvedOwner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolvedOwner.java deleted file mode 100644 index e922b041d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/CrossTileResolvedOwner.java +++ /dev/null @@ -1,21 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; -import java.util.Objects; - -record CrossTileResolvedOwner( - HydrologyOwnerDraft draft, - List 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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/DraftPhase.java b/core/src/main/java/art/arcane/iris/generation/hydrology/DraftPhase.java deleted file mode 100644 index 09d4aa340..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/DraftPhase.java +++ /dev/null @@ -1,8 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -enum DraftPhase { - CONTEXT, - SELECT, - SETTLE, - PUBLISH -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/DraftProfile.java b/core/src/main/java/art/arcane/iris/generation/hydrology/DraftProfile.java deleted file mode 100644 index 1ee174ba0..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/DraftProfile.java +++ /dev/null @@ -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() - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/DrainageEdge.java b/core/src/main/java/art/arcane/iris/generation/hydrology/DrainageEdge.java deleted file mode 100644 index 654b6889c..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/DrainageEdge.java +++ /dev/null @@ -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 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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/DrainageNode.java b/core/src/main/java/art/arcane/iris/generation/hydrology/DrainageNode.java deleted file mode 100644 index 831b03e7e..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/DrainageNode.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintCenterlineProjection.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintCenterlineProjection.java deleted file mode 100644 index 974f0e04e..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintCenterlineProjection.java +++ /dev/null @@ -1,11 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record FootprintCenterlineProjection( - int pointIndex, - double x, - double z, - double distance, - int flowX, - int flowZ -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintCourseRasterKey.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintCourseRasterKey.java deleted file mode 100644 index cb545e757..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintCourseRasterKey.java +++ /dev/null @@ -1,11 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; - -record FootprintCourseRasterKey( - long courseId, - RiverCourseType courseType, - String profileKey, - List segments -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintFeatureKey.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintFeatureKey.java deleted file mode 100644 index 84649c458..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintFeatureKey.java +++ /dev/null @@ -1,11 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record FootprintFeatureKey( - long courseId, - long segmentId, - HydrologyFeatureType type, - int role, - int flowX, - int flowZ -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintLayerShape.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintLayerShape.java deleted file mode 100644 index 10893843b..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintLayerShape.java +++ /dev/null @@ -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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintMutableColumn.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintMutableColumn.java deleted file mode 100644 index beeee8e1d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintMutableColumn.java +++ /dev/null @@ -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 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 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 resolveSurfaceHeadConflicts(List candidates) { - HashMap 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 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() - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencil.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencil.java deleted file mode 100644 index d16b7f4b9..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencil.java +++ /dev/null @@ -1,11 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record FootprintRasterStencil( - int[] deltaXs, - int[] deltaZs, - double[] distances -) { - int size() { - return deltaXs.length; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencilBuilder.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencilBuilder.java deleted file mode 100644 index 7b80ec975..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencilBuilder.java +++ /dev/null @@ -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) - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencilKey.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencilKey.java deleted file mode 100644 index b68ee12eb..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintRasterStencilKey.java +++ /dev/null @@ -1,8 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record FootprintRasterStencilKey( - int channelRadius, - double shoreWidth, - double gradingWidth -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintValidationCourseRaster.java b/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintValidationCourseRaster.java deleted file mode 100644 index e4a866efe..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/FootprintValidationCourseRaster.java +++ /dev/null @@ -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 columns, - SurfaceFootprint surface -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydraulicChannelProfile.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydraulicChannelProfile.java deleted file mode 100644 index fcd64d9e3..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydraulicChannelProfile.java +++ /dev/null @@ -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) + "]"; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydraulicSegment.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydraulicSegment.java deleted file mode 100644 index 0d7908dd5..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydraulicSegment.java +++ /dev/null @@ -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 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(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyBasisCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyBasisCache.java deleted file mode 100644 index 5a283a0be..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyBasisCache.java +++ /dev/null @@ -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 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 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 samples, Stripe[] stripes) { - for (Long2ObjectMap.Entry 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 samples; - private final Long2ObjectOpenHashMap 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; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCacheBudget.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCacheBudget.java deleted file mode 100644 index 04f0c7997..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCacheBudget.java +++ /dev/null @@ -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; - } - -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCacheWeights.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCacheWeights.java deleted file mode 100644 index 512357c32..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCacheWeights.java +++ /dev/null @@ -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 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 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 nodes, List edges, List outlets, - List courses, List 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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCandidateKind.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCandidateKind.java deleted file mode 100644 index bd50d9ff4..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCandidateKind.java +++ /dev/null @@ -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 -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCandidateRejection.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCandidateRejection.java deleted file mode 100644 index 138042704..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCandidateRejection.java +++ /dev/null @@ -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; -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCaveCourseFilter.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCaveCourseFilter.java deleted file mode 100644 index 8f94648ac..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCaveCourseFilter.java +++ /dev/null @@ -1,1284 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.hydrology.cave.CavePosition; -import art.arcane.iris.generation.hydrology.cave.CaveVoxel; -import art.arcane.iris.generation.hydrology.cave.CaveVoxelView; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveContainmentPlanner; -import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan; -import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlannerSettings; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveRejection; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -final class HydrologyCaveCourseFilter { - static final long DIAGNOSTIC_SALT = 0x43415645464c5452L; - - static final int[][] NEIGHBORS = { - {1, 0, 0}, {-1, 0, 0}, - {0, 1, 0}, {0, -1, 0}, - {0, 0, 1}, {0, 0, -1} - } - -; - - static final int[][] HORIZONTAL_NEIGHBORS = { - {1, 0}, {-1, 0}, {0, 1}, {0, -1} - } - -; - - final HydrologyCaveContainmentPlanner planner; - final CaveVoxelView view; - final Options options; - final Map candidateCache; - final HydrologyCaveContainmentPlanner.ValidationCache validationCache; - final HydrologyObservedPlannedSurface plannedSurface; - - HydrologyCaveCourseFilter(CaveVoxelView view, Options options) { - this(view, options, null, null, null); - } - - HydrologyCaveCourseFilter( - CaveVoxelView view, - Options options, - Map candidateCache - ) { - this(view, options, candidateCache, null, null); - } - - HydrologyCaveCourseFilter( - CaveVoxelView view, - Options options, - Map candidateCache, - HydrologyCaveContainmentPlanner.ValidationCache validationCache, - HydrologyObservedPlannedSurface plannedSurface - ) { - this.planner = new HydrologyCaveContainmentPlanner(); - this.options = Objects.requireNonNull(options); - CaveVoxelView observedView = Objects.requireNonNull(view); - this.view = options.connectToExistingCaves() ? observedView : new GeneratedChannelView(observedView); - this.candidateCache = candidateCache; - this.validationCache = validationCache; - this.plannedSurface = plannedSurface; - } - - Result filter( - List nodes, - List edges, - List outlets, - List courses, - Iterable columns, - List diagnostics - ) { - return filter(nodes, edges, outlets, courses, columns, null, diagnostics); - } - - Result filter( - List nodes, - List edges, - List outlets, - List courses, - HydrologyFootprintCompiler.ValidationRaster validation, - List diagnostics - ) { - return filter(nodes, edges, outlets, courses, null, validation, diagnostics); - } - - Result filter( - List nodes, - List edges, - List outlets, - List courses, - Iterable columns, - HydrologyFootprintCompiler.ValidationRaster validation, - List diagnostics - ) { - List normalizedCourses = withoutZeroContributionEdges(courses); - LinkedHashMap publishedCoursesById = new LinkedHashMap<>(normalizedCourses.size()); - for (RiverCourse course : normalizedCourses) { - publishedCoursesById.put(course.id(), course); - } - ArrayList caveCourses = new ArrayList<>(normalizedCourses.size()); - for (RiverCourse course : normalizedCourses) { - if (representativeCaveSegment(course) != null) { - caveCourses.add(course); - } - } - LinkedHashMap candidateKeys = new LinkedHashMap<>(caveCourses.size()); - LinkedHashMap cachedCandidates = new LinkedHashMap<>(caveCourses.size()); - ArrayList uncachedCourses = new ArrayList<>(caveCourses.size()); - for (RiverCourse course : caveCourses) { - CandidateKey key = CandidateKey.create(course, options); - candidateKeys.put(course.id(), key); - HydrologyCaveCandidate cached = candidateCache == null ? null : candidateCache.get(key); - if (cached == null) { - uncachedCourses.add(course); - } else { - cachedCandidates.put(course.id(), cached); - } - } - Set preflightRejectedCourseIds = validation == null - ? preflightRejectedCourseIds(uncachedCourses, columns, diagnostics) - : preflightRejectedCourseIds(uncachedCourses, validation, diagnostics); - ArrayList candidateCourses = new ArrayList<>(caveCourses.size()); - for (RiverCourse course : caveCourses) { - if (!preflightRejectedCourseIds.contains(course.id())) { - candidateCourses.add(course); - } - } - uncachedCourses.removeIf((RiverCourse course) -> preflightRejectedCourseIds.contains(course.id())); - LinkedHashMap builders = validation == null - ? candidateBuilders(uncachedCourses, columns) - : candidateBuilders(uncachedCourses, validation); - if (candidateCourses.isEmpty() && preflightRejectedCourseIds.isEmpty()) { - CaveCourseGraph graph = acceptedGraph(nodes, edges, outlets, normalizedCourses); - return new Result( - graph.nodes(), - graph.edges(), - graph.outlets(), - normalizedCourses, - List.of() - ); - } - - HashSet rejectedCourseIds = new HashSet<>(preflightRejectedCourseIds); - ArrayList candidates = new ArrayList<>(candidateCourses.size()); - Set exposureValidatedCandidates = - Collections.newSetFromMap(new IdentityHashMap<>()); - LinkedHashMap coursesById = new LinkedHashMap<>(candidateCourses.size()); - for (RiverCourse course : candidateCourses) { - CandidateKey key = candidateKeys.get(course.id()); - HydrologyCaveCandidate candidate = cachedCandidates.get(course.id()); - if (candidate == null) { - CaveCandidateBuilder builder = builders.get(course.id()); - if (builder == null) { - throw new IllegalStateException("Hydrology cave candidate raster was empty."); - } - candidate = builder.build(options); - if (candidateCache != null) { - candidateCache.put(key, candidate); - } - exposureValidatedCandidates.add(candidate); - } - CaveSurfaceComposition composition = validation == null - ? CaveSurfaceComposition.accepted(candidate) - : composeSurfacePublication(candidate, course, validation, publishedCoursesById); - if (!composition.accepted()) { - if (rejectedCourseIds.add(course.id())) { - HydraulicSegment representative = representativeCaveSegment(course); - addDiagnostic( - course, - representative, - representative.type(), - HydrologyCandidateRejection.CAVE_CONTAINMENT, - diagnostics, - HydrologyCaveRejection.OVERLAPPING_SOURCE.ordinal() - ); - } - continue; - } - candidate = composition.candidate(); - if (course.surfaceSinkholeContinuation()) { - exposureValidatedCandidates.add(candidate); - } - candidates.add(candidate); - coursesById.put(course.id(), course); - } - alignSharedTerminalCandidateActions(candidates, coursesById, exposureValidatedCandidates); - List plans = planner.validateAllPlans( - view, - candidates, - validationCache, - plannedSurface, - exposureValidatedCandidates, - (HydrologyCaveCandidate first, HydrologyCaveCandidate second) -> - compatibleSharedCandidates(first, second, coursesById) - ); - plans = alignSharedTerminalPlanActions(plans, candidates, coursesById); - ArrayList acceptedPlans = new ArrayList<>(); - for (HydrologyCavePlan plan : plans) { - if (plan.accepted()) { - acceptedPlans.add(plan); - continue; - } - long courseId = plan.source().sourceId(); - if (rejectedCourseIds.add(courseId)) { - addDiagnostic(coursesById.get(courseId), plan, diagnostics); - } - } - if (rejectedCourseIds.isEmpty()) { - CaveCourseGraph graph = acceptedGraph(nodes, edges, outlets, normalizedCourses); - return new Result( - graph.nodes(), - graph.edges(), - graph.outlets(), - normalizedCourses, - List.copyOf(acceptedPlans) - ); - } - - ArrayList acceptedCourses = new ArrayList<>(); - for (RiverCourse course : normalizedCourses) { - if (!rejectedCourseIds.contains(course.id())) { - acceptedCourses.add(course); - } - } - CaveCourseGraph graph = acceptedGraph(nodes, edges, outlets, acceptedCourses); - return new Result( - graph.nodes(), - graph.edges(), - graph.outlets(), - List.copyOf(acceptedCourses), - List.copyOf(acceptedPlans) - ); - } - - boolean compatibleSharedCandidates( - HydrologyCaveCandidate first, - HydrologyCaveCandidate second, - Map coursesById - ) { - if (!first.profileKey().equals(second.profileKey())) { - return false; - } - RiverCourse firstCourse = coursesById.get(first.source().sourceId()); - RiverCourse secondCourse = coursesById.get(second.source().sourceId()); - return sharesDrainageOutlet(firstCourse, secondCourse) - || sharesTerminalGrotto(firstCourse, secondCourse, first); - } - - List alignSharedTerminalPlanActions( - List plans, - List candidates, - Map coursesById - ) { - HashMap candidatesById = new HashMap<>(candidates.size()); - for (HydrologyCaveCandidate candidate : candidates) { - candidatesById.put(candidate.source().sourceId(), candidate); - } - ArrayList aligned = new ArrayList<>(plans); - for (int firstIndex = 0; firstIndex < aligned.size() - 1; firstIndex++) { - for (int secondIndex = firstIndex + 1; secondIndex < aligned.size(); secondIndex++) { - HydrologyCavePlan first = aligned.get(firstIndex); - HydrologyCavePlan second = aligned.get(secondIndex); - HydrologyCaveCandidate firstCandidate = candidatesById.get(first.source().sourceId()); - HydrologyCaveCandidate secondCandidate = candidatesById.get(second.source().sourceId()); - if (!first.accepted() - || !second.accepted() - || firstCandidate == null - || secondCandidate == null - || !compatibleSharedCandidates(firstCandidate, secondCandidate, coursesById)) { - continue; - } - TerminalActions actions = alignTerminalActions(first.actions(), second.actions()); - if (actions == null) { - continue; - } - aligned.set(firstIndex, withActions(first, actions.first())); - aligned.set(secondIndex, withActions(second, actions.second())); - } - } - return List.copyOf(aligned); - } - - HydrologyCavePlan withActions( - HydrologyCavePlan plan, - Map actions - ) { - return new HydrologyCavePlan( - plan.source(), - plan.rejection(), - actions, - plan.baselinePreconditions(), - plan.arbitrationWinnerSourceId() - ); - } - - void alignSharedTerminalCandidateActions( - List candidates, - Map coursesById, - Set exposureValidatedCandidates - ) { - for (int firstIndex = 0; firstIndex < candidates.size() - 1; firstIndex++) { - for (int secondIndex = firstIndex + 1; secondIndex < candidates.size(); secondIndex++) { - HydrologyCaveCandidate first = candidates.get(firstIndex); - HydrologyCaveCandidate second = candidates.get(secondIndex); - RiverCourse firstCourse = coursesById.get(first.source().sourceId()); - RiverCourse secondCourse = coursesById.get(second.source().sourceId()); - if (!sharesDrainageOutlet(firstCourse, secondCourse) - && !sharesTerminalGrotto(firstCourse, secondCourse, first)) { - continue; - } - TerminalActions actions = alignTerminalActions(first.actions(), second.actions()); - if (actions == null) { - continue; - } - HydrologyCaveCandidate alignedFirst = withActions(first, actions.first()); - HydrologyCaveCandidate alignedSecond = withActions(second, actions.second()); - replaceExposureCandidate(exposureValidatedCandidates, first, alignedFirst); - replaceExposureCandidate(exposureValidatedCandidates, second, alignedSecond); - candidates.set(firstIndex, alignedFirst); - candidates.set(secondIndex, alignedSecond); - } - } - } - - private TerminalActions alignTerminalActions( - Map first, - Map second - ) { - LinkedHashMap firstActions = null; - LinkedHashMap secondActions = null; - for (Map.Entry entry : first.entrySet()) { - HydrologyCaveAction secondAction = second.get(entry.getKey()); - if (secondAction == null || secondAction == entry.getValue()) { - continue; - } - HydrologyCaveAction sharedAction = sharedTerminalAction(entry.getValue(), secondAction); - if (firstActions == null) { - firstActions = new LinkedHashMap<>(first); - secondActions = new LinkedHashMap<>(second); - } - firstActions.put(entry.getKey(), sharedAction); - secondActions.put(entry.getKey(), sharedAction); - } - return firstActions == null ? null : new TerminalActions(firstActions, secondActions); - } - - HydrologyCaveAction sharedTerminalAction( - HydrologyCaveAction first, - HydrologyCaveAction second - ) { - return sharedTerminalActionPriority(first) <= sharedTerminalActionPriority(second) - ? first - : second; - } - - int sharedTerminalActionPriority(HydrologyCaveAction action) { - return switch (action) { - case FALLING_FLUID -> 0; - case WET_SOURCE -> 1; - case DRY_AIR -> 2; - case SEAL_GUARD -> 3; - }; - } - - HydrologyCaveCandidate withActions( - HydrologyCaveCandidate candidate, - Map actions - ) { - return new HydrologyCaveCandidate( - candidate.source(), - candidate.profileKey(), - candidate.settings(), - candidate.allowDryCaveConnections(), - actions, - candidate.intentionalOpenings() - ); - } - - void replaceExposureCandidate( - Set exposureValidatedCandidates, - HydrologyCaveCandidate existing, - HydrologyCaveCandidate replacement - ) { - if (exposureValidatedCandidates.remove(existing)) { - exposureValidatedCandidates.add(replacement); - } - } - - CaveSurfaceComposition composeSurfacePublication( - HydrologyCaveCandidate candidate, - RiverCourse course, - HydrologyFootprintCompiler.ValidationRaster validation, - Map publishedCoursesById - ) { - LinkedHashMap composedActions = null; - for (HydrologyColumnSample caveSample : validation.columnsForCourse(course.id())) { - int maximumCaveY = maximumCaveY(caveSample, course); - if (maximumCaveY == Integer.MIN_VALUE) { - continue; - } - int plannedTerrainHeight = validation.plannedSurface().resolve( - caveSample.x(), - caveSample.z(), - caveSample.naturalHeight() - ); - if (maximumCaveY <= plannedTerrainHeight) { - continue; - } - HydrologyColumnSample surfaceSample = validation.surfaceColumnAt( - caveSample.x(), - caveSample.z(), - caveSample.naturalHeight() - ); - if (surfaceSample == null) { - continue; - } - for (int y = plannedTerrainHeight + 1; y <= maximumCaveY; y++) { - HydrologyColumnSample.SurfacePublicationCell surfaceCell = surfaceSample - .surfacePublicationCellAt(y) - .orElse(null); - if (surfaceCell == null) { - continue; - } - CavePosition position = new CavePosition(caveSample.x(), y, caveSample.z()); - HydrologyCaveAction caveAction = candidate.actions().get(position); - if (caveAction == null) { - continue; - } - long surfaceCourseId = surfaceCell.layer().feature().courseId(); - RiverCourse surfaceCourse = publishedCoursesById.get(surfaceCourseId); - boolean sharedTerminalGrotto = surfaceCourseId != course.id() - && sharesTerminalGrotto(course, surfaceCourse, candidate); - if (!candidate.profileKey().equals(surfaceCell.layer().profileKey()) - || (surfaceCourseId != course.id() - && !candidate.intentionalOpenings().contains(position) - && !sharedTerminalGrotto)) { - return CaveSurfaceComposition.rejected(); - } - if (caveAction == surfaceCell.action()) { - continue; - } - if (surfaceCourseId != course.id() - && !sharesDrainageOutlet(course, surfaceCourse) - && !sharedTerminalGrotto) { - return CaveSurfaceComposition.rejected(); - } - if (composedActions == null) { - composedActions = new LinkedHashMap<>(candidate.actions()); - } - composedActions.put(position, surfaceCell.action()); - } - } - if (composedActions == null) { - return CaveSurfaceComposition.accepted(candidate); - } - return CaveSurfaceComposition.accepted(new HydrologyCaveCandidate( - candidate.source(), - candidate.profileKey(), - candidate.settings(), - candidate.allowDryCaveConnections(), - composedActions, - candidate.intentionalOpenings() - )); - } - - boolean sharesTerminalGrotto( - RiverCourse first, - RiverCourse second, - HydrologyCaveCandidate candidate - ) { - if (second == null || !first.profileKey().equals(second.profileKey())) { - return false; - } - HydraulicSegment firstGrotto = terminalGrotto(first); - HydraulicSegment secondGrotto = terminalGrotto(second); - if (firstGrotto == null || secondGrotto == null) { - return false; - } - HydrologyPoint firstPoint = firstGrotto.end(); - HydrologyPoint secondPoint = secondGrotto.end(); - int horizontalRadius = Math.addExact( - candidate.settings().grottoHorizontalRadius(), - Math.max(1, (int) StrictMath.ceil(secondGrotto.width() / 2D)) - ); - int verticalRadius = Math.addExact( - candidate.settings().grottoVerticalRadius(), - Math.max(1, secondGrotto.depth()) - ); - return firstPoint.distanceSquared2D(secondPoint) <= (long) horizontalRadius * horizontalRadius - && Math.abs(firstPoint.y() - secondPoint.y()) <= verticalRadius; - } - - HydraulicSegment terminalGrotto(RiverCourse course) { - if (course == null || course.segments().isEmpty()) { - return null; - } - HydraulicSegment segment = course.segments().getLast(); - return segment.type().isGrotto() ? segment : null; - } - - boolean sharesDrainageOutlet(RiverCourse first, RiverCourse second) { - return second != null - && first.outletId().isPresent() - && second.outletId().isPresent() - && first.outletId().getAsLong() == second.outletId().getAsLong(); - } - - int maximumCaveY(HydrologyColumnSample sample, RiverCourse course) { - int maximumY = Integer.MIN_VALUE; - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() == course.id() - && isCaveLayer(layer, course) - && !layer.oceanApron() - && layer.channel() - && (layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) { - maximumY = Math.max(maximumY, layer.ceilingY()); - } - } - return maximumY; - } - - Set preflightRejectedCourseIds( - List courses, - Iterable columns, - List diagnostics - ) { - LinkedHashMap builders = candidateSpanBuilders(courses, columns); - return preflightRejectedCourseIds(builders, diagnostics); - } - - Set preflightRejectedCourseIds( - List courses, - HydrologyFootprintCompiler.ValidationRaster validation, - List diagnostics - ) { - LinkedHashMap builders = candidateSpanBuilders(courses, validation); - return preflightRejectedCourseIds(builders, diagnostics); - } - - Set preflightRejectedCourseIds( - LinkedHashMap builders, - List diagnostics - ) { - if (builders.isEmpty()) { - return Set.of(); - } - HashSet rejectedCourseIds = new HashSet<>(); - for (CaveCandidateSpanBuilder builder : builders.values()) { - HydrologyFeatureType oversizedGrotto = builder.oversizedGrotto(options); - boolean oversizedCourse = builder.positionCount() - > HydrologyCavePlannerSettings.MAXIMUM_PLANNED_MUTATIONS; - if (oversizedGrotto == null && !oversizedCourse) { - continue; - } - rejectedCourseIds.add(builder.course().id()); - addDiagnostic( - builder.course(), - builder.representative(), - oversizedGrotto == null ? builder.representative().type() : oversizedGrotto, - HydrologyCandidateRejection.VOLUME_LIMIT, - diagnostics, - HydrologyCandidateRejection.VOLUME_LIMIT.ordinal() - ); - } - for (CaveCandidateSpanBuilder builder : builders.values()) { - if (rejectedCourseIds.contains(builder.course().id()) - || !builder.exposed(view) - || builder.allowsIntentionalSurfaceExposure()) { - continue; - } - rejectedCourseIds.add(builder.course().id()); - addDiagnostic( - builder.course(), - builder.representative(), - builder.representative().type(), - HydrologyCandidateRejection.CAVE_CONTAINMENT, - diagnostics, - HydrologyCaveRejection.OPEN_SURFACE.ordinal() - ); - } - return Set.copyOf(rejectedCourseIds); - } - - List withoutZeroContributionEdges(List courses) { - ArrayList normalized = new ArrayList<>(courses.size()); - boolean changed = false; - for (RiverCourse course : courses) { - ArrayList contributingEdges = new ArrayList<>(course.drainageEdges().size()); - for (DrainageEdge edge : course.drainageEdges()) { - if (edge.totalContributingSources() > 0) { - contributingEdges.add(edge); - } else { - changed = true; - } - } - if (contributingEdges.size() == course.drainageEdges().size()) { - normalized.add(course); - continue; - } - normalized.add(new RiverCourse( - course.id(), - course.type(), - course.sourceNodeId(), - course.outletId(), - course.profileKey(), - course.discharge(), - contributingEdges, - course.segments() - )); - } - return changed ? List.copyOf(normalized) : courses; - } - - LinkedHashMap candidateSpanBuilders( - List courses, - Iterable columns - ) { - LinkedHashMap builders = new LinkedHashMap<>(); - for (RiverCourse course : courses) { - HydraulicSegment representative = representativeCaveSegment(course); - if (representative != null) { - builders.put(course.id(), new CaveCandidateSpanBuilder(course, representative)); - } - } - if (builders.isEmpty()) { - return builders; - } - - for (HydrologyColumnSample sample : columns) { - for (HydrologyColumnLayer layer : sample.layers()) { - CaveCandidateSpanBuilder builder = builders.get(layer.feature().courseId()); - if (builder == null || !isCaveLayer(layer, builder.course()) || layer.oceanApron() - || !layer.channel() || !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) { - continue; - } - builder.addAction(sample.x(), sample.z(), layer); - } - for (HydrologyColumnLayer layer : sample.layers()) { - CaveCandidateSpanBuilder builder = builders.get(layer.feature().courseId()); - if (builder != null && layer.oceanApron()) { - builder.addOceanOpening(sample.x(), sample.z(), sample.naturalHeight() + 1, layer.ceilingY()); - } - } - } - builders.values().removeIf(CaveCandidateSpanBuilder::isEmpty); - return builders; - } - - LinkedHashMap candidateSpanBuilders( - List courses, - HydrologyFootprintCompiler.ValidationRaster validation - ) { - LinkedHashMap builders = new LinkedHashMap<>(); - for (RiverCourse course : courses) { - HydraulicSegment representative = representativeCaveSegment(course); - if (representative == null) { - continue; - } - CaveCandidateSpanBuilder builder = new CaveCandidateSpanBuilder(course, representative); - for (HydrologyColumnSample sample : validation.columnsForCourse(course.id())) { - int maximumCaveY = Integer.MIN_VALUE; - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() != course.id() - || !isCaveLayer(layer, course) - || layer.oceanApron() - || !layer.channel() - || !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) { - continue; - } - builder.addAction(sample.x(), sample.z(), layer); - maximumCaveY = Math.max(maximumCaveY, layer.ceilingY()); - } - if (maximumCaveY != Integer.MIN_VALUE - && validation.ownsSurfaceChannelAt( - sample.x(), - sample.z(), - sample.naturalHeight(), - course.id() - )) { - int plannedTerrainHeight = validation.plannedSurface().resolve( - sample.x(), - sample.z(), - sample.naturalHeight() - ); - builder.addOpeningNeighborhood( - sample.x(), - sample.z(), - plannedTerrainHeight + 1, - maximumCaveY - ); - } - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() == course.id() && layer.oceanApron()) { - builder.addOceanOpening( - sample.x(), - sample.z(), - sample.naturalHeight() + 1, - layer.ceilingY() - ); - } - } - } - builder.addAdjacentSurfaceOpenings(validation); - if (!builder.isEmpty()) { - builders.put(course.id(), builder); - } - } - return builders; - } - - LinkedHashMap candidateBuilders( - List courses, - Iterable columns - ) { - LinkedHashMap builders = new LinkedHashMap<>(); - for (RiverCourse course : courses) { - HydraulicSegment representative = representativeCaveSegment(course); - if (representative != null) { - builders.put(course.id(), new CaveCandidateBuilder(course, representative)); - } - } - if (builders.isEmpty()) { - return builders; - } - - for (HydrologyColumnSample sample : columns) { - for (HydrologyColumnLayer layer : sample.layers()) { - CaveCandidateBuilder builder = builders.get(layer.feature().courseId()); - if (builder == null || !isCaveLayer(layer, builder.course()) || layer.oceanApron() - || !layer.channel() || !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) { - continue; - } - for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) { - CavePosition position = new CavePosition(sample.x(), y, sample.z()); - builder.addAction(position, actionAt(layer, y)); - builder.addSurfaceOpening(layer, position); - } - } - for (HydrologyColumnLayer layer : sample.layers()) { - CaveCandidateBuilder builder = builders.get(layer.feature().courseId()); - if (builder == null || !layer.oceanApron()) { - continue; - } - for (int y = sample.naturalHeight() + 1; y <= layer.ceilingY(); y++) { - builder.addOpening(new CavePosition(sample.x(), y, sample.z())); - } - } - } - builders.values().removeIf(CaveCandidateBuilder::isEmpty); - return builders; - } - - LinkedHashMap candidateBuilders( - List courses, - HydrologyFootprintCompiler.ValidationRaster validation - ) { - LinkedHashMap builders = new LinkedHashMap<>(); - for (RiverCourse course : courses) { - HydraulicSegment representative = representativeCaveSegment(course); - if (representative == null) { - continue; - } - CaveCandidateBuilder builder = new CaveCandidateBuilder(course, representative); - for (HydrologyColumnSample sample : validation.columnsForCourse(course.id())) { - int maximumCaveY = Integer.MIN_VALUE; - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() != course.id() - || !isCaveLayer(layer, course) - || layer.oceanApron() - || !layer.channel() - || !(layer.terrainOwned() || layer.fallingFluid() && layer.fluidOwned())) { - continue; - } - for (int y = layer.bedY() + 1; y <= layer.ceilingY(); y++) { - CavePosition position = new CavePosition(sample.x(), y, sample.z()); - builder.addAction(position, actionAt(layer, y)); - builder.addSurfaceOpening(layer, position); - } - maximumCaveY = Math.max(maximumCaveY, layer.ceilingY()); - } - if (maximumCaveY != Integer.MIN_VALUE - && validation.ownsSurfaceChannelAt( - sample.x(), - sample.z(), - sample.naturalHeight(), - course.id() - )) { - int plannedTerrainHeight = validation.plannedSurface().resolve( - sample.x(), - sample.z(), - sample.naturalHeight() - ); - for (int y = plannedTerrainHeight + 1; y <= maximumCaveY; y++) { - builder.addOpeningNeighborhood(new CavePosition(sample.x(), y, sample.z())); - } - } - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() != course.id() || !layer.oceanApron()) { - continue; - } - for (int y = sample.naturalHeight() + 1; y <= layer.ceilingY(); y++) { - builder.addOpening(new CavePosition(sample.x(), y, sample.z())); - } - } - } - builder.addAdjacentSurfaceOpenings(validation); - if (!builder.isEmpty()) { - builders.put(course.id(), builder); - } - } - return builders; - } - - static HydraulicSegment representativeCaveSegment(RiverCourse course) { - HydraulicSegment selected = null; - for (HydraulicSegment segment : course.segments()) { - if (!segment.type().isUnderground() && !segment.type().isDeepFluid() - && !(segment.type().isSurface() && segment.fallingFluid())) { - continue; - } - if (selected == null || candidatePriority(segment.type()) < candidatePriority(selected.type())) { - selected = segment; - } - } - return selected; - } - - static int candidatePriority(HydrologyFeatureType type) { - if (type.isGrotto()) { - return 0; - } - if (type.isDeepFluid()) { - return 1; - } - return 2; - } - - boolean isCaveLayer(HydrologyColumnLayer layer, RiverCourse course) { - if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) { - return true; - } - for (HydraulicSegment segment : course.segments()) { - if (segment.id() == layer.feature().segmentId()) { - return segment.type().isSurface() && segment.fallingFluid(); - } - } - return false; - } - - HydrologyCaveAction actionAt(HydrologyColumnLayer layer, int y) { - if (y > layer.fluidHeadY()) { - return HydrologyCaveAction.DRY_AIR; - } - if (layer.fallingFluid() && y < layer.fluidHeadY()) { - return HydrologyCaveAction.FALLING_FLUID; - } - return HydrologyCaveAction.WET_SOURCE; - } - - void addDiagnostic( - RiverCourse course, - HydrologyCavePlan plan, - List diagnostics - ) { - HydraulicSegment representative = representativeCaveSegment(course); - addDiagnostic( - course, - representative, - representative.type(), - HydrologyCandidateRejection.CAVE_CONTAINMENT, - diagnostics, - plan.rejection().ordinal() - ); - } - - void addDiagnostic( - RiverCourse course, - HydraulicSegment representative, - HydrologyFeatureType type, - HydrologyCandidateRejection rejection, - List diagnostics, - int rejectionSalt - ) { - HydrologyCandidateKind kind = course.type() == RiverCourseType.DEEP_FLUID - ? HydrologyCandidateKind.DEEP_FLUID - : type.isGrotto() ? HydrologyCandidateKind.OUTLET : HydrologyCandidateKind.SOURCE; - diagnostics.add(new HydrologyDiagnosticCandidate( - HydrologyHash.mix( - course.id(), - DIAGNOSTIC_SALT, - rejectionSalt - ), - kind, - type, - representative.start(), - rejection, 0 - )); - } - - static CaveCourseGraph acceptedGraph( - List nodes, - List edges, - List outlets, - List courses - ) { - LinkedHashSet nodeIds = new LinkedHashSet<>(); - LinkedHashSet edgeIds = new LinkedHashSet<>(); - LinkedHashSet outletIds = new LinkedHashSet<>(); - for (RiverCourse course : courses) { - if (course.sourceNodeId().isPresent()) { - nodeIds.add(course.sourceNodeId().getAsLong()); - } - if (course.outletId().isPresent()) { - outletIds.add(course.outletId().getAsLong()); - } - for (DrainageEdge edge : course.drainageEdges()) { - if (edge.totalContributingSources() == 0) { - continue; - } - edgeIds.add(edge.id()); - nodeIds.add(edge.upstreamNodeId()); - nodeIds.add(edge.downstreamNodeId()); - outletIds.add(edge.outletId()); - } - } - ArrayList acceptedNodes = new ArrayList<>(); - for (DrainageNode node : nodes) { - if (nodeIds.contains(node.id())) { - acceptedNodes.add(node); - outletIds.add(node.outletId()); - } - } - ArrayList acceptedEdges = new ArrayList<>(); - for (DrainageEdge edge : edges) { - if (edgeIds.contains(edge.id()) && edge.totalContributingSources() > 0) { - acceptedEdges.add(edge); - } - } - ArrayList acceptedOutlets = new ArrayList<>(); - for (RiverOutlet outlet : outlets) { - if (outletIds.contains(outlet.id())) { - acceptedOutlets.add(outlet); - } - } - return new CaveCourseGraph( - List.copyOf(acceptedNodes), - List.copyOf(acceptedEdges), - List.copyOf(acceptedOutlets) - ); - } - - static Result withoutCourses(Result result, Set rejectedCourseIds) { - Objects.requireNonNull(result); - Objects.requireNonNull(rejectedCourseIds); - if (rejectedCourseIds.isEmpty()) { - return result; - } - ArrayList acceptedCourses = new ArrayList<>(result.courses().size()); - for (RiverCourse course : result.courses()) { - if (!rejectedCourseIds.contains(course.id())) { - acceptedCourses.add(course); - } - } - ArrayList acceptedPlans = new ArrayList<>(result.cavePlans().size()); - for (HydrologyCavePlan plan : result.cavePlans()) { - if (!rejectedCourseIds.contains(plan.source().sourceId())) { - acceptedPlans.add(plan); - } - } - CaveCourseGraph graph = acceptedGraph( - result.nodes(), - result.edges(), - result.outlets(), - acceptedCourses - ); - return new Result( - graph.nodes(), - graph.edges(), - graph.outlets(), - List.copyOf(acceptedCourses), - List.copyOf(acceptedPlans) - ); - } - - static HydrologyDiagnosticCandidate overlapDiagnostic(RiverCourse course, long winnerSourceId) { - Objects.requireNonNull(course); - HydraulicSegment representative = representativeCaveSegment(course); - if (representative == null) { - throw new IllegalArgumentException("Cross-tile cave arbitration requires a cave-bearing course."); - } - HydrologyFeatureType type = representative.type(); - HydrologyCandidateKind kind = course.type() == RiverCourseType.DEEP_FLUID - ? HydrologyCandidateKind.DEEP_FLUID - : type.isGrotto() ? HydrologyCandidateKind.OUTLET : HydrologyCandidateKind.SOURCE; - return new HydrologyDiagnosticCandidate( - HydrologyHash.mix( - course.id(), - DIAGNOSTIC_SALT, - HydrologyCaveRejection.OVERLAPPING_SOURCE.ordinal(), - winnerSourceId - ), - kind, - type, - representative.start(), - HydrologyCandidateRejection.CAVE_CONTAINMENT, 0 - ); - } - - record Result( - List nodes, - List edges, - List outlets, - List courses, - List cavePlans - ) { - } - - record Options( - boolean connectToExistingCaves, - int coastalGrottoMaximumVolume, - int inlandGrottoMaximumVolume - ) { - Options { - if (coastalGrottoMaximumVolume < 1 || inlandGrottoMaximumVolume < 1) { - throw new IllegalArgumentException("Grotto maximum volumes must be positive."); - } - } - - int maximumVolume(HydrologyFeatureType type) { - return switch (type) { - case COASTAL_GROTTO -> coastalGrottoMaximumVolume; - case INLAND_GROTTO -> inlandGrottoMaximumVolume; - default -> Integer.MAX_VALUE; - }; - } - } - - record CandidateKey( - long courseId, - RiverCourseType courseType, - String profileKey, - List segments, - Options options - ) { - CandidateKey { - Objects.requireNonNull(courseType, "courseType"); - if (profileKey == null || profileKey.isBlank()) { - throw new IllegalArgumentException("profileKey must not be blank"); - } - profileKey = profileKey.trim(); - segments = List.copyOf(Objects.requireNonNull(segments, "segments")); - Objects.requireNonNull(options, "options"); - } - - private static CandidateKey create(RiverCourse course, Options options) { - return new CandidateKey( - course.id(), - course.type(), - course.profileKey(), - course.segments(), - options - ); - } - } - - static final class CandidateCache extends LinkedHashMap { - private static final int DEFAULT_MAXIMUM_ENTRIES = 256; - private static final long DEFAULT_MAXIMUM_RETAINED_POSITIONS = 262_144L; - - private final int maximumEntries; - private final long maximumRetainedPositions; - private long retainedPositions; - - CandidateCache() { - this(DEFAULT_MAXIMUM_ENTRIES, DEFAULT_MAXIMUM_RETAINED_POSITIONS); - } - - CandidateCache(int maximumEntries, long maximumRetainedPositions) { - super(16, 0.75F, true); - if (maximumEntries < 1) { - throw new IllegalArgumentException("Maximum candidate cache entries must be positive."); - } - if (maximumRetainedPositions < 1L) { - throw new IllegalArgumentException("Maximum retained candidate positions must be positive."); - } - this.maximumEntries = maximumEntries; - this.maximumRetainedPositions = maximumRetainedPositions; - } - - @Override - public HydrologyCaveCandidate put(CandidateKey key, HydrologyCaveCandidate candidate) { - Objects.requireNonNull(key); - Objects.requireNonNull(candidate); - HydrologyCaveCandidate previous = super.remove(key); - if (previous != null) { - retainedPositions -= weight(previous); - } - long weight = weight(candidate); - if (weight > maximumRetainedPositions) { - return previous; - } - super.put(key, candidate); - retainedPositions += weight; - trim(); - return previous; - } - - @Override - public HydrologyCaveCandidate remove(Object key) { - HydrologyCaveCandidate removed = super.remove(key); - if (removed != null) { - retainedPositions -= weight(removed); - } - return removed; - } - - @Override - public void putAll(Map candidates) { - for (Map.Entry entry - : candidates.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } - - @Override - public void clear() { - super.clear(); - retainedPositions = 0L; - } - - long retainedPositions() { - return retainedPositions; - } - - private void trim() { - Iterator> iterator = entrySet().iterator(); - while (iterator.hasNext() - && (size() > maximumEntries || retainedPositions > maximumRetainedPositions)) { - Map.Entry eldest = iterator.next(); - retainedPositions -= weight(eldest.getValue()); - iterator.remove(); - } - } - - private static long weight(HydrologyCaveCandidate candidate) { - return Math.max(1L, (long) candidate.actions().size() + candidate.intentionalOpenings().size()); - } - } - - static int horizontalRadius( - int entryX, - int entryZ, - int minimumX, - int maximumX, - int minimumZ, - int maximumZ - ) { - long maximumDistanceSquared = 1L; - long[][] corners = { - {minimumX, minimumZ}, - {minimumX, maximumZ}, - {maximumX, minimumZ}, - {maximumX, maximumZ} - }; - for (long[] corner : corners) { - long deltaX = corner[0] - entryX; - long deltaZ = corner[1] - entryZ; - maximumDistanceSquared = Math.max(maximumDistanceSquared, deltaX * deltaX + deltaZ * deltaZ); - } - return Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(maximumDistanceSquared)) + 2); - } - - static List surfaceOpenings(RiverCourse course) { - if (course.type() != RiverCourseType.SURFACE) { - return List.of(); - } - ArrayList openings = new ArrayList<>(); - List segments = course.segments(); - for (int index = 0; index < segments.size(); index++) { - HydraulicSegment segment = segments.get(index); - if (segment.type().isSurface() && segment.fallingFluid()) { - int radius = Math.max(1, segment.width() / 2) + 1; - HydraulicSegment receiving = index + 1 < segments.size() ? segments.get(index + 1) : null; - boolean coastal = receiving != null && receiving.type() == HydrologyFeatureType.MOUTH - && receiving.upstreamHeadY() == segment.downstreamHeadY() - && receiving.start().equals(segment.end()); - openings.add(CaveSurfaceOpening.create(segment, true, (long) radius * radius, - segment.downstreamHeadY() + (coastal ? 0 : 2), true)); - continue; - } - if (!segment.type().isUnderground()) { - continue; - } - int radius = Math.max(1, segment.width() / 2) + 1; - if (segment.type() == HydrologyFeatureType.SINKHOLE) { - HydrologyPoint start = segment.start(); - double maximumDistance = 0D; - for (HydrologyPoint point : segment.centerline()) { - maximumDistance = Math.max( - maximumDistance, - StrictMath.hypot(point.x() - start.x(), point.z() - start.z()) - ); - } - radius += (int) StrictMath.ceil(maximumDistance); - } - long radiusSquared = (long) radius * radius; - int minimumY = segment.type() == HydrologyFeatureType.SINKHOLE - ? segment.downstreamHeadY() + 1 - : Integer.MIN_VALUE; - boolean includeNeighborhood = segment.type() != HydrologyFeatureType.SINKHOLE; - if (index == 0 - || !segments.get(index - 1).type().isUnderground()) { - openings.add(CaveSurfaceOpening.create( - segment, - true, - radiusSquared, - minimumY, - includeNeighborhood - )); - } - boolean terminalCoastalOpening = index == segments.size() - 1 - && segment.type() == HydrologyFeatureType.COASTAL_GROTTO; - if (terminalCoastalOpening - || (index < segments.size() - 1 - && !segments.get(index + 1).type().isUnderground())) { - openings.add(CaveSurfaceOpening.create( - segment, - false, - radiusSquared, - minimumY, - includeNeighborhood - )); - } - } - return List.copyOf(openings); - } - - private record TerminalActions( - LinkedHashMap first, - LinkedHashMap second - ) { - } - - private record GeneratedChannelView(CaveVoxelView terrain) implements CaveVoxelView { - @Override - public boolean isInWorld(CavePosition position) { - return terrain.isInWorld(position); - } - - @Override - public CaveVoxel voxelAt(CavePosition position) { - return CaveVoxel.UNCONDITIONAL; - } - - @Override - public boolean isOpenToSurface(CavePosition position) { - return false; - } - - @Override - public boolean isAboveTerrainSurface(CavePosition position) { - return terrain.isAboveTerrainSurface(position); - } - - @Override - public boolean hasAboveTerrainSurface(int x, int z, int minimumY, int maximumY) { - return terrain.hasAboveTerrainSurface(x, z, minimumY, maximumY); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCaveVoxelViewFactory.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCaveVoxelViewFactory.java deleted file mode 100644 index 9978d752e..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCaveVoxelViewFactory.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyChannelGeometry.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyChannelGeometry.java deleted file mode 100644 index f30bdbd62..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyChannelGeometry.java +++ /dev/null @@ -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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyChunkOccupancy.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyChunkOccupancy.java deleted file mode 100644 index 685b3aa2b..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyChunkOccupancy.java +++ /dev/null @@ -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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnLayer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnLayer.java deleted file mode 100644 index 8ffed0947..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnLayer.java +++ /dev/null @@ -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(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnSample.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnSample.java deleted file mode 100644 index c502045de..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnSample.java +++ /dev/null @@ -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 layers -) { - private static final Comparator 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 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 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 primaryLayer() { - return layers.isEmpty() ? Optional.empty() : Optional.of(layers.getFirst()); - } - - public Optional primarySurfaceLayer() { - return Optional.ofNullable(selectSurfaceLayer(false)); - } - - public Optional primarySurfaceFluidLayer() { - return Optional.ofNullable(selectSurfaceLayer(true)); - } - - public HydrologyColumnLayer primarySurfaceLayerOrNull() { - return selectSurfaceLayer(false); - } - - public HydrologyColumnLayer primarySurfaceFluidLayerOrNull() { - return selectSurfaceLayer(true); - } - - public Optional 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 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."); - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnSnapshot.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnSnapshot.java deleted file mode 100644 index 031007362..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyColumnSnapshot.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCoursePath.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCoursePath.java deleted file mode 100644 index 3b1072111..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCoursePath.java +++ /dev/null @@ -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 points, - List pairEdges, - List edges, - RiverOutlet outlet, - boolean reachesOutlet, - boolean organicSurfaceRequired, - HydrologyPoint junction -) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileCaveAdmission.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileCaveAdmission.java deleted file mode 100644 index 86c1b9182..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileCaveAdmission.java +++ /dev/null @@ -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_PRIORITY = Comparator - .comparing((Claim claim) -> claim.plan().source(), HydrologyCaveConflictPolicy.sourcePriority()) - .thenComparing(Claim::profileKey); - private static final Comparator BLOCKER_PRIORITY = Comparator - .comparingInt(RankedClaim::ownerRank) - .thenComparing(RankedClaim::claim, CLAIM_PRIORITY) - .thenComparing(RankedClaim::ownerKey); - - private HydrologyCrossTileCaveAdmission() { - } - - static Result admit( - int currentOwnerRank, - Collection currentClaims, - Collection blockers - ) { - if (currentOwnerRank < 0) { - throw new IllegalArgumentException("currentOwnerRank cannot be negative"); - } - ArrayList orderedClaims = new ArrayList<>(Objects.requireNonNull(currentClaims, "currentClaims")); - orderedClaims.sort(CLAIM_PRIORITY); - ArrayList 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 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 rejections) { - Result { - rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections")); - } - - Map loserCourseIdsToWinnerSourceIds() { - TreeMap 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(); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileResolver.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileResolver.java deleted file mode 100644 index 90cf768d2..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileResolver.java +++ /dev/null @@ -1,1114 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.concurrent.MultiBurst; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.generation.hydrology.cave.CavePosition; -import art.arcane.iris.generation.hydrology.cave.CaveVoxelView; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveContainmentPlanner; -import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan; -import art.arcane.iris.generation.hydrology.surface.SurfaceBounds; -import it.unimi.dsi.fastutil.longs.LongIterator; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinWorkerThread; - -final class HydrologyCrossTileResolver { - private static final int FALLBACK_TRIAL_PARALLELISM = 16; - private static final int MINIMUM_FALLBACK_TRIAL_BATCH = 2; - private final HydrologyPlanner planner; - private final int trialLimit; - - HydrologyCrossTileResolver(HydrologyPlanner planner, int trialLimit) { - if (trialLimit < 1) { - throw new IllegalArgumentException("Hydrology fallback trial limit must be positive."); - } - this.planner = planner; - this.trialLimit = trialLimit; - } - - static int fallbackTrialBatch(int parallelism, int idleWorkers, int trialLimit) { - if (parallelism <= 2) { - return 1; - } - int workers = Math.clamp(idleWorkers + 1L, MINIMUM_FALLBACK_TRIAL_BATCH, parallelism); - return Math.max(1, Math.min(workers, Math.min(trialLimit, FALLBACK_TRIAL_PARALLELISM))); - } - - HydrologyOwnerDraft compileOwnerDraft( - HydrologyTileKey key, - CrossTileDraftAdmission crossTileAdmission, - boolean retainFootprintCompiler - ) { - HydrologyPlanner.PlanningSamples previous = planner.planningSamples.get(); - if (previous == null) { - planner.planningSamples.set(new HydrologyPlanner.PlanningSamples()); - } - DraftProfile previousProfile = planner.draftProfiles.get(); - DraftProfile profile = new DraftProfile(); - planner.draftProfiles.set(profile); - long started = System.nanoTime(); - try { - HydrologyOwnerDraft draft = compileScopedOwnerDraft( - key, - crossTileAdmission, - retainFootprintCompiler - ); - profile.log(key, ownerColorRank(key), System.nanoTime() - started); - return draft; - } finally { - if (previous == null) { - planner.planningSamples.remove(); - } - if (previousProfile == null) { - planner.draftProfiles.remove(); - } else { - planner.draftProfiles.set(previousProfile); - } - } - } - - HydrologyOwnerDraft compileScopedOwnerDraft( - HydrologyTileKey key, - CrossTileDraftAdmission crossTileAdmission, - boolean retainFootprintCompiler - ) { - Objects.requireNonNull(key, "key"); - ArrayList diagnostics = new ArrayList<>(); - HydrologySampledGrid grid = null; - HydrologyRoutingPlan surfaceRouting = null; - HydrologyRoutingPlan undergroundRouting = null; - SourceSelection surfaceSelection = SourceSelection.empty(true); - SourceSelection undergroundSelection = SourceSelection.empty(false); - DraftProfile profile = planner.currentDraftProfile(); - long phaseStarted = System.nanoTime(); - if (planner.sourcePlanner.hasRoutedSourceSearch()) { - SourceRoutingContext primaryContext = planner.sourceRoutingContext(key); - phaseStarted = profile.record(DraftPhase.CONTEXT, phaseStarted); - grid = primaryContext.grid(); - surfaceRouting = primaryContext.surfaceRouting(); - undergroundRouting = primaryContext.undergroundRouting(); - diagnostics.addAll(primaryContext.diagnostics()); - HashMap sourceRoutingContexts = new HashMap<>(); - sourceRoutingContexts.put(key, primaryContext); - surfaceSelection = planner.sourcePlanner.selectSources( - key, - grid, - surfaceRouting, - true, - true, - diagnostics, - sourceRoutingContexts - ); - undergroundSelection = planner.sourcePlanner.selectSources( - key, - grid, - undergroundRouting, - false, - true, - diagnostics, - sourceRoutingContexts - ); - phaseStarted = profile.record(DraftPhase.SELECT, phaseStarted); - if (crossTileAdmission != null && (!surfaceSelection.selectedCandidateIndices.isEmpty() - || !undergroundSelection.selectedCandidateIndices.isEmpty())) { - crossTileAdmission.prepare(); - } - } - HydrologyFootprintCompiler footprintCompiler = new HydrologyFootprintCompiler( - planner.settings, - new HydrologyFootprintCompiler.Sampling( - planner.sampler, - planner.geometrySampler, - planner.naturalSampler - ) - ); - seedRegionalSurface(key, footprintCompiler); - HashMap> refinedEdges = new HashMap<>(); - HashMap sourceCompilations = new HashMap<>(); - settleSelection( - key, - grid, - surfaceRouting, - undergroundRouting, - surfaceSelection, - true, - footprintCompiler, - refinedEdges, - sourceCompilations, - diagnostics - ); - settleSelection( - key, - grid, - surfaceRouting, - undergroundRouting, - undergroundSelection, - false, - footprintCompiler, - refinedEdges, - sourceCompilations, - diagnostics - ); - phaseStarted = profile.record(DraftPhase.SETTLE, phaseStarted); - PublicationAttempt publication = null; - List fallbackOutlets = grid == null || surfaceRouting == null - ? List.of() - : planner.outletPlanner.resolveSurfaceFallbackOutlets(grid); - int fallbackOutletIndex = 0; - int fallbackBatchStart = 0; - List fallbackTrials = List.of(); - while (true) { - int maximumPublicationPasses = Math.addExact( - 1, - Math.addExact( - surfaceSelection.candidateCount(), - undergroundSelection.candidateCount() - ) - ); - for (int publicationPass = 0; publicationPass < maximumPublicationPasses; publicationPass++) { - publication = compilePublication( - key, - grid, - surfaceRouting, - undergroundRouting, - surfaceSelection, - undergroundSelection, - footprintCompiler, - refinedEdges, - sourceCompilations, - true - ); - diagnostics.addAll(publication.diagnostics()); - if (crossTileAdmission != null) { - CrossTilePublicationAdmission admitted = crossTileAdmission.admit(publication.result()); - publication = new PublicationAttempt(admitted.result(), publication.diagnostics()); - diagnostics.addAll(admitted.diagnostics()); - } - if (grid == null) { - break; - } - boolean surfaceChanged = surfaceSelection.advanceAfterPublication( - publication.result(), - grid - ); - boolean undergroundChanged = undergroundSelection.advanceAfterPublication( - publication.result(), - grid - ); - if (!surfaceChanged && !undergroundChanged) { - break; - } - if (surfaceChanged) { - settleSelection( - key, - grid, - surfaceRouting, - undergroundRouting, - surfaceSelection, - true, - footprintCompiler, - refinedEdges, - sourceCompilations, - diagnostics - ); - } - if (undergroundChanged) { - settleSelection( - key, - grid, - surfaceRouting, - undergroundRouting, - undergroundSelection, - false, - footprintCompiler, - refinedEdges, - sourceCompilations, - diagnostics - ); - } - if (publicationPass + 1 >= maximumPublicationPasses) { - throw new IllegalStateException("Hydrology source publication exceeded its candidate bound."); - } - } - if (grid == null || !surfaceSelection.needsSurfaceFallback()) { - break; - } - boolean fallbackSelected = false; - while (fallbackOutletIndex < fallbackOutlets.size()) { - if (fallbackOutletIndex >= fallbackBatchStart + fallbackTrials.size()) { - fallbackBatchStart = fallbackOutletIndex; - fallbackTrials = compileFallbackTrials(new FallbackContext( - key, grid, undergroundRouting, footprintCompiler, refinedEdges, sourceCompilations - ), fallbackOutlets, fallbackBatchStart); - } - FallbackTrial trial = fallbackTrials.get(fallbackOutletIndex++ - fallbackBatchStart); - if (trial.failure() != null) { - throw HydrologyPlanner.propagateOwnerFailure(trial.failure()); - } - refinedEdges.putAll(trial.refinedEdges()); - sourceCompilations.putAll(trial.sourceCompilations()); - if (!trial.selection().hasAcceptedSelection()) { - continue; - } - // Every trial re-selects over the whole owned lattice, so a discarded one invents a full set of - // per-node rejections for a plan that was never published. Only the adopted trial may report. - diagnostics.addAll(trial.diagnostics()); - surfaceRouting = trial.routing(); - surfaceSelection = trial.selection(); - footprintCompiler = trial.footprintCompiler(); - fallbackSelected = true; - break; - } - if (!fallbackSelected) { - break; - } - } - profile.record(DraftPhase.PUBLISH, phaseStarted); - if (publication == null) { - throw new IllegalStateException("Hydrology publication did not execute."); - } - if (grid != null) { - surfaceSelection.addFinalAdmissionDiagnostics(grid, diagnostics); - undergroundSelection.addFinalAdmissionDiagnostics(grid, diagnostics); - } - HydrologyCaveCourseFilter.Result result = publication.result(); - List uniqueDiagnostics = planner.sourcePlanner.uniqueDiagnostics(diagnostics); - footprintCompiler.sharedBasis = null; - HydrologyOwnerDraft draft = new HydrologyOwnerDraft( - key, - result, - uniqueDiagnostics, - retainFootprintCompiler ? footprintCompiler : null - ); - validateOwnerDraftReach(draft); - return draft; - } - - private List compileFallbackTrials( - FallbackContext context, - List outlets, - int start - ) { - int parallelism; - int idleWorkers; - if (Thread.currentThread() instanceof ForkJoinWorkerThread worker) { - ForkJoinPool pool = worker.getPool(); - parallelism = HydrologyPlanningAdmission.effectiveParallelism(pool.getParallelism()); - idleWorkers = pool.getParallelism() - pool.getActiveThreadCount(); - } else { - parallelism = HydrologyPlanningAdmission.effectiveParallelism( - IrisPlatforms.isBound() ? MultiBurst.hydrology.parallelism() : 1); - idleWorkers = 0; - } - int batchSize = fallbackTrialBatch(parallelism, idleWorkers, trialLimit); - int end = Math.min(outlets.size(), start + batchSize); - ArrayList> tasks = new ArrayList<>(end - start); - HydrologyPlanner.PlanningSamples ownerSamples = planner.planningSamples.get(); - HydrologyLandHeightCache sharedLandHeights = planner.naturalSampler == null ? null - : ownerSamples == null ? new HydrologyLandHeightCache() : ownerSamples.fallbackLandHeights(); - HydrologyBasisCache sharedBasis = null; - if (planner.naturalSampler != null) { - sharedBasis = ownerSamples == null ? new HydrologyBasisCache() - : ownerSamples.fallbackBasis(context.footprintCompiler().terrainBases); - } - FallbackSamples shared = new FallbackSamples(sharedLandHeights, sharedBasis); - for (int index = start; index < end; index++) { - OutletCandidate outlet = outlets.get(index); - tasks.add(() -> compileFallbackTrial(context, outlet, shared)); - } - List trials = HydrologyForkJoin.invokeAll(tasks, - IrisPlatforms.isBound() ? MultiBurst.hydrology : null); - DraftProfile ownerProfile = planner.currentDraftProfile(); - for (FallbackTrial trial : trials) { - DraftProfile trialProfile = trial.profile(); - ownerProfile.routeCalls += trialProfile.routeCalls; - ownerProfile.routeSolves += trialProfile.routeSolves; - ownerProfile.routeSolveNanos += trialProfile.routeSolveNanos; - ownerProfile.rasterCalls += trialProfile.rasterCalls; - ownerProfile.rasterNanos += trialProfile.rasterNanos; - ownerProfile.filterCalls += trialProfile.filterCalls; - ownerProfile.filterNanos += trialProfile.filterNanos; - } - return trials; - } - - private FallbackTrial compileFallbackTrial( - FallbackContext context, - OutletCandidate outlet, - FallbackSamples shared - ) { - HydrologyPlanner.PlanningSamples previousSamples = planner.planningSamples.get(); - DraftProfile previousProfile = planner.draftProfiles.get(); - DraftProfile profile = new DraftProfile(); - ArrayList diagnostics = new ArrayList<>(); - HashMap> refinedEdges = new HashMap<>(context.refinedEdges()); - HashMap sourceCompilations = new HashMap<>(context.sourceCompilations()); - HydrologyFootprintCompiler footprints = null; - try { - planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(shared.landHeights(), shared.basis())); - planner.draftProfiles.set(profile); - footprints = new HydrologyFootprintCompiler( - planner.settings, - new HydrologyFootprintCompiler.Sampling(planner.sampler, planner.geometrySampler, planner.naturalSampler) - ); - footprints.sharedBasis = shared.basis(); - footprints.regionalNetwork = context.footprintCompiler().regionalNetwork; - footprints.regionalSurfaceFootprints.putAll(context.footprintCompiler().regionalSurfaceFootprints); - footprints.regionalDropRasters.putAll(context.footprintCompiler().regionalDropRasters); - HydrologyRoutingPlan routing = planner.sourcePlanner.requireOrganicSurface( - planner.sourcePlanner.buildRouting(context.grid(), List.of(outlet), true) - ); - HashMap routingContexts = new HashMap<>(); - routingContexts.put(context.key(), new SourceRoutingContext( - context.grid(), routing, context.undergroundRouting(), List.of() - )); - SourceSelection selection = planner.sourcePlanner.selectSources( - context.key(), context.grid(), routing, true, false, diagnostics, routingContexts - ); - settleSelection(context.key(), context.grid(), routing, context.undergroundRouting(), selection, - true, footprints, refinedEdges, sourceCompilations, diagnostics); - return new FallbackTrial(routing, selection, List.copyOf(diagnostics), footprints, - refinedEdges, sourceCompilations, profile, null); - } catch (Throwable failure) { - return new FallbackTrial(null, null, List.copyOf(diagnostics), footprints, - refinedEdges, sourceCompilations, profile, failure); - } finally { - if (previousSamples == null) { - planner.planningSamples.remove(); - } else { - planner.planningSamples.set(previousSamples); - } - if (previousProfile == null) { - planner.draftProfiles.remove(); - } else { - planner.draftProfiles.set(previousProfile); - } - } - } - - private record FallbackSamples(HydrologyLandHeightCache landHeights, HydrologyBasisCache basis) { - } - - private record FallbackContext( - HydrologyTileKey key, - HydrologySampledGrid grid, - HydrologyRoutingPlan undergroundRouting, - HydrologyFootprintCompiler footprintCompiler, - Map> refinedEdges, - Map sourceCompilations - ) { - } - - private record FallbackTrial( - HydrologyRoutingPlan routing, - SourceSelection selection, - List diagnostics, - HydrologyFootprintCompiler footprintCompiler, - Map> refinedEdges, - Map sourceCompilations, - DraftProfile profile, - Throwable failure - ) { - } - - HydrologyTile materializeAcceptedTile(CrossTileResolution resolution) { - HydrologyCaveCourseFilter.Result result = resolution.draft().result(); - ArrayList diagnostics = new ArrayList<>(resolution.draft().diagnostics()); - for (CrossTileRejectedCourse rejection : resolution.observedRejections()) { - diagnostics.add(rejection.course().type() == RiverCourseType.SURFACE - ? surfaceConflictDiagnostic(rejection.course(), rejection.winnerSourceId()) - : HydrologyCaveCourseFilter.overlapDiagnostic( - rejection.course(), - rejection.winnerSourceId() - )); - } - HydrologyFootprintCompiler footprintCompiler = resolution.draft().footprintCompiler(); - if (footprintCompiler == null) { - footprintCompiler = new HydrologyFootprintCompiler( - planner.settings, - new HydrologyFootprintCompiler.Sampling( - planner.sampler, - planner.geometrySampler, - planner.naturalSampler - ) - ); - seedRegionalSurface(resolution.draft().key(), footprintCompiler); - } - result = planner.regional.include(result, footprintCompiler.regionalNetwork); - MaterializedHydrology materialized = materializeFinalHydrology( - result, - diagnostics, - footprintCompiler, - !resolution.observedRejections().isEmpty() - ); - result = materialized.result(); - result = clipRegionalPlans(result, footprintCompiler.regionalNetwork, - planner.regional.ownerBounds(resolution.draft().key())); - RiverFootprint footprint = materialized.footprint(); - if (footprintCompiler.fullMaterializationCount() < 1) { - throw new IllegalStateException("Hydrology publication did not materialize its full footprint."); - } - HashSet regionalIds = new HashSet<>(); - for (RiverCourse course : footprintCompiler.regionalNetwork.courses()) { - regionalIds.add(course.id()); - } - HashSet publishedRegionalIds = new HashSet<>(); - for (RiverCourse course : result.courses()) { - if (regionalIds.contains(course.id())) { - publishedRegionalIds.add(course.id()); - } - } - return new HydrologyTile( - resolution.draft().key(), - planner.worldSeed, - planner.settings.fingerprint(), - planner.settings.routing().tileSize(), - result.nodes(), - result.edges(), - result.outlets(), - result.courses(), - publishedRegionalIds, - result.cavePlans(), - planner.sourcePlanner.uniqueDiagnostics(diagnostics), - footprint - ); - } - - private void seedRegionalSurface(HydrologyTileKey key, HydrologyFootprintCompiler compiler) { - if (!planner.regional.enabled()) { - return; - } - SurfaceBounds bounds = planner.regional.ownerBounds(key); - compiler.seedRegionalSurface(planner.regional.coursesIn(bounds), bounds); - } - - static HydrologyCaveCourseFilter.Result clipRegionalPlans(HydrologyCaveCourseFilter.Result result, - HydrologyRegionalNetwork regional, SurfaceBounds bounds) { - if (regional.cavePlans().isEmpty()) { - return result; - } - HashSet regionalIds = new HashSet<>(); - for (RiverCourse course : regional.courses()) { - regionalIds.add(course.id()); - } - ArrayList plans = new ArrayList<>(result.cavePlans().size()); - for (HydrologyCavePlan plan : result.cavePlans()) { - plans.add(regionalIds.contains(plan.source().sourceId()) ? HydrologyRegionalFalls.clip(plan, bounds) : plan); - } - return new HydrologyCaveCourseFilter.Result(result.nodes(), result.edges(), result.outlets(), result.courses(), List.copyOf(plans)); - } - - MaterializedHydrology materializeFinalHydrology( - HydrologyCaveCourseFilter.Result initial, - List diagnostics, - HydrologyFootprintCompiler footprintCompiler, - boolean revalidationRequired - ) { - HydrologyCaveCourseFilter.Result current = initial; - HashSet regionalIds = new HashSet<>(); - for (RiverCourse course : footprintCompiler.regionalNetwork.courses()) { - regionalIds.add(course.id()); - } - int maximumPasses = Math.addExact(initial.courses().size(), 1); - for (int pass = 0; pass < maximumPasses; pass++) { - RiverFootprint footprint = footprintCompiler.compile(current.courses()); - HydrologyCaveCourseFilter.Result local = HydrologyCaveCourseFilter.withoutCourses(current, regionalIds); - if (local.cavePlans().isEmpty()) { - return new MaterializedHydrology(current, footprint); - } - HydrologyFootprintCompiler.ValidationRaster compactValidation = footprintCompiler.compileValidation( - current.courses() - ); - if (!revalidationRequired - && compactSurfaceMatchesMaterializedPlans(local, compactValidation, footprint)) { - return new MaterializedHydrology(current, footprint); - } - HydrologyFootprintCompiler.ValidationRaster validation = compactValidation.withMaterializedSurface( - footprint - ); - HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface( - validation.plannedSurface() - ); - CaveVoxelView caveView = Objects.requireNonNull( - planner.caveViewFactory.create(plannedSurface), - "Hydrology cave view factory returned null" - ); - HydrologyCaveCourseFilter.Result revalidated = new HydrologyCaveCourseFilter( - caveView, - new HydrologyCaveCourseFilter.Options( - planner.settings.underground().connectToExistingCaves(), - planner.settings.outlets().coastalGrotto().maximumVolume(), - planner.settings.outlets().inlandGrotto().maximumVolume() - ), - null, - null, - plannedSurface - ).filter( - local.nodes(), - local.edges(), - local.outlets(), - local.courses(), - validation, - diagnostics - ); - revalidated = HydrologyRegionalProtection.rejectPlans(revalidated, footprintCompiler.regionalNetwork, diagnostics); - revalidated = planner.regional.include(revalidated, footprintCompiler.regionalNetwork); - if (revalidated.courses().equals(current.courses())) { - return new MaterializedHydrology(revalidated, footprint); - } - current = revalidated; - revalidationRequired = true; - } - throw new IllegalStateException("Final hydrology cave publication exceeded its course bound."); - } - - boolean compactSurfaceMatchesMaterializedPlans( - HydrologyCaveCourseFilter.Result result, - HydrologyFootprintCompiler.ValidationRaster validation, - RiverFootprint footprint - ) { - HashSet visitedColumns = new HashSet<>(); - for (HydrologyCavePlan plan : result.cavePlans()) { - for (CavePosition position : plan.baselinePreconditions().keySet()) { - long packed = RiverFootprint.pack(position.x(), position.z()); - if (!visitedColumns.add(packed)) { - continue; - } - HydrologyColumnSample sample = footprint.sample(position.x(), position.z()).orElse(null); - HydrologyTerrainSample terrain = sample == null - ? Objects.requireNonNull(planner.sampler.sample(position.x(), position.z())) - : null; - int naturalHeight = sample == null ? terrain.naturalHeight() : sample.naturalHeight(); - int materializedHeight = sample == null ? naturalHeight : sample.terrainHeight(); - int compactHeight = validation.plannedSurface().resolve( - position.x(), - position.z(), - naturalHeight - ); - if (compactHeight != materializedHeight) { - return false; - } - } - } - return true; - } - - CrossTileBlockers lowerRankBlockers( - HydrologyTileKey ownerKey, - HydrologyCaveCourseFilter.Result result, - int ownerRank, - CrossTileResolutionContext context, - Map> preparedOwners - ) { - ArrayList blockers = new ArrayList<>(); - ArrayList surfaceBlockers = new ArrayList<>(); - ArrayList candidateKeys = new ArrayList<>(); - for (HydrologyTileKey candidateKey : conflictOwnerKeys(ownerKey, result)) { - if (candidateKey.equals(ownerKey)) { - continue; - } - int candidateRank = ownerColorRank(candidateKey); - if (candidateRank == ownerRank) { - throw new IllegalStateException("Equal owner colors cannot share a cave-action conflict envelope."); - } - if (candidateRank > ownerRank) { - continue; - } - candidateKeys.add(candidateKey); - } - long dependenciesStarted = System.nanoTime(); - List resolvedCandidates = planner.resolveLowerRankOwners(candidateKeys, context, preparedOwners); - planner.currentDraftProfile().recordDependencies(candidateKeys.size(), dependenciesStarted); - for (int index = 0; index < candidateKeys.size(); index++) { - HydrologyTileKey candidateKey = candidateKeys.get(index); - int candidateRank = ownerColorRank(candidateKey); - CrossTileResolvedOwner resolved = resolvedCandidates.get(index); - for (HydrologyCrossTileCaveAdmission.Claim claim : caveClaims(resolved.draft())) { - blockers.add(new HydrologyCrossTileCaveAdmission.RankedClaim( - candidateKey, - candidateRank, - claim - )); - } - for (HydrologyCrossTileSurfaceAdmission.Claim claim : surfaceClaims(resolved.draft())) { - surfaceBlockers.add(new HydrologyCrossTileSurfaceAdmission.RankedClaim( - candidateKey, - candidateRank, - claim - )); - } - } - return new CrossTileBlockers(List.copyOf(blockers), List.copyOf(surfaceBlockers)); - } - - List conflictOwnerKeys(HydrologyOwnerDraft target) { - return conflictOwnerKeys(target.key(), target.result()); - } - - List conflictOwnerKeys( - HydrologyTileKey ownerKey, - HydrologyCaveCourseFilter.Result result - ) { - int tileSize = planner.settings.routing().tileSize(); - int reach = Math.addExact(planner.settings.publicationRadius(), 1); - int colorPeriod = planner.settings.crossTileColorPeriod(); - LongOpenHashSet columns = new LongOpenHashSet(); - for (HydrologyCavePlan plan : result.cavePlans()) { - for (CavePosition position : plan.actions().keySet()) { - columns.add(RiverFootprint.pack(position.x(), position.z())); - } - } - for (RiverCourse course : result.courses()) { - if (course.type() != RiverCourseType.SURFACE) { - continue; - } - for (HydraulicSegment segment : course.segments()) { - for (HydrologyPoint point : segment.centerline()) { - columns.add(RiverFootprint.pack(point.x(), point.z())); - } - } - } - LinkedHashSet owners = new LinkedHashSet<>(); - owners.add(ownerKey); - LongIterator iterator = columns.iterator(); - while (iterator.hasNext()) { - long column = iterator.nextLong(); - int x = RiverFootprint.unpackX(column); - int z = RiverFootprint.unpackZ(column); - int minimumTileX = ownerCoordinate((long) x - reach, tileSize); - int maximumTileX = ownerCoordinate((long) x + reach, tileSize); - int minimumTileZ = ownerCoordinate((long) z - reach, tileSize); - int maximumTileZ = ownerCoordinate((long) z + reach, tileSize); - long ownerWidth = Math.addExact((long) maximumTileX - minimumTileX, 1L); - long ownerDepth = Math.addExact((long) maximumTileZ - minimumTileZ, 1L); - if (ownerWidth > colorPeriod || ownerDepth > colorPeriod) { - throw new IllegalStateException("A cave action exceeds its bounded owner color neighborhood."); - } - for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) { - for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) { - owners.add(new HydrologyTileKey(tileX, tileZ)); - } - } - } - int sourceReach = Math.max(0, planner.settings.maximumSurfaceSourceSpacing() - 1); - for (HydrologyCrossTileSurfaceAdmission.Claim claim : surfaceClaims(result)) { - HydrologyPoint source = claim.centerline().getFirst(); - int minimumTileX = ownerCoordinate((long) source.x() - sourceReach, tileSize); - int maximumTileX = ownerCoordinate((long) source.x() + sourceReach, tileSize); - int minimumTileZ = ownerCoordinate((long) source.z() - sourceReach, tileSize); - int maximumTileZ = ownerCoordinate((long) source.z() + sourceReach, tileSize); - for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) { - for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) { - owners.add(new HydrologyTileKey(tileX, tileZ)); - } - } - } - ArrayList ordered = new ArrayList<>(owners); - ordered.sort(HydrologyTileKey::compareTo); - return List.copyOf(ordered); - } - - int ownerColorRank(HydrologyTileKey key) { - int colorPeriod = planner.settings.crossTileColorPeriod(); - int colorX = Math.floorMod(key.tileX(), colorPeriod); - int colorZ = Math.floorMod(key.tileZ(), colorPeriod); - return Math.addExact(Math.multiplyExact(colorZ, colorPeriod), colorX); - } - - List caveClaims(HydrologyOwnerDraft draft) { - return caveClaims(draft.result()); - } - - List caveClaims( - HydrologyCaveCourseFilter.Result result - ) { - if (result.cavePlans().isEmpty()) { - return List.of(); - } - Map coursesById = coursesById(result.courses()); - ArrayList claims = - new ArrayList<>(result.cavePlans().size()); - for (HydrologyCavePlan plan : result.cavePlans()) { - RiverCourse course = coursesById.get(plan.source().sourceId()); - if (course == null) { - throw new IllegalStateException("Accepted cave plan has no owner course."); - } - claims.add(new HydrologyCrossTileCaveAdmission.Claim(course.profileKey(), plan)); - } - return List.copyOf(claims); - } - - List surfaceClaims(HydrologyOwnerDraft draft) { - return surfaceClaims(draft.result()); - } - - List surfaceClaims( - HydrologyCaveCourseFilter.Result result - ) { - ArrayList claims = new ArrayList<>(); - for (RiverCourse course : result.courses()) { - if (course.type() != RiverCourseType.SURFACE || course.outletId().isEmpty()) { - continue; - } - ArrayList centerline = new ArrayList<>(); - int maximumWidth = 1; - for (HydraulicSegment segment : course.segments()) { - maximumWidth = Math.max(maximumWidth, segment.width()); - for (HydrologyPoint point : segment.centerline()) { - if (centerline.isEmpty() - || point.x() != centerline.getLast().x() - || point.z() != centerline.getLast().z()) { - centerline.add(point); - } - } - } - if (centerline.size() < 2) { - continue; - } - HydrologyFeatureType terminalType = course.segments().getLast().type(); - boolean reachesOutlet = terminalType == HydrologyFeatureType.MOUTH - || terminalType == HydrologyFeatureType.COASTAL_GROTTO - || terminalType == HydrologyFeatureType.INLAND_GROTTO; - claims.add(new HydrologyCrossTileSurfaceAdmission.Claim( - course.id(), - course.outletId().getAsLong(), - centerline.getLast(), - reachesOutlet, - maximumWidth, - centerline, - sourceSpacing(course, result.nodes()) - )); - } - return List.copyOf(claims); - } - - private int sourceSpacing(RiverCourse course, List nodes) { - int fallback = planner.settings.surface().sources().minimumSpacing(); - if (course.sourceNodeId().isPresent()) { - long sourceId = course.sourceNodeId().getAsLong(); - for (DrainageNode node : nodes) { - if (node.id() == sourceId) { - return node.terrain().surfacePolicy().sourceSpacing(fallback); - } - } - } - return fallback; - } - - Map coursesById(List courses) { - LinkedHashMap indexed = new LinkedHashMap<>(courses.size()); - for (RiverCourse course : courses) { - RiverCourse existing = indexed.putIfAbsent(course.id(), course); - if (existing != null && !existing.equals(course)) { - throw new IllegalStateException("Hydrology course id collision."); - } - } - return Map.copyOf(indexed); - } - - void validateOwnerDraftReach(HydrologyOwnerDraft draft) { - int tileSize = planner.settings.routing().tileSize(); - int reach = Math.addExact(planner.settings.publicationRadius(), 1); - long minimumX = (long) draft.key().minimumBlockX(tileSize) - reach; - long minimumZ = (long) draft.key().minimumBlockZ(tileSize) - reach; - long maximumX = (long) draft.key().minimumBlockX(tileSize) + tileSize - 1L + reach; - long maximumZ = (long) draft.key().minimumBlockZ(tileSize) + tileSize - 1L + reach; - for (HydrologyCavePlan plan : draft.result().cavePlans()) { - for (CavePosition position : plan.actions().keySet()) { - if (position.x() < minimumX || position.x() > maximumX - || position.z() < minimumZ || position.z() > maximumZ) { - throw new IllegalStateException("Hydrology cave plan exceeds its bounded owner reach."); - } - } - } - } - - static int ownerCoordinate(long blockCoordinate, int tileSize) { - return Math.toIntExact(Math.floorDiv(blockCoordinate, tileSize)); - } - - void settleSelection( - HydrologyTileKey key, - HydrologySampledGrid grid, - HydrologyRoutingPlan surfaceRouting, - HydrologyRoutingPlan undergroundRouting, - SourceSelection selection, - boolean surface, - HydrologyFootprintCompiler footprintCompiler, - Map> refinedEdges, - Map sourceCompilations, - List diagnostics - ) { - HydrologyRoutingPlan selectedRouting = surface ? surfaceRouting : undergroundRouting; - if (grid == null - || selectedRouting == null - || selection.selectedCandidateIndices.isEmpty()) { - return; - } - SourceSelection emptySelection = SourceSelection.empty(!surface); - int maximumPasses = Math.addExact(1, selection.candidateCount()); - for (int pass = 0; pass < maximumPasses; pass++) { - PublicationAttempt attempt = compilePublication( - key, - grid, - surfaceRouting, - undergroundRouting, - surface ? selection : emptySelection, - surface ? emptySelection : selection, - footprintCompiler, - refinedEdges, - sourceCompilations, - false - ); - diagnostics.addAll(attempt.diagnostics()); - if (!selection.advanceAfterPublication(attempt.result(), grid)) { - return; - } - } - throw new IllegalStateException("Hydrology source trial exceeded its candidate bound."); - } - - PublicationAttempt compilePublication( - HydrologyTileKey key, - HydrologySampledGrid grid, - HydrologyRoutingPlan surfaceRouting, - HydrologyRoutingPlan undergroundRouting, - SourceSelection surfaceSelection, - SourceSelection undergroundSelection, - HydrologyFootprintCompiler footprintCompiler, - Map> refinedEdges, - Map sourceCompilations, - boolean includeDeepFluids - ) { - ArrayList diagnostics = new ArrayList<>(); - ArrayList courses = new ArrayList<>(); - CompiledGraph graph = new CompiledGraph(List.of(), List.of(), List.of(), Map.of()); - if (grid != null) { - List surfaceSources = surfaceSelection.selectedNodeIndices(); - List undergroundSources = undergroundSelection.selectedNodeIndices(); - if (surfaceRouting != null && surfaceRouting == undergroundRouting) { - int[] surfaceContributions = planner.sourcePlanner.countContributions(surfaceSources, surfaceRouting); - int[] undergroundContributions = planner.sourcePlanner.countContributions(undergroundSources, surfaceRouting); - graph = planner.sourcePlanner.compileGraph( - key, - grid, - surfaceRouting, - surfaceContributions, - undergroundContributions, - refinedEdges, - true - ); - if (planner.settings.surface().enabled()) { - planner.surfaceCourses.compileSurfaceCourses( - key, - grid, - surfaceRouting, - graph, - surfaceSources, - courses, - diagnostics - ); - } - if (planner.settings.underground().enabled()) { - planner.undergroundCourses.compileUndergroundCourses( - key, - grid, - surfaceRouting, - graph, - undergroundSources, - courses, - diagnostics - ); - } - } else { - CompiledGraph surfaceGraph = new CompiledGraph(List.of(), List.of(), List.of(), Map.of()); - CompiledGraph undergroundGraph = new CompiledGraph(List.of(), List.of(), List.of(), Map.of()); - if (surfaceRouting != null && !surfaceSources.isEmpty()) { - SourceCompilation surfaceCompilation = compileSourceSelection( - true, - key, - grid, - surfaceRouting, - surfaceSources, - refinedEdges, - sourceCompilations - ); - surfaceGraph = surfaceCompilation.graph(); - courses.addAll(surfaceCompilation.courses()); - diagnostics.addAll(surfaceCompilation.diagnostics()); - } - if (undergroundRouting != null && !undergroundSources.isEmpty()) { - SourceCompilation undergroundCompilation = compileSourceSelection( - false, - key, - grid, - undergroundRouting, - undergroundSources, - refinedEdges, - sourceCompilations - ); - undergroundGraph = undergroundCompilation.graph(); - courses.addAll(undergroundCompilation.courses()); - diagnostics.addAll(undergroundCompilation.diagnostics()); - } - graph = planner.sourcePlanner.mergeGraphs(surfaceGraph, undergroundGraph); - } - } - graph = new HydrologyRegionalTributaries(planner).connect(graph, courses, footprintCompiler, diagnostics); - HydrologySurfaceProfiles.rejectExcludedWetFootprints(footprintCompiler, courses, diagnostics); - List normalizedTrunkCourses = planner.tributaries.normalizeSharedTrunks(courses, diagnostics); - List normalizedOutletCourses = planner.tributaries.normalizeOutletContinuations(normalizedTrunkCourses); - courses.clear(); - courses.addAll(normalizedOutletCourses); - if (includeDeepFluids) { - planner.featureSites.compileDeepFluidCourses(key, courses, diagnostics); - } - planner.featureSites.compileSurfacePools(key, courses, diagnostics); - if (includeDeepFluids && grid != null) { - planner.featureSites.compileSeaCaves(grid, courses, diagnostics); - } - HydrologyRegionalProtection.rejectFootprints(courses, footprintCompiler, diagnostics); - DraftProfile profile = planner.currentDraftProfile(); - long rasterStarted = System.nanoTime(); - HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(courses); - profile.rasterNanos += System.nanoTime() - rasterStarted; - profile.rasterCalls++; - HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface( - validation.plannedSurface() - ); - CaveVoxelView caveView = Objects.requireNonNull( - planner.caveViewFactory.create(plannedSurface), - "Hydrology cave view factory returned null" - ); - HydrologyPlanner.PlanningSamples samples = planner.planningSamples.get(); - if (samples != null && caveView instanceof HydrologyTerrainCaveVoxelView terrainCaveView) { - caveView = terrainCaveView.withSampleCache(samples.detailed); - } - Map candidateCache = - samples == null ? null : samples.caveCandidates; - HydrologyCaveContainmentPlanner.ValidationCache validationCache = - samples == null ? null : samples.caveValidations; - long filterStarted = System.nanoTime(); - HydrologyCaveCourseFilter.Result containment = new HydrologyCaveCourseFilter( - caveView, - new HydrologyCaveCourseFilter.Options( - planner.settings.underground().connectToExistingCaves(), - planner.settings.outlets().coastalGrotto().maximumVolume(), - planner.settings.outlets().inlandGrotto().maximumVolume() - ), - candidateCache, - validationCache, - plannedSurface - ).filter( - graph.nodes(), - graph.edges(), - graph.outlets(), - courses, - validation, - diagnostics - ); - profile.filterNanos += System.nanoTime() - filterStarted; - profile.filterCalls++; - containment = HydrologyRegionalProtection.rejectPlans(containment, footprintCompiler.regionalNetwork, diagnostics); - return new PublicationAttempt(containment, diagnostics); - } - - SourceCompilation compileSourceSelection( - boolean surface, - HydrologyTileKey key, - HydrologySampledGrid grid, - HydrologyRoutingPlan routing, - List sources, - Map> refinedEdges, - Map sourceCompilations - ) { - SourceCompilationKey compilationKey = new SourceCompilationKey(surface, routing, sources); - SourceCompilation cached = sourceCompilations.get(compilationKey); - if (cached != null) { - return cached; - } - int[] contributions = planner.sourcePlanner.countContributions(sources, routing); - CompiledGraph graph = planner.sourcePlanner.compileGraph( - key, - grid, - routing, - surface ? contributions : new int[contributions.length], - surface ? new int[contributions.length] : contributions, - refinedEdges, - surface - ); - ArrayList courses = new ArrayList<>(); - ArrayList diagnostics = new ArrayList<>(); - if (surface && planner.settings.surface().enabled()) { - planner.surfaceCourses.compileSurfaceCourses(key, grid, routing, graph, sources, courses, diagnostics); - } else if (!surface && planner.settings.underground().enabled()) { - planner.undergroundCourses.compileUndergroundCourses(key, grid, routing, graph, sources, courses, diagnostics); - } - SourceCompilation compiled = new SourceCompilation( - graph, - List.copyOf(courses), - List.copyOf(diagnostics) - ); - sourceCompilations.put(compilationKey, compiled); - return compiled; - } - - record SourceCompilationKey( - boolean surface, - HydrologyRoutingPlan routing, - List sources - ) { - SourceCompilationKey { - Objects.requireNonNull(routing, "routing"); - sources = List.copyOf(sources); - } - } - - record SourceCompilation( - CompiledGraph graph, - List courses, - List diagnostics - ) { - } - - record PublicationAttempt( - HydrologyCaveCourseFilter.Result result, - List diagnostics - ) { - } - - record MaterializedHydrology( - HydrologyCaveCourseFilter.Result result, - RiverFootprint footprint - ) { - MaterializedHydrology { - Objects.requireNonNull(result); - Objects.requireNonNull(footprint); - } - } - - HydrologyDiagnosticCandidate surfaceConflictDiagnostic( - RiverCourse course, - long winnerCourseId - ) { - HydraulicSegment representative = course.segments().getFirst(); - return new HydrologyDiagnosticCandidate( - HydrologyHash.mix(course.id(), HydrologySourcePlanner.DIAGNOSTIC_SALT, winnerCourseId), - HydrologyCandidateKind.SOURCE, - HydrologyFeatureType.SURFACE_POOL, - representative.start(), - HydrologyCandidateRejection.SOURCE_SPACING, 0 - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileSurfaceAdmission.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileSurfaceAdmission.java deleted file mode 100644 index 7057049ca..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyCrossTileSurfaceAdmission.java +++ /dev/null @@ -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 currentClaims, - List blockers - ) { - ArrayList 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 first, - List 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 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 rejections) { - Result { - rejections = List.copyOf(Objects.requireNonNull(rejections, "rejections")); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDiagnosticCandidate.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDiagnosticCandidate.java deleted file mode 100644 index 8426c5756..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDiagnosticCandidate.java +++ /dev/null @@ -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"); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDiagnosticRenderSample.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDiagnosticRenderSample.java deleted file mode 100644 index cac96f98d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDiagnosticRenderSample.java +++ /dev/null @@ -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 candidates -) { - private static final Comparator CANDIDATE_ORDER = Comparator - .comparing(HydrologyDiagnosticCandidate::kind) - .thenComparing(HydrologyDiagnosticCandidate::rejection) - .thenComparingLong(HydrologyDiagnosticCandidate::id); - - public HydrologyDiagnosticRenderSample { - Objects.requireNonNull(candidates, "candidates"); - ArrayList ordered = new ArrayList<>(candidates); - ordered.sort(CANDIDATE_ORDER); - candidates = List.copyOf(ordered); - } - - public boolean present() { - return !candidates.isEmpty(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDrainageGeometry.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDrainageGeometry.java deleted file mode 100644 index a6fb31b20..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDrainageGeometry.java +++ /dev/null @@ -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 anchors; - private final AtomicReferenceArray>> 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 edge(int index) { - DraftProfile profile = planner.currentDraftProfile(); - profile.routeCalls++; - CompletableFuture> future = edges.get(index); - if (future == null) { - CompletableFuture> owned = new CompletableFuture<>(); - CompletableFuture> existing = edges.compareAndExchange(index, null, owned); - if (existing == null) { - long started = System.nanoTime(); - profile.routeSolves++; - try { - List 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 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 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 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 outlets) { - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDrainageGraph.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDrainageGraph.java deleted file mode 100644 index ce37d76d9..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyDrainageGraph.java +++ /dev/null @@ -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]; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureQuery.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureQuery.java deleted file mode 100644 index b3132d0be..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureQuery.java +++ /dev/null @@ -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 types, String profileKey) { - private static final Set SURFACE = Set.of( - HydrologyFeatureType.SURFACE_POOL, - HydrologyFeatureType.RIFFLE, - HydrologyFeatureType.CASCADE, - HydrologyFeatureType.WATERFALL, - HydrologyFeatureType.MOUTH - ); - private static final Set UNDERGROUND = Set.of( - HydrologyFeatureType.UNDERGROUND_POOL, - HydrologyFeatureType.UNDERGROUND_DROP, - HydrologyFeatureType.SINKHOLE - ); - private static final Set GROTTOS = Set.of( - HydrologyFeatureType.COASTAL_GROTTO, - HydrologyFeatureType.INLAND_GROTTO - ); - private static final Set DEEP = Set.of( - HydrologyFeatureType.DEEP_POOL, - HydrologyFeatureType.DEEP_CHANNEL - ); - private static final Set PROFILED = Set.of( - HydrologyFeatureType.DEEP_POOL, - HydrologyFeatureType.DEEP_CHANNEL, - HydrologyFeatureType.STANDING_POOL - ); - private static final Map> 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 builtInTypes = BUILT_INS.get(key); - return builtInTypes == null - ? new HydrologyFeatureQuery(PROFILED, normalized) - : new HydrologyFeatureQuery(builtInTypes, null); - } - - public static List suggestions(Collection deepFluidIds) { - LinkedHashSet suggestions = new LinkedHashSet<>(BUILT_INS.keySet()); - TreeSet 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> builtIns() { - LinkedHashMap> 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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureRef.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureRef.java deleted file mode 100644 index a9ad1a0a5..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureRef.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureSitePlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureSitePlanner.java deleted file mode 100644 index 458707085..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureSitePlanner.java +++ /dev/null @@ -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 courses, - List 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 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 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 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 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 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 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 courses, - List 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 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 seaCaveSites(HydrologySampledGrid grid) { - int depth = planner.settings.seaCaves().depth(); - ArrayList 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 accepted, - List 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 centerline = planner.segments.line( - HydrologyPlanner.withY(site.inner(), seaLevel), - HydrologyPlanner.withY(site.boundary().oceanPoint(), seaLevel), - planner.settings.routing().refinementSpacing() - ); - ArrayList 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 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 courses, - List diagnostics - ) { - for (HydrologyPlannerSettings.DeepFluid deepFluid : planner.settings.deepFluids()) { - if (!deepFluid.enabled() || deepFluid.maximumPerTile() == 0) { - continue; - } - ArrayList 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 deepSites( - HydrologyTileKey key, - HydrologyPlannerSettings.DeepFluid deepFluid, - List 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 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 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 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 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 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) { - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureType.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureType.java deleted file mode 100644 index c8aa283e7..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFeatureType.java +++ /dev/null @@ -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; - }; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFootprintCompiler.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFootprintCompiler.java deleted file mode 100644 index ba9047cfa..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFootprintCompiler.java +++ /dev/null @@ -1,1386 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint; -import art.arcane.iris.generation.hydrology.surface.SurfaceBounds; -import art.arcane.iris.generation.hydrology.surface.SurfaceFootprintCompiler; -import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn; -import art.arcane.iris.generation.hydrology.surface.SurfaceNoise; -import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -final class HydrologyFootprintCompiler { - static final int FEATURE_ROLE_COUNT = 7; - static final int COURSE_FOOTPRINT_CACHE_SIZE = 32; - static final int VALIDATION_RASTER_CACHE_SIZE = 32; - - static final int[][] HORIZONTAL_NEIGHBORS = { - {1, 0}, {-1, 0}, {0, 1}, {0, -1} - } - -; - - final HydrologyPlannerSettings settings; - final HydrologyTerrainSampler sampler; - final HydrologyNaturalTerrainSampler naturalSampler; - final HydrologyGeometrySampler geometrySampler; - final Map deepFluids; - final Long2ObjectOpenHashMap terrainSamples; - final Long2ObjectOpenHashMap terrainBases; - final Long2ObjectOpenHashMap naturalClassifications; - final Map features; - final LinkedHashMap courseFootprints; - final LinkedHashMap validationCourseRasters; - final Map rasterStencils; - final SurfaceFootprintCompiler surfaceCompiler; - final LinkedHashMap surfaceFootprints; - final Map regionalSurfaceFootprints; - final Map regionalDropRasters; - HydrologyRegionalNetwork regionalNetwork; - HydrologyBasisCache sharedBasis; - - int fullMaterializationCount; - final HydrologyFootprintRasterizer rasterizer; - final HydrologyChannelGeometry channelGeometry; - - HydrologyFootprintCompiler( - HydrologyPlannerSettings settings, - HydrologyTerrainSampler sampler - ) { - this(settings, sampler, HydrologyGeometrySampler.deterministic(sampler)); - } - - HydrologyFootprintCompiler( - HydrologyPlannerSettings settings, - HydrologyTerrainSampler sampler, - HydrologyGeometrySampler geometrySampler - ) { - this(settings, new Sampling(sampler, geometrySampler, null)); - } - - HydrologyFootprintCompiler( - HydrologyPlannerSettings settings, - Sampling sampling - ) { - this.settings = settings; - this.sampler = sampling.sampler(); - this.naturalSampler = sampling.naturalSampler(); - this.geometrySampler = sampling.geometrySampler(); - HashMap indexed = new HashMap<>(); - for (HydrologyPlannerSettings.DeepFluid deepFluid : settings.deepFluids()) { - indexed.put(deepFluid.id(), deepFluid); - } - this.deepFluids = Map.copyOf(indexed); - this.terrainSamples = new Long2ObjectOpenHashMap<>(); - this.terrainBases = new Long2ObjectOpenHashMap<>(); - this.naturalClassifications = new Long2ObjectOpenHashMap<>(); - this.features = new HashMap<>(); - this.courseFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true); - this.validationCourseRasters = new LinkedHashMap<>(VALIDATION_RASTER_CACHE_SIZE, 1F, true); - this.rasterStencils = new HashMap<>(); - this.surfaceCompiler = new SurfaceFootprintCompiler(settings, new HydrologyTerrainSampler() { - @Override - public HydrologyTerrainSample sample(int x, int z) { - return sampleTerrainBasis(x, z); - } - - @Override - public HydrologyTerrainSample[] sampleBatch(long[] coordinates, int count) { - return sampleTerrainBases(coordinates, count); - } - }, this.geometrySampler); - this.surfaceFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true); - this.regionalSurfaceFootprints = new LinkedHashMap<>(); - this.regionalDropRasters = new LinkedHashMap<>(); - this.regionalNetwork = HydrologyRegionalNetwork.EMPTY; - this.fullMaterializationCount = 0; - this.rasterizer = new HydrologyFootprintRasterizer(this); - this.channelGeometry = new HydrologyChannelGeometry(this); - } - - SurfaceRasterIndex emptySurfaceRaster() { - return new SurfaceRasterIndex(List.of(), new Long2ObjectOpenHashMap<>()); - } - - SurfaceFootprint surfaceFootprint(RiverCourse course) { - return surfaceFootprint(course, false); - } - - SurfaceFootprint surfaceFootprintForPublication(RiverCourse course) { - return surfaceFootprint(course, true); - } - - private SurfaceFootprint surfaceFootprint(RiverCourse course, boolean publication) { - SurfaceFootprint regional = regionalSurfaceFootprints.get(course.id()); - if (regional != null) { - return regional; - } - FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey( - course.id(), - course.type(), - course.profileKey(), - course.segments() - ); - SurfaceFootprint cached = surfaceFootprints.get(rasterKey); - if (cached != null) { - return cached; - } - SurfaceFootprint compiled = publication ? surfaceCompiler.compileForPublication(course) : surfaceCompiler.compile(course); - if (publication && !compiled.accepted()) { - return compiled; - } - surfaceFootprints.put(rasterKey, compiled); - if (surfaceFootprints.size() > COURSE_FOOTPRINT_CACHE_SIZE) { - surfaceFootprints.remove(surfaceFootprints.sequencedKeySet().getFirst()); - } - return compiled; - } - - void seedRegionalSurface(HydrologyRegionalNetwork network, SurfaceBounds bounds) { - regionalNetwork = Objects.requireNonNull(network); - regionalSurfaceFootprints.clear(); - regionalDropRasters.clear(); - for (RiverCourse course : network.courses()) { - SurfaceFootprint footprint = surfaceCompiler.compile(course, bounds); - if (!footprint.accepted()) { - throw new IllegalStateException("Accepted regional river changed during bounded footprint compilation: " + course.id()); - } - regionalSurfaceFootprints.put(course.id(), footprint); - regionalDropRasters.put(course.id(), HydrologySurfaceDropRaster.compile(settings, - HydrologyOceanReceiver.forCourse(settings, this::sampleTerrainBasis, course), geometrySampler, course, bounds)); - } - } - - RiverFootprint compile(List courses) { - fullMaterializationCount++; - if (courses.isEmpty()) { - return RiverFootprint.empty(); - } - if (courses.size() == 1) { - return compileCourse(courses.getFirst()); - } - Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); - for (RiverCourse course : courses) { - RiverFootprint footprint = compileCourse(course); - for (Map.Entry entry : footprint.columns().entrySet()) { - HydrologyColumnSample sample = entry.getValue(); - FootprintMutableColumn column = columns.get(entry.getKey()); - if (column == null) { - columns.put(entry.getKey(), new FootprintMutableColumn(sample)); - continue; - } - column.merge(sample); - } - } - return build(columns); - } - - ValidationRaster compileValidation(List courses) { - boolean caveCandidatePresent = false; - for (RiverCourse course : courses) { - if (hasCaveSegment(course)) { - caveCandidatePresent = true; - break; - } - } - if (!caveCandidatePresent) { - return new ValidationRaster( - List.of(), - new SurfaceRasterIndex(List.of(), new Long2ObjectOpenHashMap<>()) - ); - } - - ArrayList courseRasters = new ArrayList<>(courses.size()); - ArrayList surfaces = new ArrayList<>(); - Long2ObjectOpenHashMap firstSamples = new Long2ObjectOpenHashMap<>(); - for (RiverCourse course : courses) { - FootprintValidationCourseRaster raster = compileValidationCourse(course); - courseRasters.add(raster); - surfaces.add(raster.surface()); - for (HydrologyColumnSample sample : raster.columns()) { - long packed = RiverFootprint.pack(sample.x(), sample.z()); - HydrologyColumnSample first = firstSamples.putIfAbsent(packed, sample); - if (first != null) { - validateMatchingTerrainMetadata(first, sample); - FootprintMutableColumn merged = new FootprintMutableColumn(first); - merged.merge(sample); - firstSamples.put(packed, merged.build()); - } - } - } - return new ValidationRaster( - List.copyOf(courseRasters), - new SurfaceRasterIndex(surfaces, firstSamples) - ); - } - - ValidationRaster compileSurfaceDropValidation(RiverCourse course, SurfaceFootprint surface, - HydrologySurfaceDropRaster drops) { - Objects.requireNonNull(course, "course"); - Objects.requireNonNull(surface, "surface"); - Objects.requireNonNull(drops, "drops"); - Long2ObjectOpenHashMap samples = new Long2ObjectOpenHashMap<>(); - for (HydrologyColumnSample sample : drops.columns()) { - samples.put(RiverFootprint.pack(sample.x(), sample.z()), sample); - } - FootprintValidationCourseRaster raster = new FootprintValidationCourseRaster( - course.id(), drops.columns(), surface); - return new ValidationRaster(List.of(raster), new SurfaceRasterIndex(List.of(surface), samples)); - } - - int fullMaterializationCount() { - return fullMaterializationCount; - } - - FootprintValidationCourseRaster compileValidationCourse(RiverCourse course) { - FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey( - course.id(), - course.type(), - course.profileKey(), - course.segments() - ); - FootprintValidationCourseRaster cached = validationCourseRasters.get(rasterKey); - if (cached != null) { - return cached; - } - - boolean caveCourse = hasCaveSegment(course); - features.clear(); - Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); - SurfaceFootprint surface = surfaceFootprint(course); - for (SurfaceLayerColumn column : surface.columns()) { - addLayer(columns, column.x(), column.z(), column.terrain(), column.layer()); - } - boolean boundedRegionalDrops = addRegionalDrops(columns, course.id()); - SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(List.of(surface), new Long2ObjectOpenHashMap<>()); - for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) { - HydraulicSegment segment = course.segments().get(segmentIndex); - if (boundedRegionalDrops && segment.type().isSurface() && segment.fallingFluid()) { - continue; - } - boolean firstSegment = segmentIndex == 0; - boolean clipStart = segmentIndex > 0 - && rasterizer.segmentsJoin(course.segments().get(segmentIndex - 1), segment); - boolean clipEnd = segmentIndex + 1 < course.segments().size() - && rasterizer.segmentsJoin(segment, course.segments().get(segmentIndex + 1)); - if (caveCourse && (segment.type().isUnderground() - || segment.type().isDeepFluid() || segment.type().isSurface() && segment.fallingFluid())) { - rasterizer.rasterizeSegment( - columns, - course, - segment, - firstSegment, - clipStart, - clipEnd, - true, - courseSurface - ); - } - } - FootprintValidationCourseRaster raster = new FootprintValidationCourseRaster( - course.id(), - buildValidationColumns(columns), - surface - ); - validationCourseRasters.put(rasterKey, raster); - if (validationCourseRasters.size() > VALIDATION_RASTER_CACHE_SIZE) { - validationCourseRasters.remove(validationCourseRasters.sequencedKeySet().getFirst()); - } - return raster; - } - - boolean hasCaveSegment(RiverCourse course) { - for (HydraulicSegment segment : course.segments()) { - if (segment.type().isUnderground() || segment.type().isDeepFluid() - || segment.type().isSurface() && segment.fallingFluid()) { - return true; - } - } - return false; - } - - List buildValidationColumns( - Long2ObjectLinkedOpenHashMap columns - ) { - ArrayList built = new ArrayList<>(columns.size()); - for (FootprintMutableColumn column : columns.values()) { - built.add(column.build()); - } - built.sort(Comparator.comparingLong( - (HydrologyColumnSample sample) -> RiverFootprint.pack(sample.x(), sample.z()) - )); - return List.copyOf(built); - } - - void mergeSurfaceDrop(Long2ObjectLinkedOpenHashMap columns, - Long2ObjectLinkedOpenHashMap dropColumns) { - List constrained = HydrologySurfaceDropBankBounds.constrain( - buildValidationColumns(dropColumns), settings.surface().banks().erosion().excavation().maximumWidth()); - for (HydrologyColumnSample sample : constrained) { - long key = RiverFootprint.pack(sample.x(), sample.z()); - FootprintMutableColumn column = columns.get(key); - if (column == null) { - columns.put(key, new FootprintMutableColumn(sample)); - } else { - column.merge(sample); - } - } - } - - static void validateMatchingTerrainMetadata( - HydrologyColumnSample first, - HydrologyColumnSample second - ) { - if (first.x() != second.x() - || first.z() != second.z() - || first.naturalHeight() != second.naturalHeight() - || first.seaLevel() != second.seaLevel() - || first.ocean() != second.ocean() - || !first.parentBiomeKey().equals(second.parentBiomeKey())) { - throw new IllegalStateException("Hydrology course footprints disagree on terrain metadata at " - + first.x() + "," + first.z() + "."); - } - } - - RiverFootprint compileCourse(RiverCourse course) { - FootprintCourseRasterKey rasterKey = new FootprintCourseRasterKey( - course.id(), - course.type(), - course.profileKey(), - course.segments() - ); - RiverFootprint cached = courseFootprints.get(rasterKey); - if (cached != null) { - return cached; - } - features.clear(); - Long2ObjectLinkedOpenHashMap columns = new Long2ObjectLinkedOpenHashMap<>(); - SurfaceFootprint surface = surfaceFootprint(course); - for (SurfaceLayerColumn column : surface.columns()) { - addLayer(columns, column.x(), column.z(), column.terrain(), column.layer()); - } - boolean boundedRegionalDrops = addRegionalDrops(columns, course.id()); - SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(List.of(surface), new Long2ObjectOpenHashMap<>()); - for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) { - HydraulicSegment segment = course.segments().get(segmentIndex); - if (SurfaceFootprintCompiler.exposedSegment(segment) - || boundedRegionalDrops && segment.type().isSurface() && segment.fallingFluid()) { - continue; - } - boolean clipStart = segmentIndex > 0 - && rasterizer.segmentsJoin(course.segments().get(segmentIndex - 1), segment); - boolean clipEnd = segmentIndex + 1 < course.segments().size() - && rasterizer.segmentsJoin(segment, course.segments().get(segmentIndex + 1)); - rasterizer.rasterizeSegment( - columns, - course, - segment, - segmentIndex == 0, - clipStart, - clipEnd, - false, - courseSurface - ); - } - RiverFootprint footprint = build(columns); - courseFootprints.put(rasterKey, footprint); - if (courseFootprints.size() > COURSE_FOOTPRINT_CACHE_SIZE) { - courseFootprints.remove(courseFootprints.sequencedKeySet().getFirst()); - } - return footprint; - } - - private boolean addRegionalDrops(Long2ObjectLinkedOpenHashMap columns, long courseId) { - HydrologySurfaceDropRaster drops = regionalDropRasters.get(courseId); - if (drops == null) { - return false; - } - for (HydrologyColumnSample sample : drops.columns()) { - for (HydrologyColumnLayer layer : sample.layers()) { - addLayer(columns, sample.x(), sample.z(), sampleTerrainBasis(sample.x(), sample.z()), layer); - } - } - return true; - } - - RiverFootprint build(Long2ObjectLinkedOpenHashMap columns) { - Long2ObjectOpenHashMap immutable = new Long2ObjectOpenHashMap<>(columns.size()); - for (Long2ObjectMap.Entry entry : columns.long2ObjectEntrySet()) { - immutable.put(entry.getLongKey(), entry.getValue().build()); - } - return new RiverFootprint(immutable); - } - - HydrologyColumnLayer terrainContainedCaveLayer( - RiverCourse course, - HydraulicSegment segment, - HydrologyTerrainSample terrain, - HydrologyColumnLayer layer, - int x, - int z, - SurfaceRasterIndex plannedSurface - ) { - if (!terrainRoofedLayer(course, segment)) { - return layer; - } - boolean seaCave = course.type() == RiverCourseType.SEA_CAVE; - int plannedTerrainHeight = plannedSurface.resolve( - x, - z, - terrain.naturalHeight() - ); - boolean surfaceChannel = plannedSurface.ownsSurfaceChannelAt( - x, - z, - course.id() - ); - int maximumCeiling = surfaceChannel ? Integer.MAX_VALUE : plannedTerrainHeight - 1; - for (int[] offset : HORIZONTAL_NEIGHBORS) { - int neighborX = x + offset[0]; - int neighborZ = z + offset[1]; - HydrologyTerrainSample neighborTerrain = sampleTerrainBasis(neighborX, neighborZ); - if (neighborTerrain == null) { - continue; - } - if (seaCave && neighborTerrain.ocean()) { - // The sea face is the intended opening; the seabed never lowers the chamber roof. - continue; - } - if (plannedSurface.ownsSurfaceChannelAt(neighborX, neighborZ, course.id())) { - continue; - } - int neighborSurface = plannedSurface.resolve( - neighborX, - neighborZ, - neighborTerrain.naturalHeight() - ); - maximumCeiling = Math.min(maximumCeiling, neighborSurface - 1); - } - if (seaCave && Math.min(layer.ceilingY(), maximumCeiling) <= layer.fluidHeadY()) { - // A sea-cave column needs air between the sea and its roof; a roof at or below sea level is not carved. - return null; - } - if (maximumCeiling == Integer.MAX_VALUE || layer.ceilingY() <= maximumCeiling) { - return layer; - } - if (maximumCeiling < layer.fluidHeadY()) { - return null; - } - return withCeiling(layer, maximumCeiling); - } - - HydrologyColumnLayer withCeiling(HydrologyColumnLayer layer, int ceiling) { - return new HydrologyColumnLayer( - layer.feature(), - layer.bedY(), - layer.fluidHeadY(), - Math.min(layer.ceilingY(), ceiling), - layer.channel(), - layer.shore(), - layer.grading(), - layer.connectedFluid(), - layer.fallingFluid(), - layer.receivingPool(), - layer.terrainOwned(), - layer.fluidOwned(), - layer.oceanApron(), - layer.profileKey(), - layer.surfaceBiomeKey(), - layer.mouthBiomeKey(), - layer.shoreBiomeKey(), - layer.bankBiomeKey(), - layer.floodedCaveBiomeKey() - ); - } - - HydrologyColumnLayer regularLayer( - RiverCourse course, - HydraulicSegment segment, - HydrologyPoint point, - FootprintLayerShape shape, - HydrologyTerrainSample terrain, - double distance, - int deltaX, - int deltaZ, - int flowX, - int flowZ, - boolean source, - boolean channel, - boolean shore, - boolean grading, - boolean falling, - boolean receiving, - HydrologyFeatureRef[] pointFeatures - ) { - HydrologyFeatureRef feature = feature( - course, - segment, - point.x() + deltaX, - shape.fluidHead(), - point.z() + deltaZ, - flowX, - flowZ, - source && deltaX == 0 && deltaZ == 0, - channel, - shore, - grading, - channel && falling, - channel && receiving, - pointFeatures - ); - return regularLayer( - course, - segment, - shape, - terrain, - distance, - point.x() + deltaX, - point.z() + deltaZ, - channel, - shore, - grading, - falling, - receiving, - feature - ); - } - - HydrologyColumnLayer regularLayer( - RiverCourse course, - HydraulicSegment segment, - FootprintLayerShape shape, - HydrologyTerrainSample terrain, - double distance, - int worldX, - int worldZ, - boolean channel, - boolean shore, - boolean grading, - boolean falling, - boolean receiving, - HydrologyFeatureRef feature - ) { - HydrologyPlannerSettings.ChannelShape channelShape = channelGeometry.channelShape(segment.type()); - double ellipsoidScale = channel && (shape.ellipsoid() || shape.archedChannel()) - ? channelGeometry.ellipsoidScale(shape.channelRadius(), distance, channelShape.bedRoundness()) - : 0D; - int bed = resolvedBed( - segment, - shape, - channelShape, - terrain, - distance, - worldX, - worldZ, - channel, - ellipsoidScale - ); - boolean surfaceDrop = segment.type().isSurface() && segment.fallingFluid(); - if (surfaceDrop && channel && !falling) { - int maximumIncision = terrain.surfacePolicy().maximumIncision(settings.surface().maximumIncision()); - if (shape.inlet()) { - maximumIncision = Math.max(maximumIncision, settings.surface().banks().inlet().maximumIncision()); - } - int allowedIncision = Math.min(maximumIncision, - (int) StrictMath.floor(maximumIncision * terrain.incisionMultiplier())); - bed = Math.max(bed, Math.min(shape.fluidHead(), terrain.naturalHeight() - allowedIncision)); - if (bed == shape.fluidHead()) { - channel = false; - shore = true; - grading = true; - } - } - if (surfaceDrop && !channel) { - HydrologyPlannerSettings.Excavation excavation = settings.surface().banks().erosion().excavation(); - double bankDistance = Math.max(0D, distance - shape.channelRadius()); - bed = bankDistance > excavation.maximumWidth() ? terrain.naturalHeight() - : Math.max(bed, terrain.naturalHeight() - excavation.maximumDepth()); - } - int fluidHead = channel ? shape.fluidHead() : bed; - int ceiling = channel ? localCeiling(shape, ellipsoidScale, segment, channelShape, worldX, worldZ) : fluidHead; - if (channel && terrainRoofedLayer(course, segment)) { - ceiling = Math.max(fluidHead, Math.min(ceiling, terrain.naturalHeight() - 1)); - } - boolean terrainOwned = !falling && (!surfaceDrop || channel || bed < terrain.naturalHeight()); - return new HydrologyColumnLayer( - feature, - bed, - fluidHead, - ceiling, - channel, - !falling && shore, - terrainOwned && (grading || shore), - channel, - channel && falling, - channel && receiving, - terrainOwned, - channel, - false, - course.profileKey(), - terrain.surfaceBiomeKey(), - terrain.mouthBiomeKey(), - terrain.shoreBiomeKey(), - terrain.bankBiomeKey(), - terrain.floodedCaveBiomeKey() - ); - } - - FootprintRasterStencil rasterStencil(FootprintLayerShape shape) { - FootprintRasterStencilKey key = new FootprintRasterStencilKey( - shape.channelRadius(), - shape.shoreWidth(), - shape.gradingWidth() - ); - FootprintRasterStencil cached = rasterStencils.get(key); - if (cached != null) { - return cached; - } - FootprintRasterStencil stencil = buildRasterStencil(key); - rasterStencils.put(key, stencil); - return stencil; - } - - FootprintRasterStencil buildRasterStencil(FootprintRasterStencilKey key) { - double totalRadius = key.channelRadius() + key.shoreWidth() + key.gradingWidth(); - int blockRadius = (int) StrictMath.ceil(totalRadius); - FootprintRasterStencilBuilder builder = new FootprintRasterStencilBuilder(); - for (int deltaZ = -blockRadius; deltaZ <= blockRadius; deltaZ++) { - for (int deltaX = -blockRadius; deltaX <= blockRadius; deltaX++) { - double distance = StrictMath.hypot(deltaX, deltaZ); - if (distance > totalRadius + 0.25D) { - continue; - } - boolean channel = distance <= key.channelRadius() + 0.25D; - boolean shore = !channel && distance <= key.channelRadius() + key.shoreWidth() + 0.25D; - boolean grading = !channel && !shore && distance <= totalRadius + 0.25D; - if (!channel && !shore && !grading) { - continue; - } - builder.add(deltaX, deltaZ, distance); - } - } - return builder.build(); - } - - void addOceanApron( - Long2ObjectLinkedOpenHashMap columns, - RiverCourse course, - HydraulicSegment segment, - HydrologyPoint point, - int x, - int z, - double distance, - int flowX, - int flowZ, - HydrologyTerrainSample terrain, - HydrologyFeatureRef[] pointFeatures - ) { - if (!oceanApronEligible(segment, distance)) { - return; - } - addOceanApronLayer(columns, course, segment, point, x, z, flowX, flowZ, terrain, pointFeatures); - } - - void addOceanApronLayer( - Long2ObjectLinkedOpenHashMap columns, - RiverCourse course, - HydraulicSegment segment, - HydrologyPoint point, - int x, - int z, - int flowX, - int flowZ, - HydrologyTerrainSample terrain, - HydrologyFeatureRef[] pointFeatures - ) { - int fluidHead = Math.min(settings.seaLevel(), point.y()); - HydrologyFeatureRef feature = feature( - course, - segment, - x, - fluidHead, - z, - flowX, - flowZ, - false, - true, - false, - false, - false, - false, - pointFeatures - ); - HydrologyColumnLayer layer = new HydrologyColumnLayer( - feature, - fluidHead, - fluidHead, - segment.type() == HydrologyFeatureType.COASTAL_GROTTO - ? fluidHead + settings.outlets().coastalGrotto().headroom() - : fluidHead, - true, - false, - false, - true, - false, - false, - false, - false, - true, - course.profileKey(), - terrain.surfaceBiomeKey(), - terrain.mouthBiomeKey(), - terrain.shoreBiomeKey(), - terrain.bankBiomeKey(), - terrain.floodedCaveBiomeKey() - ); - addLayer(columns, x, z, terrain, layer); - } - - int gradedBed( - FootprintLayerShape shape, - HydrologyTerrainSample terrain, - double distance - ) { - double transitionWidth = shape.shoreWidth() + shape.gradingWidth(); - if (transitionWidth <= 0D) { - return terrain.naturalHeight(); - } - double bankDistance = Math.max(0D, distance - shape.channelRadius()); - int targetBed = Math.addExact(shape.fluidHead(), 1); - double progress = channelGeometry.smoothStep(Math.min(1D, bankDistance / transitionWidth)); - int blendedBed = (int) StrictMath.round( - targetBed + (terrain.naturalHeight() - targetBed) * progress - ); - return Math.min(terrain.naturalHeight(), blendedBed); - } - - int resolvedBed( - HydraulicSegment segment, - FootprintLayerShape shape, - HydrologyPlannerSettings.ChannelShape channelShape, - HydrologyTerrainSample terrain, - double distance, - int worldX, - int worldZ, - boolean channel, - double ellipsoidScale - ) { - int resolved; - if (shape.ellipsoid() || shape.archedChannel()) { - resolved = channel - ? localBed(segment, shape, channelShape, ellipsoidScale, worldX, worldZ) - : terrain.naturalHeight(); - } else if (!shape.roundedSurfaceBed()) { - resolved = channel ? shape.bed() : gradedBed(shape, terrain, distance); - } else if (channel) { - double normalized = Math.min(1D, distance / Math.max(1D, shape.channelRadius())); - double thalwegFraction = settings.surface().banks().flow().waterfallThalwegFraction(); - double shoulder = Math.max( - 0D, - (normalized - thalwegFraction) / (1D - thalwegFraction) - ); - int maximumDepth = Math.max(1, shape.fluidHead() - shape.bed()); - double roughness = SurfaceNoise.signed( - HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_BED_VARIATION_SALT), - worldX, - worldZ, - channelShape.roughnessWavelength() - ) * channelShape.bedRoughness(); - double depthScale = Math.max(0.55D, 1D + roughness); - int localDepth = 1 + (int) StrictMath.floor( - (maximumDepth - 1D) - * (1D - StrictMath.pow(shoulder, channelShape.bedRoundness())) - * depthScale - + 1.0E-9D - ); - localDepth = Math.min(maximumDepth, localDepth); - if (segment.type().isDrop()) { - localDepth = Math.min( - maximumDepth, - Math.max(localDepth, settings.geometry().drops().stepLimit(segment.type()) + 1) - ); - } - resolved = shape.fluidHead() - localDepth; - } else { - resolved = gradedBed(shape, terrain, distance); - } - return Math.min(resolved, terrain.naturalHeight()); - } - - int localBed( - HydraulicSegment segment, - FootprintLayerShape shape, - HydrologyPlannerSettings.ChannelShape channelShape, - double scale, - int worldX, - int worldZ - ) { - if (!shape.ellipsoid() && !shape.archedChannel()) { - return shape.bed(); - } - int lowerExtent = shape.fluidHead() - shape.bed(); - double roughness = SurfaceNoise.signed( - HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_BED_VARIATION_SALT), - worldX, - worldZ, - channelShape.roughnessWavelength() - ) * channelShape.bedRoughness(); - int localExtent = (int) StrictMath.ceil(lowerExtent * scale * Math.max(0.55D, 1D + roughness)); - localExtent = Math.min(lowerExtent, localExtent); - if (shape.archedChannel()) { - localExtent = Math.max(1, localExtent); - } - if (segment.type().isDrop()) { - localExtent = Math.max(localExtent, settings.geometry().drops().stepLimit(segment.type()) + 1); - } - return shape.fluidHead() - localExtent; - } - - int localCeiling( - FootprintLayerShape shape, - double scale, - HydraulicSegment segment, - HydrologyPlannerSettings.ChannelShape channelShape, - int worldX, - int worldZ - ) { - if (!shape.ellipsoid() && !shape.archedChannel()) { - return shape.ceiling(); - } - int upperExtent = shape.ceiling() - shape.fluidHead(); - double extent = upperExtent * scale; - if (channelShape.ceilingRoughness() > 0D) { - double roughness = SurfaceNoise.signed( - HydrologyHash.mix(segment.id(), HydrologyChannelGeometry.ORGANIC_CEILING_VARIATION_SALT), - worldX, - worldZ, - channelShape.roughnessWavelength() - ) * channelShape.ceilingRoughness(); - extent *= Math.max(0.55D, 1D + roughness); - } - int localExtent = (int) StrictMath.floor(extent); - if (shape.archedChannel()) { - localExtent = Math.max(1, localExtent); - } - return shape.fluidHead() + localExtent; - } - - /** - * Layers whose ceiling follows the terrain: the underground transition of a surface river, and the - * chamber of a standalone sea cave, which must never break the surface above it. - */ - boolean terrainRoofedLayer(RiverCourse course, HydraulicSegment segment) { - if (course.type() == RiverCourseType.SEA_CAVE) { - return segment.type() == HydrologyFeatureType.COASTAL_GROTTO; - } - return course.type() == RiverCourseType.SURFACE - && (segment.type() == HydrologyFeatureType.UNDERGROUND_POOL - || segment.type() == HydrologyFeatureType.UNDERGROUND_DROP); - } - - void addLayer( - Long2ObjectLinkedOpenHashMap columns, - int x, - int z, - HydrologyTerrainSample terrain, - HydrologyColumnLayer layer - ) { - long packed = RiverFootprint.pack(x, z); - FootprintMutableColumn column = columns.get(packed); - if (column == null) { - column = new FootprintMutableColumn(x, z, terrain, settings.seaLevel()); - columns.put(packed, column); - } - column.add(layer); - } - - HydrologyTerrainSample sampleTerrain(int x, int z) { - long packed = RiverFootprint.pack(x, z); - HydrologyTerrainSample cached = terrainSamples.get(packed); - if (cached != null) { - return cached; - } - HydrologyTerrainSample sampled = sampler.sample(x, z); - if (sampled != null) { - terrainSamples.put(packed, sampled); - if (naturalSampler == null) { - terrainBases.put(packed, sampled); - } - } - return sampled; - } - - HydrologyTerrainSample sampleTerrainBasis(int x, int z) { - if (naturalSampler == null) { - return sampleTerrain(x, z); - } - long packed = RiverFootprint.pack(x, z); - HydrologyTerrainSample cached = terrainBases.get(packed); - if (cached != null) { - return cached; - } - HydrologyTerrainSample sampled = sharedBasis == null ? naturalSampler.sampleBasisWithoutSlope(x, z) - : sharedBasis.sampleWithoutSlope(x, z, naturalSampler); - if (sampled != null) { - terrainBases.put(packed, sampled); - } - return sampled; - } - - private HydrologyTerrainSample[] sampleTerrainBases(long[] coordinates, int count) { - HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count]; - long[] missing = new long[count]; - int[] positions = new int[count]; - int missingCount = 0; - for (int index = 0; index < count; index++) { - long key = coordinates[index]; - HydrologyTerrainSample cached = terrainBases.get(key); - if (cached != null) { - samples[index] = cached; - } else if (naturalSampler == null) { - samples[index] = sampleTerrainBasis(RiverFootprint.unpackX(key), RiverFootprint.unpackZ(key)); - } else { - missing[missingCount] = key; - positions[missingCount++] = index; - } - } - if (missingCount > 0) { - HydrologyTerrainSample[] loaded = sharedBasis == null - ? naturalSampler.sampleBasisWithoutSlopeBatch(missing, missingCount) - : sharedBasis.sampleWithoutSlopeBatch(missing, missingCount, naturalSampler); - for (int index = 0; index < missingCount; index++) { - samples[positions[index]] = loaded[index]; - } - } - for (int index = 0; index < count; index++) { - if (samples[index] != null) { - terrainBases.put(coordinates[index], samples[index]); - } - } - return samples; - } - - HydrologyRoutingTerrainSampler.NaturalClassification classifyNatural(int x, int z) { - if (naturalSampler == null) { - return HydrologyRoutingTerrainSampler.NaturalClassification.UNAVAILABLE; - } - long packed = RiverFootprint.pack(x, z); - HydrologyRoutingTerrainSampler.NaturalClassification cached = naturalClassifications.get(packed); - if (cached != null) { - return cached; - } - HydrologyRoutingTerrainSampler.NaturalClassification sampled = Objects.requireNonNull( - naturalSampler.classifyNatural(x, z), - "Hydrology natural terrain classifier returned null at " + x + "," + z - ); - naturalClassifications.put(packed, sampled); - return sampled; - } - - boolean elevatedSeaLevelSurfaceColumn(HydraulicSegment segment, HydrologyTerrainSample terrain, int fluidHead) { - return segment.type().isSurface() && terrain.naturalHeight() <= settings.seaLevel() - && fluidHead > settings.seaLevel(); - } - - boolean naturallySubmergedSurfaceColumn( - HydraulicSegment segment, - HydrologyTerrainSample terrain - ) { - return segment.type().isSurface() && terrain.naturalHeight() < settings.seaLevel(); - } - - boolean oceanApronEligible(HydraulicSegment segment, double distance) { - return (segment.type() == HydrologyFeatureType.MOUTH - || segment.type() == HydrologyFeatureType.COASTAL_GROTTO) - && distance <= apronReach(segment) + 0.25D; - } - - /** - * How far from the centerline the ocean apron reaches. A coastal grotto chamber is as wide as its - * horizontal radius, so its apron reaches at least that far: every sea column the chamber touches - * must carry the apron that declares the sea face an intentional opening. - */ - int apronReach(HydraulicSegment segment) { - int reach = settings.outlets().maximumOceanApron(); - if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) { - return Math.max(reach, settings.outlets().coastalGrotto().horizontalRadius()); - } - return reach; - } - - /** - * The organic chamber boundary can put a sea column one block past the apron reach while the land - * column beside it is still carved; the apron follows the chamber there so the face stays open. - */ - void addAdjacentSeaApron( - Long2ObjectLinkedOpenHashMap columns, - RiverCourse course, - HydraulicSegment segment, - HydrologyPoint point, - int x, - int z, - int flowX, - int flowZ, - HydrologyFeatureRef[] pointFeatures - ) { - for (int[] offset : HORIZONTAL_NEIGHBORS) { - int neighborX = x + offset[0]; - int neighborZ = z + offset[1]; - HydrologyTerrainSample neighborTerrain = sampleTerrainBasis(neighborX, neighborZ); - if (neighborTerrain == null) { - continue; - } - if (classifyNatural(neighborX, neighborZ) != HydrologyRoutingTerrainSampler.NaturalClassification.OCEAN - && !neighborTerrain.ocean()) { - continue; - } - addOceanApronLayer( - columns, - course, - segment, - point, - neighborX, - neighborZ, - flowX, - flowZ, - neighborTerrain, - pointFeatures - ); - } - } - - HydrologyFeatureRef feature( - RiverCourse course, - HydraulicSegment segment, - int x, - int y, - int z, - int flowX, - int flowZ, - boolean source, - boolean channel, - boolean shore, - boolean grading, - boolean falling, - boolean receiving, - HydrologyFeatureRef[] pointFeatures - ) { - int role = source ? 6 : falling ? 4 : receiving ? 5 : channel ? 1 : shore ? 2 : grading ? 3 : 0; - HydrologyFeatureRef cached = pointFeatures[role]; - if (cached != null) { - return cached; - } - int featureFlowX = Integer.compare(flowX, 0); - int featureFlowZ = Integer.compare(flowZ, 0); - FootprintFeatureKey key = new FootprintFeatureKey( - course.id(), - segment.id(), - segment.type(), - role, - featureFlowX, - featureFlowZ - ); - HydrologyFeatureRef feature = features.get(key); - if (feature == null) { - feature = new HydrologyFeatureRef( - HydrologyHash.mix( - course.id(), - segment.id(), - segment.type().ordinal(), - role, - featureFlowX, - featureFlowZ - ), - segment.type(), - course.id(), - segment.id(), - x, - y, - z, - featureFlowX, - featureFlowZ, - source - ); - features.put(key, feature); - } - pointFeatures[role] = feature; - return feature; - } - - int flowDelta(List centerline, int pointIndex, boolean xAxis) { - HydrologyPoint from; - HydrologyPoint to; - if (centerline.size() == 1) { - return 0; - } - if (pointIndex < centerline.size() - 1) { - from = centerline.get(pointIndex); - to = centerline.get(pointIndex + 1); - } else { - from = centerline.get(pointIndex - 1); - to = centerline.get(pointIndex); - } - return Integer.compare(xAxis ? to.x() : to.z(), xAxis ? from.x() : from.z()); - } - - record Sampling( - HydrologyTerrainSampler sampler, - HydrologyGeometrySampler geometrySampler, - HydrologyNaturalTerrainSampler naturalSampler - ) { - Sampling { - Objects.requireNonNull(sampler, "sampler"); - Objects.requireNonNull(geometrySampler, "geometrySampler"); - } - } - - final class ValidationRaster { - private final List courseRasters; - private final Long2ObjectOpenHashMap> columnsByCourse; - private final SurfaceRasterIndex surfaceRaster; - private final RiverFootprint materializedSurface; - private final int columnReferenceCount; - private List mergedColumns; - - private ValidationRaster( - List courseRasters, - SurfaceRasterIndex surfaceRaster - ) { - this(courseRasters, surfaceRaster, null); - } - - private ValidationRaster( - List courseRasters, - SurfaceRasterIndex surfaceRaster, - RiverFootprint materializedSurface - ) { - this.courseRasters = List.copyOf(Objects.requireNonNull(courseRasters, "courseRasters")); - this.surfaceRaster = Objects.requireNonNull(surfaceRaster, "surfaceRaster"); - this.materializedSurface = materializedSurface; - this.columnsByCourse = new Long2ObjectOpenHashMap<>(courseRasters.size()); - int references = 0; - for (FootprintValidationCourseRaster raster : courseRasters) { - if (columnsByCourse.put(raster.courseId(), raster.columns()) != null) { - throw new IllegalStateException("Duplicate hydrology validation course " + raster.courseId()); - } - references += raster.columns().size(); - } - this.columnReferenceCount = references; - } - - List columnsForCourse(long courseId) { - List columns = columnsByCourse.get(courseId); - return columns == null ? List.of() : columns; - } - - List columns() { - List cached = mergedColumns; - if (cached != null) { - return cached; - } - Long2ObjectLinkedOpenHashMap merged = new Long2ObjectLinkedOpenHashMap<>(); - for (FootprintValidationCourseRaster raster : courseRasters) { - for (HydrologyColumnSample sample : raster.columns()) { - long packed = RiverFootprint.pack(sample.x(), sample.z()); - FootprintMutableColumn column = merged.get(packed); - if (column == null) { - merged.put(packed, new FootprintMutableColumn(sample)); - } else { - column.merge(sample); - } - } - } - cached = buildValidationColumns(merged); - mergedColumns = cached; - return cached; - } - - HydrologyCaveVoxelViewFactory.PlannedSurface plannedSurface() { - if (materializedSurface == null) { - return surfaceRaster; - } - return new MaterializedSurface(materializedSurface); - } - - HydrologyColumnSample surfaceColumnAt(int x, int z) { - return materializedSurface == null ? surfaceRaster.fluidColumns.get(RiverFootprint.pack(x, z)) - : materializedSurface.sample(x, z).orElse(null); - } - - HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) { - if (materializedSurface != null) { - HydrologyColumnSample sample = materializedSurface.sample(x, z).orElse(null); - if (sample != null && sample.naturalHeight() != naturalHeight) { - throw new IllegalStateException("Hydrology surface and cave rasters disagree on natural terrain at " - + x + "," + z + "."); - } - return sample; - } - return surfaceRaster.surfaceColumnAt(x, z, naturalHeight); - } - - boolean ownsSurfaceChannelAt(int x, int z, int naturalHeight, long courseId) { - if (materializedSurface != null) { - HydrologyColumnSample sample = materializedSurface.sample(x, z).orElse(null); - if (sample == null) { - return false; - } - if (sample.naturalHeight() != naturalHeight) { - throw new IllegalStateException("Hydrology surface and cave rasters disagree on natural terrain at " - + x + "," + z + "."); - } - return ownsSurfaceChannel(sample, courseId); - } - return surfaceRaster.ownsSurfaceChannelAt(x, z, courseId); - } - - boolean ownsSurfaceChannelAt(int x, int z, long courseId) { - if (materializedSurface == null) { - return surfaceRaster.ownsSurfaceChannelAt(x, z, courseId); - } - HydrologyColumnSample sample = materializedSurface.sample(x, z).orElse(null); - return sample != null && ownsSurfaceChannel(sample, courseId); - } - - private boolean ownsSurfaceChannel(HydrologyColumnSample sample, long courseId) { - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() == courseId - && layer.feature().type().isSurface() - && layer.channel() - && layer.terrainOwned()) { - return true; - } - } - return false; - } - - ValidationRaster withMaterializedSurface(RiverFootprint footprint) { - return new ValidationRaster(courseRasters, surfaceRaster, Objects.requireNonNull(footprint)); - } - - int columnReferenceCount() { - return columnReferenceCount; - } - } - - private record MaterializedSurface(RiverFootprint footprint) implements HydrologyCaveVoxelViewFactory.PlannedSurface { - @Override - public int resolve(int x, int z, int naturalHeight) { - return footprint.sample(x, z).map(HydrologyColumnSample::terrainHeight).orElse(naturalHeight); - } - - @Override - public boolean ownsTerrain(int x, int z) { - HydrologyColumnSample sample = footprint.sample(x, z).orElse(null); - return sample != null && sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent(); - } - } - - final class SurfaceRasterIndex implements HydrologyCaveVoxelViewFactory.PlannedSurface { - private final Long2ObjectOpenHashMap surfaceColumns; - private final Long2ObjectOpenHashMap fluidColumns; - private final Long2IntOpenHashMap validationNaturalHeights; - - private SurfaceRasterIndex( - List footprints, - Long2ObjectOpenHashMap validationSamples - ) { - Long2ObjectLinkedOpenHashMap merged = new Long2ObjectLinkedOpenHashMap<>(); - ArrayList allFootprints = new ArrayList<>(footprints); - for (SurfaceFootprint regional : regionalSurfaceFootprints.values()) { - if (!allFootprints.contains(regional)) { - allFootprints.add(regional); - } - } - for (SurfaceFootprint footprint : allFootprints) { - for (SurfaceLayerColumn column : footprint.columns()) { - addLayer(merged, column.x(), column.z(), column.terrain(), column.layer()); - } - } - this.surfaceColumns = new Long2ObjectOpenHashMap<>(merged.size()); - for (Long2ObjectMap.Entry entry : merged.long2ObjectEntrySet()) { - surfaceColumns.put(entry.getLongKey(), entry.getValue().build()); - } - for (HydrologySurfaceDropRaster drops : regionalDropRasters.values()) { - for (HydrologyColumnSample sample : drops.columns()) { - addSurfaceFluid(merged, sample); - } - } - for (HydrologyColumnSample sample : validationSamples.values()) { - addSurfaceFluid(merged, sample); - } - this.fluidColumns = new Long2ObjectOpenHashMap<>(merged.size()); - for (Long2ObjectMap.Entry entry : merged.long2ObjectEntrySet()) { - fluidColumns.put(entry.getLongKey(), entry.getValue().build()); - } - this.validationNaturalHeights = new Long2IntOpenHashMap(validationSamples.size()); - for (HydrologyColumnSample sample : validationSamples.values()) { - validationNaturalHeights.put( - RiverFootprint.pack(sample.x(), sample.z()), - sample.naturalHeight() - ); - } - } - - private void addSurfaceFluid(Long2ObjectLinkedOpenHashMap columns, HydrologyColumnSample sample) { - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.publishesSurfaceFluid()) { - addLayer(columns, sample.x(), sample.z(), sampleTerrainBasis(sample.x(), sample.z()), layer); - } - } - } - - @Override - public int resolve(int x, int z, int naturalHeight) { - long packed = RiverFootprint.pack(x, z); - HydrologyColumnSample sample = surfaceColumns.get(packed); - if (sample != null) { - return sample.terrainHeight(); - } - if (validationNaturalHeights.containsKey(packed)) { - return validationNaturalHeights.get(packed); - } - return naturalHeight; - } - - @Override - public boolean ownsTerrain(int x, int z) { - HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z)); - return sample != null && sample.primarySurfaceLayer().filter(HydrologyColumnLayer::terrainOwned).isPresent(); - } - - private HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) { - HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z)); - if (sample != null && sample.naturalHeight() != naturalHeight) { - throw new IllegalStateException("Hydrology surface and cave rasters disagree on natural terrain at " - + x + "," + z + "."); - } - return sample; - } - - private boolean ownsSurfaceChannelAt(int x, int z, long courseId) { - HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z)); - if (sample == null) { - return false; - } - for (HydrologyColumnLayer layer : sample.layers()) { - if (layer.feature().courseId() == courseId - && layer.feature().type().isSurface() - && layer.channel() - && layer.terrainOwned()) { - return true; - } - } - return false; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFootprintRasterizer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFootprintRasterizer.java deleted file mode 100644 index 8d9ebb06e..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyFootprintRasterizer.java +++ /dev/null @@ -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 columns, - RiverCourse course, - HydraulicSegment segment, - boolean firstSegment, - boolean clipStart, - boolean clipEnd, - boolean validationOnly, - HydrologyFootprintCompiler.SurfaceRasterIndex plannedSurface - ) { - Long2ObjectLinkedOpenHashMap segmentColumns = segment.type().isSurface() && segment.fallingFluid() - ? new Long2ObjectLinkedOpenHashMap<>() : columns; - List 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 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 columns, - RiverCourse course, - HydraulicSegment segment, - List 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 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 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 continuousCenterline(HydraulicSegment segment) { - List configured = segment.centerline(); - if (configured.size() == 1) { - return configured; - } - ArrayList 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 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); - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyForkJoin.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyForkJoin.java deleted file mode 100644 index b26c1148b..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyForkJoin.java +++ /dev/null @@ -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 List invokeAll(List> tasks, Executor fallback) { - Objects.requireNonNull(tasks, "tasks"); - boolean callerRuns = Thread.currentThread() instanceof ForkJoinWorkerThread; - if (tasks.size() < 2 || fallback == null && !callerRuns) { - ArrayList results = new ArrayList<>(tasks.size()); - for (Callable task : tasks) { - results.add(call(task)); - } - return results; - } - Executor executor = callerRuns ? ((ForkJoinWorkerThread) Thread.currentThread()).getPool() : fallback; - ArrayList> pending = new ArrayList<>(tasks.size()); - for (Callable 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 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 call(Callable 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 implements Runnable, ForkJoinPool.ManagedBlocker { - private final Callable 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 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(); - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGeometrySampler.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGeometrySampler.java deleted file mode 100644 index 646d7431a..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGeometrySampler.java +++ /dev/null @@ -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."); - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGridNode.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGridNode.java deleted file mode 100644 index 3bcaec72c..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGridNode.java +++ /dev/null @@ -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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGridOffset.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGridOffset.java deleted file mode 100644 index b9d194736..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyGridOffset.java +++ /dev/null @@ -1,4 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -record HydrologyGridOffset(int x, int z) { -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyHash.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyHash.java deleted file mode 100644 index 4b3fefbe0..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyHash.java +++ /dev/null @@ -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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyLandHeightCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyLandHeightCache.java deleted file mode 100644 index 0dce954a4..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyLandHeightCache.java +++ /dev/null @@ -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); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyNaturalTerrainSampler.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyNaturalTerrainSampler.java deleted file mode 100644 index e8f715dfa..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyNaturalTerrainSampler.java +++ /dev/null @@ -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; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyObservedPlannedSurface.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyObservedPlannedSurface.java deleted file mode 100644 index a1e425bd3..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyObservedPlannedSurface.java +++ /dev/null @@ -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> 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 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 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 observationsAt(int x, int z) { - ArrayList 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; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOceanBoundaryRefiner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOceanBoundaryRefiner.java deleted file mode 100644 index c9dd14aef..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOceanBoundaryRefiner.java +++ /dev/null @@ -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 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 - ) { - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOceanReceiver.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOceanReceiver.java deleted file mode 100644 index 76045ced3..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOceanReceiver.java +++ /dev/null @@ -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 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 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 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 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 points, double width) { - } - - private static final class Samples { - private final HydrologyTerrainSampler source; - private final Long2ObjectOpenHashMap 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; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOutletPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOutletPlanner.java deleted file mode 100644 index d31cb098f..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOutletPlanner.java +++ /dev/null @@ -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 COASTAL_OUTLET_TYPES = List.of( - HydrologyFeatureType.MOUTH, - HydrologyFeatureType.COASTAL_GROTTO - ); - - static final List 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 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 resolveOutlets( - HydrologyTileKey key, - HydrologySampledGrid grid, - boolean surface, - List diagnostics - ) { - ArrayList 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 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 inlandCandidates = inlandOutletCandidates(grid, oceanReachable); - sortInlandOutletCandidates(grid, inlandCandidates); - List selectedInland = styledInlandOutlets( - grid, - surface ? limitSurfaceOutlets(key, grid, inlandCandidates, false) - : limitOutlets(inlandCandidates, planner.settings.outlets().maximumPerTile()), - surface - ); - addRejectedOutlets(inlandCandidates, selectedInland, diagnostics); - ArrayList selected = new ArrayList<>(selectedOcean.size() + selectedInland.size()); - selected.addAll(selectedOcean); - selected.addAll(selectedInland); - return List.copyOf(selected); - } - - ArrayList oceanOutletCandidates(HydrologySampledGrid grid, boolean surface) { - ArrayList 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 regionalOceanOutletCandidates(HydrologySampledGrid grid) { - ArrayList oceanCandidates = new ArrayList<>(); - int index = 0; - while (index < grid.nodes().size()) { - ArrayList> 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 resolveSurfaceFallbackOutlets(HydrologySampledGrid grid) { - ArrayList oceanCandidates = oceanOutletCandidates(grid, true); - ArrayList 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 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 limitSurfaceOutlets(HydrologyTileKey key, HydrologySampledGrid grid, - List 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> 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 selected = new ArrayList<>(); - long spacingSquared = outletSpacingSquared(); - for (Map.Entry> entry : grouped.entrySet()) { - int target = budgets.outletTarget(budgets.area(entry.getKey()), coastal, planner, key); - List 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 inlandOutletCandidates( - HydrologySampledGrid grid, - boolean[] oceanReachable - ) { - ArrayList 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 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 candidates - ) { - HashMap 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 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 styledInlandOutlets( - HydrologySampledGrid grid, - List candidates, - boolean surface - ) { - ArrayList 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 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 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 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 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 owned = Comparator.comparingInt( - (OutletCandidate candidate) -> ownedOutletRank(grid, candidate)); - if (!surface) { - candidates.sort(owned.thenComparing(outletComparator(key))); - return; - } - HashMap capacities = new HashMap<>(candidates.size()); - for (OutletCandidate candidate : candidates) { - capacities.put(candidate.landIndex(), surfaceOutletCapacity(grid, candidate.landIndex())); - } - Comparator 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 limitOutlets(List candidates, int maximum) { - if (maximum <= 0 || candidates.isEmpty()) { - return List.of(); - } - ArrayList 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 limitOutletsByType(List candidates, int maximum) { - if (maximum <= 0 || candidates.isEmpty()) { - return List.of(); - } - ArrayList 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 selected, - long minimumSpacingSquared - ) { - for (OutletCandidate existing : selected) { - if (candidate.outlet().landwardPoint().distanceSquared2D(existing.outlet().landwardPoint()) - < minimumSpacingSquared) { - return true; - } - } - return false; - } - - void addRejectedOutlets( - List candidates, - List selected, - List 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 candidates, - List diagnostics - ) { - int maximumFluidY = planner.settings.underground().maximumFluidY(); - ArrayList 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) { - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOwnerDraft.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOwnerDraft.java deleted file mode 100644 index 89704ad52..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyOwnerDraft.java +++ /dev/null @@ -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 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); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java deleted file mode 100644 index 371a0d1e8..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java +++ /dev/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 routingContexts; - final ConcurrentHashMap resolvingRoutingContexts; - final Cache resolvedOwners; - final Cache> refinedEdgeCache; - final ConcurrentHashMap> resolvingOwners; - final ThreadLocal planningSamples; - final ThreadLocal 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 earlyOwnerScope; - private volatile Function 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 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 diagnosticCandidates(HydrologyTile tile) { - Objects.requireNonNull(tile, "tile"); - ArrayList diagnostics = new ArrayList<>(tile.localDiagnosticCandidates()); - diagnostics.addAll(regional.diagnosticsIn(regional.ownerBounds(tile.key()))); - ArrayList 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 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 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 owned = new CompletableFuture<>(); - CompletableFuture 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 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 resolveLowerRankOwners( - List candidateKeys, - CrossTileResolutionContext context, - Map> preparedOwners - ) { - if (candidateKeys.size() < 2 || !IrisPlatforms.isBound()) { - ArrayList resolved = new ArrayList<>(candidateKeys.size()); - for (HydrologyTileKey candidateKey : candidateKeys) { - CrossTileResolvedOwner local = context.resolved(candidateKey); - if (local != null) { - resolved.add(local); - continue; - } - HydrologyForkJoin.Task 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> tasks = new ArrayList<>(candidateKeys.size()); - ArrayList resolved = new ArrayList<>(candidateKeys.size()); - for (HydrologyTileKey candidateKey : candidateKeys) { - CrossTileResolvedOwner local = context.resolved(candidateKey); - resolved.add(local); - if (local != null) { - continue; - } - HydrologyForkJoin.Task prepared = preparedOwners.get(candidateKey); - tasks.add(prepared == null ? () -> resolveIndependentOwner(candidateKey) : prepared::await); - } - List 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 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 basis; - private final Long2ObjectOpenHashMap basisWithoutSlope; - private final Long2DoubleOpenHashMap landHeights; - private HydrologyLandHeightCache sharedLandHeights; - private HydrologyBasisCache sharedBasis; - final Long2ObjectOpenHashMap detailed; - private final Long2ObjectOpenHashMap classifications; - private final HashMap geometry; - final HashMap minimumLandHeights; - final HashMap> radialOffsets; - final HashMap undergroundSegmentCaps; - final HashMap routeAnchors; - final HashMap 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 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 rejectedCourseIds; - private final Map> preparedOwners = new LinkedHashMap<>(); - private final Set demandedOwners = new HashSet<>(); - private Throwable primaryFailure; - private final LinkedHashMap 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 scope = earlyOwnerScope; - ArrayList 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 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 unusedFailures = new LinkedHashMap<>(); - for (Map.Entry> 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 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 currentCourses = crossTile.coursesById(current.courses()); - HashSet rejectedSurfaceOutlets = new HashSet<>(); - HashMap 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 newRejectedCourseIds = new HashSet<>(); - ArrayList 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 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 newRejectedCourseIds, - List 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 observedRejections() { - return List.copyOf(observedRejections.values()); - } - } - - DraftProfile currentDraftProfile() { - DraftProfile profile = draftProfiles.get(); - return profile == null ? new DraftProfile() : profile; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlannerSettings.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlannerSettings.java deleted file mode 100644 index 6abff3fd9..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlannerSettings.java +++ /dev/null @@ -1,1040 +0,0 @@ -package art.arcane.iris.generation.hydrology; - - -import java.util.List; -import java.util.Objects; - -public record HydrologyPlannerSettings( - int seaLevel, - Routing routing, - Surface surface, - Hydraulics hydraulics, - Underground underground, - Outlets outlets, - Geometry geometry, - List deepFluids, - List surfacePools, - double widestShoreBiomeWidth, - SeaCaves seaCaves, - SurfacePolicyBounds surfacePolicyBounds -) { - private static final long PLAN_FORMAT_REVISION = 9L; - private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4; - - public HydrologyPlannerSettings { - if (seaLevel < -2048 || seaLevel > 2048) { - throw new IllegalArgumentException("seaLevel is outside the supported planning range."); - } - if (routing == null || surface == null || hydraulics == null || underground == null - || outlets == null || geometry == null) { - throw new IllegalArgumentException("Hydrology planner settings cannot contain null sections."); - } - deepFluids = deepFluids == null ? List.of() : List.copyOf(deepFluids); - surfacePools = surfacePools == null ? List.of() : List.copyOf(surfacePools); - if (!Double.isFinite(widestShoreBiomeWidth) || widestShoreBiomeWidth < 0D) { - throw new IllegalArgumentException("widestShoreBiomeWidth must be finite and non-negative."); - } - if (seaCaves == null) { - throw new IllegalArgumentException("Sea cave settings are required."); - } - surfacePolicyBounds = Objects.requireNonNull(surfacePolicyBounds); - int publicationRadius = Math.max(surfacePolicyBounds.maximumSourceSpacing(), - publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools, widestShoreBiomeWidth, seaCaves)); - if (crossTileColorPeriod(publicationRadius, routing.tileSize()) > MAXIMUM_CROSS_TILE_COLOR_PERIOD) { - throw new IllegalArgumentException("Hydrology publication envelope exceeds the bounded cross-tile admission period."); - } - } - - public static HydrologyPlannerSettings defaults() { - Source surfaceSources = new Source(true, 0.5D, 88, 0, 1, 384); - Source undergroundSources = new Source(true, 0.25D, Integer.MIN_VALUE, 0, 1, 512); - return new HydrologyPlannerSettings( - 63, - new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0, HydrologyPlannerSettings.Regional.disabled()), - new Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()), - new Hydraulics(8), - Underground.of(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true, 1), - Outlets.of( - true, - new Grotto(true, 18, 8, 8, 32768), - new Grotto(true, 18, 8, 8, 32768), - false, - 12, - 64, - 8, - 12, - 2 - ), - Geometry.defaults(), - List.of(), - List.of(), - 0D, - SeaCaves.disabled(), - HydrologyPlannerSettings.SurfacePolicyBounds.NONE - ); - } - - public long fingerprint() { - long hash = HydrologyHash.mix( - 0x485944524f4c4f47L, - PLAN_FORMAT_REVISION, - routing.hashCode(), - surface.hashCode(), - hydraulics.hashCode(), - underground.hashCode(), - outlets.hashCode(), - geometry.hashCode(), - Double.doubleToLongBits(widestShoreBiomeWidth), - seaCaves.hashCode() - ); - for (DeepFluid deepFluid : deepFluids) { - hash = HydrologyHash.mix(hash, deepFluid.hashCode(), HydrologyHash.text(deepFluid.id())); - } - for (SurfacePool pool : surfacePools) { - hash = HydrologyHash.mix(hash, pool.hashCode(), HydrologyHash.text(pool.id())); - } - if (surfacePolicyBounds.fingerprint() != 0L) { - hash = HydrologyHash.mix(hash, surfacePolicyBounds.fingerprint(), surfacePolicyBounds.maximumSourceSpacing()); - } - return hash; - } - - public int publicationRadius() { - return Math.max(surfacePolicyBounds.maximumSourceSpacing(), - publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools, widestShoreBiomeWidth, seaCaves)); - } - - public int maximumSurfaceSourceSpacing() { - return Math.max(surface.sources().minimumSpacing(), surfacePolicyBounds.maximumSourceSpacing()); - } - - public record SurfacePolicyBounds(long fingerprint, int maximumSourceSpacing) { - public static final SurfacePolicyBounds NONE = new SurfacePolicyBounds(0L, 0); - - public SurfacePolicyBounds { - if (maximumSourceSpacing < 0 || maximumSourceSpacing > 8192) { - throw new IllegalArgumentException("Maximum surface policy source spacing must be between 0 and 8192."); - } - } - } - - int crossTileColorPeriod() { - return crossTileColorPeriod(publicationRadius(), routing.tileSize()); - } - - private static int publicationRadius( - Routing routing, - Surface surface, - Underground underground, - Outlets outlets, - Geometry geometry, - List deepFluids, - List surfacePools, - double widestShoreBiomeWidth, - SeaCaves seaCaves - ) { - double shoreReach = Math.max(surface.shoreWidth(), widestShoreBiomeWidth); - int alignedHalo = Math.floorDiv( - Math.min( - routing.maximumRouteLength(), - Math.multiplyExact(routing.sampleSpacing(), 2) - ), - routing.sampleSpacing() - ) * routing.sampleSpacing(); - int radius = 0; - int routeDisplacement = Math.multiplyExact(routing.sampleSpacing(), 3); - if (surface.enabled() && surface.sources().enabled()) { - int blendWidth = surface.banks().maximumBlendWidth(); - int surfaceRadius = (int) StrictMath.ceil( - surface.maximumWidth() * surface.banks().mouthFlareRatio() / 2D + shoreReach + blendWidth - ); - surfaceRadius = Math.max( - surfaceRadius, - (int) StrictMath.ceil(geometry.drops().basinWidth( - geometry.drops().flowWidth(surface.maximumWidth()) - ) / 2D + shoreReach + blendWidth) - ); - surfaceRadius = Math.max(surfaceRadius, outlets.coastalGrotto().horizontalRadius()); - surfaceRadius = Math.max(surfaceRadius, outlets.inlandGrotto().horizontalRadius()); - radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(surfaceRadius, routeDisplacement))); - } - if (underground.enabled() && underground.sources().enabled()) { - int undergroundRadius = (int) StrictMath.ceil(underground.maximumWidth() / 2D); - undergroundRadius = Math.max( - undergroundRadius, - (int) StrictMath.ceil(geometry.drops().basinWidth( - geometry.drops().flowWidth(underground.maximumWidth()) - ) / 2D) - ); - undergroundRadius = Math.max(undergroundRadius, outlets.coastalGrotto().horizontalRadius()); - undergroundRadius = Math.max(undergroundRadius, outlets.inlandGrotto().horizontalRadius()); - radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact(undergroundRadius, routeDisplacement))); - } - for (DeepFluid deepFluid : deepFluids) { - if (!deepFluid.enabled() || deepFluid.maximumPerTile() == 0) { - continue; - } - int poolReach = deepFluid.containedPools() ? deepFluid.maximumHorizontalRadius() : 0; - int channelReach = deepFluid.shortChannels() - ? Math.addExact(deepFluid.maximumChannelLength(), (int) StrictMath.ceil(deepFluid.channelWidth() / 2D)) - : 0; - radius = Math.max(radius, Math.max(poolReach, channelReach)); - } - for (SurfacePool pool : surfacePools) { - if (!pool.enabled()) { - continue; - } - radius = Math.max(radius, pool.maximumRadius() + (int) StrictMath.ceil(shoreReach) - + surface.banks().maximumBlendWidth()); - } - if (seaCaves.enabled() && seaCaves.maximumPerTile() > 0) { - // A chamber hangs off a shoreline that may lie a halo away, swept inland by its depth. - radius = Math.max(radius, Math.addExact(alignedHalo, Math.addExact( - outlets.coastalGrotto().horizontalRadius(), - Math.addExact(seaCaves.depth(), 1) - ))); - } - return radius; - } - - private static int crossTileColorPeriod(int publicationRadius, int tileSize) { - long actionReach = Math.addExact((long) publicationRadius, 1L); - return Math.toIntExact(Math.addExact(Math.floorDiv(Math.multiplyExact(2L, actionReach), tileSize), 2L)); - } - - public record Routing( - int tileSize, - int sampleSpacing, - int maximumRouteNodes, - int maximumRouteLength, - int minimumSurfaceCourseLength, - int minimumUndergroundCourseLength, - double valleyPreference, - double uphillPenalty, - double slopePenalty, - double confluenceAttraction, - double lengthPreference, - int tributaries, - Regional regional - ) { - public Routing { - Objects.requireNonNull(regional, "regional"); - if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) { - throw new IllegalArgumentException("Routing sizes must form an exact bounded tile lattice."); - } - int latticeWidth = tileSize / sampleSpacing + 1; - int halo = Math.min(maximumRouteLength, Math.multiplyExact(sampleSpacing, 2)); - int alignedHalo = Math.floorDiv(halo, sampleSpacing) * sampleSpacing; - int expandedWidth = latticeWidth + alignedHalo * 2 / sampleSpacing; - long latticeNodes = (long) expandedWidth * expandedWidth; - if (maximumRouteNodes < latticeNodes || maximumRouteNodes > 1_000_000 || maximumRouteLength < 1) { - throw new IllegalArgumentException("maximumRouteNodes must contain the complete lattice and remain bounded."); - } - if (regional.enabled() && maximumRouteLength > 32_768) { - throw new IllegalArgumentException("Regional route length cannot exceed 32768 blocks."); - } - if (minimumSurfaceCourseLength < 0 || minimumSurfaceCourseLength > 32_768 - || minimumUndergroundCourseLength < 0 || minimumUndergroundCourseLength > 32_768) { - throw new IllegalArgumentException("Hydrology course lengths are invalid."); - } - if (minimumSurfaceCourseLength > maximumRouteLength - || minimumUndergroundCourseLength > maximumRouteLength) { - throw new IllegalArgumentException("Minimum course lengths cannot exceed maximum route length."); - } - requireFiniteNonNegative(valleyPreference, "valleyPreference"); - requireFiniteNonNegative(uphillPenalty, "uphillPenalty"); - requireFiniteNonNegative(slopePenalty, "slopePenalty"); - requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction"); - requireFiniteNonNegative(lengthPreference, "lengthPreference"); - if (tributaries < 0) { - throw new IllegalArgumentException("tributaries must not be negative."); - } - } - - // Route refinement is derived from the lattice, never authored. - public static int refinementSpacing(int sampleSpacing) { - if (sampleSpacing % 4 == 0) { - return 4; - } - return sampleSpacing % 2 == 0 ? 2 : 1; - } - - public int refinementSpacing() { - return refinementSpacing(sampleSpacing); - } - - public int minimumCourseLength(boolean surface) { - return surface ? minimumSurfaceCourseLength : minimumUndergroundCourseLength; - } - } - - public record Regional( - boolean enabled, - int sampleSpacing, - int minimumLength, - int maximumTrunks, - int maximumCachedBasins, - int maximumCachedStations, - boolean coastalChannels, - double coastalChannelChance, - int maximumCoastalIncision - ) { - public Regional { - if (sampleSpacing < 128 || sampleSpacing > 1024 || Integer.bitCount(sampleSpacing) != 1 - || minimumLength < 256 || minimumLength > 32768 - || maximumTrunks < 1 || maximumTrunks > 8 - || maximumCachedBasins < 1 || maximumCachedBasins > 64 - || maximumCachedStations < 16384 || maximumCachedStations > 1048576 - || !Double.isFinite(coastalChannelChance) || coastalChannelChance < 0D || coastalChannelChance > 1D - || maximumCoastalIncision < 1 || maximumCoastalIncision > 32) { - throw new IllegalArgumentException("Regional drainage settings exceed their bounded ranges."); - } - } - - public static Regional disabled() { - return new Regional(false, 256, 2048, 2, 8, 131072, false, 0.25D, 8); - } - - public int basinSize(Routing routing) { - return Math.max(sampleSpacing * 4, Integer.highestOneBit(routing.maximumRouteLength())); - } - } - - public record Source( - boolean enabled, - double density, - int minimumElevation, - int minimumPerTile, - int maximumPerTile, - int minimumSpacing - ) { - public Source { - if (!Double.isFinite(density) || density < 0D || density > 64D) { - throw new IllegalArgumentException("Source density must be between 0 and 64 expected sources per tile."); - } - if (minimumPerTile < 0 || maximumPerTile < minimumPerTile || maximumPerTile > 4096) { - throw new IllegalArgumentException("Source tile quotas are invalid."); - } - if (minimumSpacing < 0) { - throw new IllegalArgumentException("Source spacing cannot be negative."); - } - } - } - - public record Surface( - boolean enabled, - Source sources, - int minimumWidth, - int maximumWidth, - int minimumDepth, - int maximumDepth, - int maximumIncision, - double shoreWidth, - Banks banks - ) { - public Surface { - if (sources == null || banks == null) { - throw new IllegalArgumentException("Surface source and bank settings are required."); - } - requireRange(minimumWidth, maximumWidth, 1, 256, "surface width"); - requireRange(minimumDepth, maximumDepth, 1, 64, "surface depth"); - if (maximumIncision < 0 || !Double.isFinite(shoreWidth) || shoreWidth < 0D || shoreWidth > 32D) { - throw new IllegalArgumentException("Surface incision and shore width are invalid."); - } - } - } - - /** - * {@code sink} is how many blocks the water surface sits below the shaped bank top; - * zero keeps the water flush with the bank. - * {@code channel} shapes the wet outline and {@code flow} the waterfalls and plunge basins. - */ - public record Banks( - int sink, - double blendSlope, - int minimumBlendWidth, - int maximumBlendWidth, - double roughness, - int roughnessWavelength, - int cascadeRun, - int waterfallMinimumDrop, - double mouthFlareRatio, - Inlet inlet, - double springWidthRatio, - int springLength, - boolean exposeCutStrata, - Erosion erosion, - Ponds ponds, - Channel channel, - Flow flow - ) { - // Structural invariants only; authoring bounds live in the pack validator. - public Banks { - if (erosion == null || ponds == null || inlet == null || channel == null || flow == null) { - throw new IllegalArgumentException("Surface erosion, inlet, pond, channel and flow settings are required."); - } - if (sink < 0 - || !Double.isFinite(blendSlope) || blendSlope <= 0D - || minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth - || !Double.isFinite(roughness) || roughness < 0D || roughness > 1D - || roughnessWavelength < 1 - || cascadeRun < 1 - || waterfallMinimumDrop < 1 - || !Double.isFinite(mouthFlareRatio) || mouthFlareRatio < 1D - || !Double.isFinite(springWidthRatio) || springWidthRatio < 1D - || springLength < 1) { - throw new IllegalArgumentException("Surface bank settings are invalid."); - } - } - - public static Banks defaults() { - return new Banks(0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, Inlet.defaults(), 2.5D, 24, true, Erosion.defaults(), - Ponds.defaults(), Channel.defaults(), Flow.defaults()); - } - - /** The bank settings before channel and flow shaping existed; both take their defaults. */ - public static Banks of( - int sink, - double blendSlope, - int minimumBlendWidth, - int maximumBlendWidth, - double roughness, - int roughnessWavelength, - int cascadeRun, - int waterfallMinimumDrop, - double mouthFlareRatio, - Inlet inlet, - double springWidthRatio, - int springLength, - boolean exposeCutStrata, - Erosion erosion, - Ponds ponds - ) { - return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength, - cascadeRun, waterfallMinimumDrop, mouthFlareRatio, inlet, springWidthRatio, springLength, - exposeCutStrata, erosion, ponds, Channel.defaults(), Flow.defaults()); - } - - public Banks withInlet(Inlet inlet) { - return new Banks(sink, blendSlope, minimumBlendWidth, maximumBlendWidth, roughness, roughnessWavelength, - cascadeRun, waterfallMinimumDrop, mouthFlareRatio, inlet, springWidthRatio, springLength, - exposeCutStrata, erosion, ponds, channel, flow); - } - } - - /** - * The drowned reach where a surface river meets the sea. Over the last {@code length} blocks before - * the coast the water is held at sea level, the channel widens toward the mouth flare and its bed - * deepens by {@code depth}; the stations above it grade down {@code rampSlope} blocks per station into - * the inlet. The inlet and its approach may be cut up to {@code maximumIncision} deep instead of the - * channel cap, so a coastal rise no longer rejects the course; a rise the cap cannot pass ends the - * inlet there, and the inlet never takes more than {@code courseFraction} of the exposed course. A - * zero length is the plain crossing: the head drops to sea level at the coast, the channel neither - * widens nor deepens. - */ - public record Inlet( - int length, - int depth, - int maximumIncision, - double courseFraction, - double rampSlope - ) { - public Inlet { - if (length < 0 || length > 1024 || depth < 0 || depth > 64 || maximumIncision < 0 || maximumIncision > 512) { - throw new IllegalArgumentException("Surface inlet settings are invalid."); - } - if (!Double.isFinite(courseFraction) || courseFraction <= 0D || courseFraction > 1D - || !Double.isFinite(rampSlope) || rampSlope <= 0D) { - throw new IllegalArgumentException("Surface inlet course fraction and ramp slope are invalid."); - } - } - - public static Inlet none() { - return new Inlet(0, 0, 0, 0.5D, 1D); - } - - public static Inlet defaults() { - return new Inlet(64, 3, 32, 0.5D, 1D); - } - - /** The inlet reach with the default course fraction and ramp slope. */ - public static Inlet of(int length, int depth, int maximumIncision) { - return new Inlet(length, depth, maximumIncision, 0.5D, 1D); - } - } - - /** - * How the ground around a channel is eroded into a valley. {@code smoothingRadius} is the run of - * stations the blend width is averaged over, {@code thalwegFraction} the share of the channel - * half-width that stays at full bed depth, {@code blendCurve} the exponent on the blend progress - * (below one hollows the valley sides, above one steepens them near the shore), and - * {@code bedNoise} the share of the channel roughness applied to the bed. Disabled erosion keeps - * only the wet channel, the shore band and the containing lip. {@code style} is the curve of the - * valley side ({@code terraceSteps} and {@code cliffFraction} shape only the terraced and cliff - * styles), {@code bedProfile} the wet bed cross-section, {@code shoreRise} how many blocks the shore - * bench climbs from the water to the valley foot, and {@code blendBaseWidth} a width added to every - * valley blend before its bounds apply. - */ - public record Erosion( - boolean enabled, - int smoothingRadius, - double thalwegFraction, - double blendCurve, - double bedNoise, - IrisRiverBlendStyle style, - int terraceSteps, - double cliffFraction, - IrisRiverBedProfile bedProfile, - double shoreRise, - double blendBaseWidth, - Excavation excavation - ) { - public Erosion { - Objects.requireNonNull(excavation, "excavation"); - if (smoothingRadius < 0 - || !Double.isFinite(thalwegFraction) || thalwegFraction < 0D || thalwegFraction >= 1D - || !Double.isFinite(blendCurve) || blendCurve <= 0D - || !Double.isFinite(bedNoise) || bedNoise < 0D) { - throw new IllegalArgumentException("Surface erosion settings are invalid."); - } - if (style == null || bedProfile == null - || terraceSteps < 2 - || !Double.isFinite(cliffFraction) || cliffFraction < 0D || cliffFraction > 1D - || !Double.isFinite(shoreRise) || shoreRise < 0D - || !Double.isFinite(blendBaseWidth) || blendBaseWidth < 0D) { - throw new IllegalArgumentException("Surface erosion style settings are invalid."); - } - } - - public static Erosion defaults() { - return new Erosion(true, 12, 0.45D, 1D, 0.5D, IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, Excavation.defaults()); - } - - /** The erosion amounts with the smooth valley side, bowl bed and flat shore bench they always had. */ - public static Erosion of(boolean enabled, int smoothingRadius, double thalwegFraction, double blendCurve, double bedNoise) { - return new Erosion(enabled, smoothingRadius, thalwegFraction, blendCurve, bedNoise, - IrisRiverBlendStyle.SMOOTH, 4, 0.5D, IrisRiverBedProfile.BOWL, 0D, 0D, Excavation.defaults()); - } - - // Enum hash codes are identity based and change between runs; the plan fingerprint mixes this hash in. - @Override - public int hashCode() { - return Objects.hash(enabled, smoothingRadius, thalwegFraction, blendCurve, bedNoise, style.name(), - terraceSteps, cliffFraction, bedProfile.name(), shoreRise, blendBaseWidth, excavation); - } - } - - public record Excavation(int maximumDepth, int maximumWidth, int maximumVolumePerBlock) { - public Excavation { - if (maximumDepth < 0 || maximumDepth > 64 || maximumWidth < 1 || maximumWidth > 64 - || maximumVolumePerBlock < 0 || maximumVolumePerBlock > 8192) { - throw new IllegalArgumentException("Surface bank excavation limits are invalid."); - } - } - - public static Excavation defaults() { - return new Excavation(8, 16, 256); - } - } - - /** - * The wet outline of a surface channel. {@code smoothingRadius} is the run of stations the profile is - * averaged over, {@code outlineMinimumRatio} and {@code outlineMaximumRatio} bound the outline against - * the planned half-width, and {@code springExtraDepth} is how much of the remaining spring taper is - * added to the bed depth at a spring. - */ - public record Channel( - int smoothingRadius, - double outlineMinimumRatio, - double outlineMaximumRatio, - double springExtraDepth - ) { - public Channel { - if (smoothingRadius < 0 - || !Double.isFinite(outlineMinimumRatio) || outlineMinimumRatio <= 0D || outlineMinimumRatio > 1D - || !Double.isFinite(outlineMaximumRatio) || outlineMaximumRatio < 1D - || !Double.isFinite(springExtraDepth) || springExtraDepth < 0D) { - throw new IllegalArgumentException("Surface channel settings are invalid."); - } - } - - public static Channel defaults() { - return new Channel(16, 0.6D, 1.4D, 1D); - } - } - - /** - * Waterfalls and plunge basins on a surface river. {@code waterfallThalwegFraction} is the share of the - * half-width kept at full depth over a fall; a drop of at least {@code plungeBasinMinimumDrop} blocks - * scours a basin {@code plungeBasinLengthRatio} half-widths long and {@code plungeBasinDepth} blocks - * deeper than the bed. - */ - public record Flow( - double waterfallThalwegFraction, - int plungeBasinMinimumDrop, - double plungeBasinLengthRatio, - int plungeBasinDepth - ) { - public Flow { - if (!Double.isFinite(waterfallThalwegFraction) || waterfallThalwegFraction < 0D || waterfallThalwegFraction >= 1D - || plungeBasinMinimumDrop < 1 - || !Double.isFinite(plungeBasinLengthRatio) || plungeBasinLengthRatio < 0D - || plungeBasinDepth < 0) { - throw new IllegalArgumentException("Surface flow settings are invalid."); - } - } - - public static Flow defaults() { - return new Flow(0.65D, 2, 2D, 1); - } - } - - /** A pond at one end of a surface course: a shaped bowl holding the course's head at that end. */ - public record Pond(boolean enabled, int minimumRadius, int maximumRadius, int depth) { - public Pond { - if (minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1) { - throw new IllegalArgumentException("Surface pond settings are invalid."); - } - } - } - - /** The ponds at a surface course's source and at an inland terminal. */ - public record Ponds(Pond source, Pond terminal) { - public Ponds { - if (source == null || terminal == null) { - throw new IllegalArgumentException("Surface pond settings are required."); - } - } - - public static Ponds defaults() { - return new Ponds(new Pond(true, 6, 12, 3), new Pond(true, 4, 7, 3)); - } - - /** No pond at either end: what a standing pool or a test channel asks for. */ - public static Ponds none() { - return new Ponds(new Pond(false, 1, 1, 1), new Pond(false, 1, 1, 1)); - } - } - - /** A standing surface pool profile: a bowl of the given radius and depth, filled with its own fluid. */ - public record SurfacePool( - String id, - boolean enabled, - double density, - int spacing, - int minimumRadius, - int maximumRadius, - int depth, - int maximumPerTile, - String biomeKey - ) { - public SurfacePool { - if (id == null || id.isBlank()) { - throw new IllegalArgumentException("Surface pool id must not be blank."); - } - id = id.trim(); - if (!Double.isFinite(density) || density < 0D || spacing < 1 - || minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1 || maximumPerTile < 0) { - throw new IllegalArgumentException("Surface pool settings are invalid."); - } - biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey.trim(); - } - } - - /** - * Sea caves: coastal grottos that open from the ocean into the coast without a river. A tile keeps at - * most {@code maximumPerTile} of them, the steepest owned coast first, at least {@code minimumSpacing} - * apart, only where the coast stands {@code minimumCoastHeight} above the sea, each chamber swept - * {@code depth} blocks inland from the shoreline, its sweep turned up to {@code sweepJitterDegrees} - * either side of the inland normal. Chamber size and volume come from the coastal grotto. - */ - public record SeaCaves( - boolean enabled, - int maximumPerTile, - int minimumSpacing, - int minimumCoastHeight, - int depth, - double sweepJitterDegrees - ) { - public SeaCaves { - if (maximumPerTile < 0 || maximumPerTile > 64 - || minimumSpacing < 16 || minimumSpacing > 8192 - || minimumCoastHeight < 1 || minimumCoastHeight > 128 - || depth < 0 || depth > 128 - || !Double.isFinite(sweepJitterDegrees) || sweepJitterDegrees < 0D || sweepJitterDegrees > 180D) { - throw new IllegalArgumentException("Sea cave bounds are invalid."); - } - } - - public static SeaCaves disabled() { - return new SeaCaves(false, 0, 16, 1, 0, 25D); - } - - /** The chamber budget with the default sweep jitter. */ - public static SeaCaves of(boolean enabled, int maximumPerTile, int minimumSpacing, int minimumCoastHeight, int depth) { - return new SeaCaves(enabled, maximumPerTile, minimumSpacing, minimumCoastHeight, depth, 25D); - } - } - - // The routing gates read one hydraulic threshold: the drop that makes a reach a waterfall. - public record Hydraulics(int waterfallMinimumDrop) { - public Hydraulics { - if (waterfallMinimumDrop < 1) { - throw new IllegalArgumentException("waterfallMinimumDrop must be at least one block."); - } - } - } - - public record Underground( - boolean enabled, - Source sources, - int minimumFluidY, - int maximumFluidY, - int minimumWidth, - int maximumWidth, - int minimumDepth, - int maximumDepth, - int minimumHeadroom, - int maximumHeadroom, - boolean connectToExistingCaves, - int tributaries, - int minimumRockCover, - int minimumFloorCover, - int wideningSources - ) { - public Underground { - if (sources == null || minimumFluidY > maximumFluidY) { - throw new IllegalArgumentException("Underground source and height settings are invalid."); - } - requireRange(minimumWidth, maximumWidth, 1, 256, "underground width"); - requireRange(minimumDepth, maximumDepth, 1, 64, "underground depth"); - requireRange(minimumHeadroom, maximumHeadroom, 1, 128, "underground headroom"); - if (tributaries < 0 || tributaries > 4) { - throw new IllegalArgumentException("underground tributaries must be between 0 and 4."); - } - if (minimumRockCover < 1 || minimumRockCover > 256 - || minimumFloorCover < 1 || minimumFloorCover > 256 - || wideningSources < 1 || wideningSources > 1024) { - throw new IllegalArgumentException("Underground rock cover and widening settings are invalid."); - } - } - - /** One block of rock above and below a tunnel, and widening that saturates at eight sources. */ - public static Underground of( - boolean enabled, - Source sources, - int minimumFluidY, - int maximumFluidY, - int minimumWidth, - int maximumWidth, - int minimumDepth, - int maximumDepth, - int minimumHeadroom, - int maximumHeadroom, - boolean connectToExistingCaves, - int tributaries - ) { - return new Underground(enabled, sources, minimumFluidY, maximumFluidY, minimumWidth, maximumWidth, minimumDepth, - maximumDepth, minimumHeadroom, maximumHeadroom, connectToExistingCaves, tributaries, 1, 1, 8); - } - } - - public record Outlets( - boolean oceanEnabled, - Grotto coastalGrotto, - Grotto inlandGrotto, - boolean surfaceSinkholesEnabled, - int coastalCliffMinimumHeight, - int mouthLevelingDistance, - int maximumOceanApron, - int maximumPerTile, - int maximumCoastalPerTile, - double coastalCliffSlopeFactor - ) { - public Outlets { - if (coastalGrotto == null || inlandGrotto == null) { - throw new IllegalArgumentException("Coastal and inland grotto settings are required."); - } - if (surfaceSinkholesEnabled && !inlandGrotto.enabled()) { - throw new IllegalArgumentException("Surface sinkholes require inland grotto outlets."); - } - if (coastalCliffMinimumHeight < 0 || maximumOceanApron < 0 || maximumOceanApron > 64 - || mouthLevelingDistance < 0 || maximumPerTile < 1 || maximumPerTile > 256 - || maximumCoastalPerTile < 0 || maximumCoastalPerTile > 256) { - throw new IllegalArgumentException("Outlet bounds are invalid."); - } - requireFiniteNonNegative(coastalCliffSlopeFactor, "coastalCliffSlopeFactor"); - } - - /** The outlet budget with the default coastal cliff slope factor. */ - public static Outlets of( - boolean oceanEnabled, - Grotto coastalGrotto, - Grotto inlandGrotto, - boolean surfaceSinkholesEnabled, - int coastalCliffMinimumHeight, - int mouthLevelingDistance, - int maximumOceanApron, - int maximumPerTile, - int maximumCoastalPerTile - ) { - return new Outlets(oceanEnabled, coastalGrotto, inlandGrotto, surfaceSinkholesEnabled, coastalCliffMinimumHeight, - mouthLevelingDistance, maximumOceanApron, maximumPerTile, maximumCoastalPerTile, 0.5D); - } - } - - public record Grotto( - boolean enabled, - int horizontalRadius, - int verticalRadius, - int headroom, - int maximumVolume - ) { - public Grotto { - if (horizontalRadius < 1 || horizontalRadius > 128 || verticalRadius < 1 || verticalRadius > 128 - || headroom < 1 || headroom > 128 || maximumVolume < 1 || maximumVolume > 1_048_576) { - throw new IllegalArgumentException("Grotto bounds are invalid."); - } - } - } - - public record Geometry( - Meanders meanders, - ChannelShape surface, - ChannelShape underground, - ChannelShape grottos, - Drops drops - ) { - public Geometry { - if (meanders == null || surface == null || underground == null || grottos == null || drops == null) { - throw new IllegalArgumentException("Hydrology geometry settings cannot contain null sections."); - } - } - - public static Geometry defaults() { - ChannelShape channel = ChannelShape.of(2.4D, 0.28D, 0.24D, 11); - return new Geometry( - new Meanders(64, 12, 0.34D, 0.42D, 0.48D, 1, 82D), - channel, - channel, - channel, - Drops.of(2, 1.4D, 2, 0.45D, 2, 1.8D, 8) - ); - } - } - - public record Meanders( - int primaryWavelength, - int detailWavelength, - double primaryStrength, - double detailStrength, - double maximumOffsetRatio, - int smoothingPasses, - double maximumTurnDegrees - ) { - public Meanders { - if (primaryWavelength < 8 || primaryWavelength > 512 - || detailWavelength < 4 || detailWavelength > 128 - || detailWavelength > primaryWavelength - || smoothingPasses < 0 || smoothingPasses > 4 - || !Double.isFinite(primaryStrength) || primaryStrength < 0D || primaryStrength > 2D - || !Double.isFinite(detailStrength) || detailStrength < 0D || detailStrength > 2D - || !Double.isFinite(maximumOffsetRatio) || maximumOffsetRatio < 0D || maximumOffsetRatio > 1D - || !Double.isFinite(maximumTurnDegrees) - || maximumTurnDegrees < 10D || maximumTurnDegrees > 150D) { - throw new IllegalArgumentException("Hydrology meander geometry is invalid."); - } - } - } - - /** - * The cross-section of a carved channel. The bed and wall roughness shape every channel; the radial - * and lobe terms shape the organic wall outline of an underground channel or grotto around - * {@code radialBase}, clamped to {@code [radialMinimum, radialMaximum]}; {@code ceilingRoughness} - * roughens a tunnel ceiling; {@code aspectMinimum} and {@code aspectRange} bound a grotto's plan aspect. - */ - public record ChannelShape( - double bedRoundness, - double bedRoughness, - double wallRoughness, - int roughnessWavelength, - double radialBase, - double radialMinimum, - double radialMaximum, - double primaryLobeStrength, - double detailLobeStrength, - double ceilingRoughness, - double aspectMinimum, - double aspectRange - ) { - public ChannelShape { - if (!Double.isFinite(bedRoundness) || bedRoundness < 1D || bedRoundness > 6D - || !Double.isFinite(bedRoughness) || bedRoughness < 0D || bedRoughness > 1D - || !Double.isFinite(wallRoughness) || wallRoughness < 0D || wallRoughness > 1D - || roughnessWavelength < 3 || roughnessWavelength > 128) { - throw new IllegalArgumentException("Hydrology channel shape is invalid."); - } - if (!Double.isFinite(radialBase) || radialBase < 0.1D || radialBase > 4D - || !Double.isFinite(radialMinimum) || radialMinimum <= 0D - || !Double.isFinite(radialMaximum) || radialMaximum < radialMinimum || radialMaximum > 4D - || !Double.isFinite(primaryLobeStrength) || primaryLobeStrength < 0D || primaryLobeStrength > 1D - || !Double.isFinite(detailLobeStrength) || detailLobeStrength < 0D || detailLobeStrength > 1D - || !Double.isFinite(ceilingRoughness) || ceilingRoughness < 0D || ceilingRoughness > 1D - || !Double.isFinite(aspectMinimum) || aspectMinimum <= 0D || aspectMinimum > 1D - || !Double.isFinite(aspectRange) || aspectRange < 0D || aspectRange > 1D) { - throw new IllegalArgumentException("Hydrology channel wall shape is invalid."); - } - } - - /** The bed and wall roughness with the organic wall outline every channel had before it was configurable. */ - public static ChannelShape of(double bedRoundness, double bedRoughness, double wallRoughness, int roughnessWavelength) { - return new ChannelShape(bedRoundness, bedRoughness, wallRoughness, roughnessWavelength, - 0.86D, 0.58D, 1.18D, 0.08D, 0.06D, 0D, 0.62D, 0.2D); - } - } - - public record Drops( - int cascadeRunPerBlock, - double cascadeExponent, - int maximumCascadeStep, - double flowWidthRatio, - int maximumFlowDepth, - double basinWidthRatio, - int maximumBasinDepth, - int undergroundCascadeRunPerBlock - ) { - public Drops { - if (cascadeRunPerBlock < 1 || cascadeRunPerBlock > 16 - || !Double.isFinite(cascadeExponent) || cascadeExponent < 0.25D || cascadeExponent > 6D - || maximumCascadeStep < 1 || maximumCascadeStep > 4 - || !Double.isFinite(flowWidthRatio) || flowWidthRatio < 0.25D || flowWidthRatio > 1D - || maximumFlowDepth < 1 || maximumFlowDepth > 16 - || !Double.isFinite(basinWidthRatio) || basinWidthRatio < 1D || basinWidthRatio > 4D - || maximumBasinDepth < maximumFlowDepth || maximumBasinDepth > 32) { - throw new IllegalArgumentException("Hydrology drop geometry is invalid."); - } - if (undergroundCascadeRunPerBlock < 0 || undergroundCascadeRunPerBlock > 64) { - throw new IllegalArgumentException("undergroundCascadeRunPerBlock must be between 0 and 64."); - } - } - - /** The drop geometry with the underground cascade run left at the routing minimum. */ - public static Drops of( - int cascadeRunPerBlock, - double cascadeExponent, - int maximumCascadeStep, - double flowWidthRatio, - int maximumFlowDepth, - double basinWidthRatio, - int maximumBasinDepth - ) { - return new Drops(cascadeRunPerBlock, cascadeExponent, maximumCascadeStep, flowWidthRatio, maximumFlowDepth, - basinWidthRatio, maximumBasinDepth, 0); - } - - public int flowWidth(int channelWidth) { - return Math.max(1, Math.min(channelWidth, (int) StrictMath.ceil(channelWidth * flowWidthRatio))); - } - - public int stepLimit(HydrologyFeatureType type) { - return type.isSurface() ? 1 : maximumCascadeStep; - } - - public int flowDepth(int channelDepth) { - return Math.max(1, Math.min(channelDepth, maximumFlowDepth)); - } - - public int basinWidth(int flowWidth) { - return Math.max(flowWidth, (int) StrictMath.ceil(flowWidth * basinWidthRatio)); - } - - public int basinDepth(int flowDepth, int drop) { - int erodedDepth = Math.addExact(flowDepth, Math.max(1, (int) StrictMath.ceil(StrictMath.sqrt(drop)))); - return Math.max(flowDepth, Math.min(maximumBasinDepth, erodedDepth)); - } - } - - public record DeepFluid( - String id, - boolean enabled, - double density, - int spacing, - int minimumY, - int maximumY, - int minimumHorizontalRadius, - int maximumHorizontalRadius, - int minimumVerticalRadius, - int maximumVerticalRadius, - int minimumChannelLength, - int maximumChannelLength, - int channelWidth, - int channelDepth, - int headroom, - int maximumVolume, - int maximumPerTile, - boolean containedPools, - boolean shortChannels - ) { - public DeepFluid { - if (id == null || id.isBlank()) { - throw new IllegalArgumentException("Deep-fluid id must not be blank."); - } - id = id.trim(); - if (!Double.isFinite(density) || density < 0D || density > 64D || spacing < 16 || minimumY > maximumY) { - throw new IllegalArgumentException("Deep-fluid density or height range is invalid."); - } - requireRange(minimumHorizontalRadius, maximumHorizontalRadius, 1, 128, "deep-fluid horizontal radius"); - requireRange(minimumVerticalRadius, maximumVerticalRadius, 1, 128, "deep-fluid vertical radius"); - requireRange(minimumChannelLength, maximumChannelLength, 0, 32_768, "deep-fluid channel length"); - if (channelWidth < 1 || channelWidth > 256 || channelDepth < 1 || channelDepth > 64 || headroom < 1 - || maximumVolume < 64 || maximumVolume > 1_048_576 - || maximumPerTile < 0 || maximumPerTile > 1024) { - throw new IllegalArgumentException("Deep-fluid channel bounds are invalid."); - } - long poolEnvelope = ellipsoidVolume( - maximumHorizontalRadius, - Math.max(channelDepth, maximumVerticalRadius), - headroom - ); - if (enabled && containedPools && poolEnvelope > maximumVolume) { - throw new IllegalArgumentException("Deep-fluid maximumVolume cannot contain its configured pool ellipsoid."); - } - if (enabled && !containedPools && !shortChannels) { - throw new IllegalArgumentException("An enabled deep fluid requires a pool or channel."); - } - if (enabled && shortChannels && minimumChannelLength < 1) { - throw new IllegalArgumentException("Enabled deep-fluid channels require a positive minimum length."); - } - } - } - - private static void requireRange(int minimum, int maximum, int lower, int upper, String name) { - if (minimum < lower || maximum < minimum || maximum > upper) { - throw new IllegalArgumentException(name + " range is invalid."); - } - } - - private static void requireFiniteNonNegative(double value, String name) { - if (!Double.isFinite(value) || value < 0D) { - throw new IllegalArgumentException(name + " must be finite and non-negative."); - } - } - - static long ellipsoidVolume(int radius, int lowerExtent, int upperExtent) { - long volume = 0L; - for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) { - for (int deltaX = -radius; deltaX <= radius; deltaX++) { - double distance = StrictMath.hypot(deltaX, deltaZ); - if (distance > radius + 0.25D) { - continue; - } - double normalized = Math.min(1D, distance / Math.max(1D, radius)); - double scale = StrictMath.sqrt(Math.max(0D, 1D - normalized * normalized)); - int lower = (int) StrictMath.ceil(lowerExtent * scale); - int upper = (int) StrictMath.floor(upperExtent * scale); - volume += lower + upper + 1L; - } - } - return volume; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanningAdmission.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanningAdmission.java deleted file mode 100644 index e2f53a068..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanningAdmission.java +++ /dev/null @@ -1,136 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.Objects; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BooleanSupplier; - -final class HydrologyPlanningAdmission { - private static final long BYTES_PER_ROOT = 2L * 1024L * 1024L * 1024L; - private static final long BYTES_PER_TRIAL = 128L * 1024L * 1024L; - private static final int MAXIMUM_TRIAL_LIMIT = 16; - private static final int MAXIMUM_ROOTS = rootLimit( - Runtime.getRuntime().maxMemory(), Runtime.getRuntime().availableProcessors()); - private static final int MAXIMUM_TRIALS = trialLimit(Runtime.getRuntime().maxMemory(), MAXIMUM_ROOTS); - private static final HydrologyPlanningAdmission ROOTS = new HydrologyPlanningAdmission(MAXIMUM_ROOTS); - - private final Semaphore permits; - - HydrologyPlanningAdmission(int maximumRoots) { - if (maximumRoots < 1) { - throw new IllegalArgumentException("Maximum hydrology roots must be positive."); - } - permits = new Semaphore(maximumRoots, true); - } - - static Permit acquireRoot(BooleanSupplier cancelled) { - return ROOTS.acquire(cancelled); - } - - static int maximumRoots() { - return MAXIMUM_ROOTS; - } - - static int maximumTrials() { - return MAXIMUM_TRIALS; - } - - static int effectiveParallelism(int poolParallelism) { - return effectiveParallelism(poolParallelism, Runtime.getRuntime().availableProcessors()); - } - - static int effectiveParallelism(int poolParallelism, int availableProcessors) { - return Math.max(1, Math.min(poolParallelism, availableProcessors)); - } - - static int trialLimit(long maximumHeap, int maximumRoots) { - if (maximumRoots < 1) { - throw new IllegalArgumentException("Maximum hydrology roots must be positive."); - } - return (int) Math.clamp(maximumHeap / maximumRoots / BYTES_PER_TRIAL, 1L, MAXIMUM_TRIAL_LIMIT); - } - - static int rootLimit(long maximumHeap, int processors) { - return (int) Math.max(1L, Math.min(Math.min(8L, processors), maximumHeap / BYTES_PER_ROOT)); - } - - Permit acquire(BooleanSupplier cancelled) { - AdmissionWait wait = new AdmissionWait(Objects.requireNonNull(cancelled)); - try { - ForkJoinPool.managedBlock(wait); - wait.requireActive(); - return new Permit(permits); - } catch (InterruptedException interruption) { - Thread.currentThread().interrupt(); - if (wait.acquired) { - permits.release(); - } - throw new CancellationException("Hydrology planning admission interrupted."); - } catch (RuntimeException | Error failure) { - if (wait.acquired) { - permits.release(); - } - throw failure; - } - } - - static final class Permit implements AutoCloseable { - private final Semaphore permits; - private final AtomicBoolean released = new AtomicBoolean(); - - private Permit(Semaphore permits) { - this.permits = permits; - } - - @Override - public void close() { - if (released.compareAndSet(false, true)) { - permits.release(); - } - } - } - - private final class AdmissionWait implements ForkJoinPool.ManagedBlocker { - private final BooleanSupplier cancelled; - private boolean acquired; - - private AdmissionWait(BooleanSupplier cancelled) { - this.cancelled = cancelled; - } - - @Override - public boolean isReleasable() { - requireActive(); - if (!acquired) { - try { - acquired = permits.tryAcquire(0L, TimeUnit.NANOSECONDS); - } catch (InterruptedException interruption) { - Thread.currentThread().interrupt(); - throw new CancellationException("Hydrology planning admission interrupted."); - } - } - return acquired; - } - - @Override - public boolean block() throws InterruptedException { - requireActive(); - if (!acquired) { - acquired = permits.tryAcquire(100L, TimeUnit.MILLISECONDS); - } - return acquired; - } - - private void requireActive() { - if (Thread.currentThread().isInterrupted()) { - throw new CancellationException("Hydrology planning admission interrupted."); - } - if (cancelled.getAsBoolean()) { - throw new CancellationException("Hydrology tile cache is closed."); - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPoint.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPoint.java deleted file mode 100644 index cc38429ca..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPoint.java +++ /dev/null @@ -1,13 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -public record HydrologyPoint(int x, int y, int z) { - public long packedColumn() { - return RiverFootprint.pack(x, z); - } - - public long distanceSquared2D(HydrologyPoint other) { - long deltaX = (long) x - other.x; - long deltaZ = (long) z - other.z; - return deltaX * deltaX + deltaZ * deltaZ; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalCoarsePlan.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalCoarsePlan.java deleted file mode 100644 index 3ada89387..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalCoarsePlan.java +++ /dev/null @@ -1,34 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.hydrology.surface.SurfaceBounds; - -import java.util.List; - -record HydrologyRegionalCoarsePlan( - HydrologySampledGrid grid, - List outlets, - HydrologyRegionalGraph.Tree tree, - List sources, - List envelopes, - double longest, - boolean coastal, - int weight -) { - HydrologyRegionalCoarsePlan { - outlets = List.copyOf(outlets); - sources = List.copyOf(sources); - envelopes = List.copyOf(envelopes); - } - - boolean intersects(SurfaceBounds bounds) { - if (coastal) { - return true; - } - for (HydrologyRegionalEnvelope envelope : envelopes) { - if (envelope.intersects(bounds)) { - return true; - } - } - return false; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalConnectivity.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalConnectivity.java deleted file mode 100644 index f1713cfb1..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalConnectivity.java +++ /dev/null @@ -1,246 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline; -import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn; -import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; -import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongArrayList; -import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; - -import java.util.List; - -final class HydrologyRegionalConnectivity { - private static final int[][] NEIGHBORS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - - private final Long2LongOpenHashMap water = new Long2LongOpenHashMap(); - private final Long2ObjectOpenHashMap disjoint = new Long2ObjectOpenHashMap<>(); - - void add(List columns) { - for (SurfaceLayerColumn column : columns) { - HydrologyColumnLayer layer = column.layer(); - if (layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) { - addFluidColumn(column.x(), column.z(), layer.bedY(), layer.fluidHeadY()); - } - } - } - - void add(HydrologyColumnSample column) { - for (HydrologyColumnLayer layer : column.layers()) { - if (layer.fluidOwned() && layer.bedY() < layer.fluidHeadY()) { - addFluidColumn(column.x(), column.z(), layer.bedY(), layer.fluidHeadY()); - } - } - } - - void addFluidColumn(int x, int z, int bed, int head) { - long key = RiverFootprint.pack(x, z); - long interval = levels(bed, head); - if (!water.containsKey(key)) { - water.put(key, interval); - return; - } - long previous = water.get(key); - LongArrayList gaps = disjoint.get(key); - if (gaps == null && bed <= (int) previous && (int) (previous >> 32) <= head) { - water.put(key, levels(Math.min(bed, (int) (previous >> 32)), Math.max(head, (int) previous))); - return; - } - if (gaps == null) { - gaps = new LongArrayList(3); - gaps.add(previous); - disjoint.put(key, gaps); - } - gaps.add(interval); - gaps.sort(null); - int retained = 1; - for (int index = 1; index < gaps.size(); index++) { - long first = gaps.getLong(retained - 1); - long next = gaps.getLong(index); - if ((int) (next >> 32) <= (int) first) { - gaps.set(retained - 1, levels((int) (first >> 32), Math.max((int) first, (int) next))); - } else { - gaps.set(retained++, next); - } - } - gaps.size(retained); - water.put(key, gaps.getLong(0)); - if (retained == 1) { - disjoint.remove(key); - } - } - - boolean connected(RiverCourse course, HydrologyTerrainSampler sampler, HydrologyPlannerSettings settings) { - if (water.isEmpty()) { - return false; - } - LongOpenHashSet visited = new LongOpenHashSet(water.size()); - if (!disjoint.isEmpty()) { - if (!connectedIntervals(visited)) { - return false; - } - return reachesTerminal(course, sampler, settings, visited); - } - LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); - long start = water.keySet().iterator().nextLong(); - queue.enqueue(start); - visited.add(start); - while (!queue.isEmpty()) { - long current = queue.dequeueLong(); - long currentLevels = water.get(current); - int currentBed = (int) (currentLevels >> 32); - int currentHead = (int) currentLevels; - int x = RiverFootprint.unpackX(current); - int z = RiverFootprint.unpackZ(current); - for (int[] offset : NEIGHBORS) { - long next = RiverFootprint.pack(x + offset[0], z + offset[1]); - if (!water.containsKey(next) || visited.contains(next)) { - continue; - } - long levels = water.get(next); - if (Math.max(currentBed, (int) (levels >> 32)) >= Math.min(currentHead, (int) levels)) { - continue; - } - visited.add(next); - queue.enqueue(next); - } - } - if (visited.size() != water.size()) { - return false; - } - return reachesTerminal(course, sampler, settings, visited); - } - - private boolean connectedIntervals(LongOpenHashSet visited) { - int count = water.size(); - for (LongArrayList intervals : disjoint.values()) { - count += intervals.size() - 1; - } - long[] columns = new long[count]; - long[] intervals = new long[count]; - Long2IntOpenHashMap starts = new Long2IntOpenHashMap(water.size()); - starts.defaultReturnValue(-1); - int index = 0; - for (long column : water.keySet()) { - starts.put(column, index); - LongArrayList layers = disjoint.get(column); - int size = layers == null ? 1 : layers.size(); - for (int layer = 0; layer < size; layer++) { - columns[index] = column; - intervals[index++] = layers == null ? water.get(column) : layers.getLong(layer); - } - } - boolean[] reached = new boolean[count]; - IntArrayFIFOQueue queue = new IntArrayFIFOQueue(); - queue.enqueue(0); - reached[0] = true; - visited.add(columns[0]); - int reachedCount = 1; - while (!queue.isEmpty()) { - int current = queue.dequeueInt(); - int currentBed = (int) (intervals[current] >> 32); - int currentHead = (int) intervals[current]; - int x = RiverFootprint.unpackX(columns[current]); - int z = RiverFootprint.unpackZ(columns[current]); - for (int[] offset : NEIGHBORS) { - long nextColumn = RiverFootprint.pack(x + offset[0], z + offset[1]); - int start = starts.get(nextColumn); - if (start < 0) { - continue; - } - LongArrayList layers = disjoint.get(nextColumn); - int end = start + (layers == null ? 1 : layers.size()); - for (int next = start; next < end; next++) { - if (reached[next] || Math.max(currentBed, (int) (intervals[next] >> 32)) - >= Math.min(currentHead, (int) intervals[next])) { - continue; - } - reached[next] = true; - reachedCount++; - visited.add(nextColumn); - queue.enqueue(next); - } - } - } - return reachedCount == count; - } - - private boolean reachesTerminal(RiverCourse course, HydrologyTerrainSampler sampler, - HydrologyPlannerSettings settings, LongOpenHashSet visited) { - HydraulicSegment first = course.segments().getFirst(); - HydraulicSegment last = course.segments().getLast(); - return reaches(last, false, sampler, settings, visited) - && (first.type() != HydrologyFeatureType.MOUTH || reaches(first, true, sampler, settings, visited)); - } - - private boolean reaches(HydraulicSegment mouth, boolean upstream, HydrologyTerrainSampler sampler, - HydrologyPlannerSettings settings, LongOpenHashSet visited) { - int seaLevel = settings.seaLevel(); - if (mouth.type() != HydrologyFeatureType.MOUTH || mouth.upstreamHeadY() != seaLevel - || mouth.downstreamHeadY() != seaLevel) { - return false; - } - HydrologyPoint endpoint = upstream ? mouth.start() : mouth.end(); - HydrologyTerrainSample terrain = sampler.sample(endpoint.x(), endpoint.z()); - if (terrain == null || !terrain.ocean() || terrain.naturalHeight() >= seaLevel) { - return false; - } - HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forMouth(settings, sampler, mouth); - SurfaceCenterline centerline = SurfaceCenterline.densify(mouth.centerline()); - int step = upstream ? 1 : -1; - int previousX = endpoint.x(); - int previousZ = endpoint.z(); - for (int station = upstream ? 0 : centerline.size() - 1; station >= 0 && station < centerline.size(); station += step) { - int x = centerline.x()[station]; - int z = centerline.z()[station]; - if (x != previousX && z != previousZ) { - boolean firstBridge = receiver.receivingWater(previousX, z, seaLevel); - boolean secondBridge = receiver.receivingWater(x, previousZ, seaLevel); - if (!firstBridge && !secondBridge) { - return false; - } - if (firstBridge && touchesOwnedWater(previousX, z, seaLevel, visited) - || secondBridge && touchesOwnedWater(x, previousZ, seaLevel, visited)) { - return true; - } - } - if (!receiver.receivingWater(x, z, seaLevel)) { - return false; - } - if (touchesOwnedWater(x, z, seaLevel, visited)) { - return true; - } - previousX = x; - previousZ = z; - } - return false; - } - - private boolean touchesOwnedWater(int x, int z, int seaLevel, LongOpenHashSet visited) { - for (int[] offset : NEIGHBORS) { - long key = RiverFootprint.pack(x + offset[0], z + offset[1]); - if (!visited.contains(key)) { - continue; - } - LongArrayList layers = disjoint.get(key); - if (layers == null) { - if ((int) water.get(key) == seaLevel) { - return true; - } - continue; - } - for (int index = 0; index < layers.size(); index++) { - if ((int) layers.getLong(index) == seaLevel) { - return true; - } - } - } - return false; - } - - private static long levels(int bed, int head) { - return ((long) bed << 32) | (head & 0xffffffffL); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalCourseGraph.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalCourseGraph.java deleted file mode 100644 index 3ae535761..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalCourseGraph.java +++ /dev/null @@ -1,65 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import it.unimi.dsi.fastutil.ints.IntArrayList; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -record HydrologyRegionalCourseGraph(List nodes, List edges, int discharge) { - private static final long NODE_SALT = 0x5245475055424c49L; - - HydrologyRegionalCourseGraph { - nodes = List.copyOf(nodes); - edges = List.copyOf(edges); - } - - static HydrologyRegionalCourseGraph create(HydrologyTerrainSampler sampler, Options options) { - List path = options.path(); - double[] distance = new double[path.size()]; - IntArrayList selected = new IntArrayList(); - selected.add(0); - double next = options.spacing(); - for (int index = 1; index < path.size(); index++) { - HydrologyPoint previous = path.get(index - 1); - HydrologyPoint point = path.get(index); - distance[index] = distance[index - 1] + StrictMath.hypot( - (double) point.x() - previous.x(), (double) point.z() - previous.z()); - if (distance[index] >= next || index == path.size() - 1) { - selected.add(index); - next = distance[index] + options.spacing(); - } - } - ArrayList nodes = new ArrayList<>(selected.size()); - ArrayList edges = new ArrayList<>(Math.max(0, selected.size() - 1)); - double length = distance[distance.length - 1]; - int contribution = 1; - for (int index = 0; index < selected.size(); index++) { - int station = selected.getInt(index); - HydrologyPoint point = path.get(station); - HydrologyTerrainSample terrain = Objects.requireNonNull(sampler.sample(point.x(), point.z()), - "Accepted regional drainage terrain"); - DrainageNode node = new DrainageNode(HydrologyHash.mix(options.courseId(), NODE_SALT, index, point.x(), point.z()), - point.x(), point.z(), terrain, length - distance[station], options.outletId()); - nodes.add(node); - contribution = Math.max(contribution, options.flow().contributionAt(point.x(), point.z())); - if (index > 0) { - DrainageNode upstream = nodes.get(index - 1); - edges.add(new DrainageEdge(HydrologyHash.mix(options.courseId(), upstream.id(), node.id()), - upstream.id(), node.id(), options.outletId(), upstream.potential() - node.potential(), - contribution, 0, List.of(upstream.naturalPoint(), node.naturalPoint()))); - } - } - return new HydrologyRegionalCourseGraph(nodes, edges, contribution); - } - - record Options(long courseId, long outletId, int spacing, List path, HydrologyRegionalFlow flow) { - Options { - path = List.copyOf(path); - Objects.requireNonNull(flow, "Regional drainage flow"); - if (spacing < 1 || path.size() < 2) { - throw new IllegalArgumentException("Regional drainage requires positive spacing and a complete path."); - } - } - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalEnvelope.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalEnvelope.java deleted file mode 100644 index 6ee83398c..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalEnvelope.java +++ /dev/null @@ -1,43 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.hydrology.surface.SurfaceBounds; - -import java.util.List; - -record HydrologyRegionalEnvelope(long minimumX, long minimumZ, long maximumX, long maximumZ) { - private static final HydrologyRegionalEnvelope UNBOUNDED = new HydrologyRegionalEnvelope( - Long.MIN_VALUE, Long.MIN_VALUE, Long.MAX_VALUE, Long.MAX_VALUE); - - static HydrologyRegionalEnvelope of(List guide, HydrologyPlannerSettings settings, int footprintRadius) { - long minimumX = Long.MAX_VALUE; - long minimumZ = Long.MAX_VALUE; - long maximumX = Long.MIN_VALUE; - long maximumZ = Long.MIN_VALUE; - for (HydrologyPoint point : guide) { - minimumX = Math.min(minimumX, point.x()); - minimumZ = Math.min(minimumZ, point.z()); - maximumX = Math.max(maximumX, point.x()); - maximumZ = Math.max(maximumZ, point.z()); - } - if (guide.isEmpty()) { - return UNBOUNDED; - } - int spacing = settings.routing().regional().sampleSpacing(); - double commonPadding = spacing + spacing * settings.geometry().meanders().maximumOffsetRatio() - + 256D + 2D + Math.max(footprintRadius, (long) settings.publicationRadius() + 1L); - double paddingX = StrictMath.ceil(commonPadding + (maximumX - minimumX + 2D * spacing) / 8D); - double paddingZ = StrictMath.ceil(commonPadding + (maximumZ - minimumZ + 2D * spacing) / 8D); - if (!Double.isFinite(paddingX) || !Double.isFinite(paddingZ) - || minimumX - paddingX < Integer.MIN_VALUE || maximumX + paddingX > Integer.MAX_VALUE - || minimumZ - paddingZ < Integer.MIN_VALUE || maximumZ + paddingZ > Integer.MAX_VALUE) { - return UNBOUNDED; - } - return new HydrologyRegionalEnvelope(minimumX - (long) paddingX, minimumZ - (long) paddingZ, - maximumX + (long) paddingX, maximumZ + (long) paddingZ); - } - - boolean intersects(SurfaceBounds bounds) { - return maximumX >= bounds.minimumX() && minimumX <= bounds.maximumX() - && maximumZ >= bounds.minimumZ() && minimumZ <= bounds.maximumZ(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalFalls.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalFalls.java deleted file mode 100644 index 213163125..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalFalls.java +++ /dev/null @@ -1,135 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import art.arcane.iris.generation.hydrology.cave.CavePosition; -import art.arcane.iris.generation.hydrology.cave.CaveVoxelPrecondition; -import art.arcane.iris.generation.hydrology.cave.CaveVoxelView; -import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; -import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan; -import art.arcane.iris.generation.hydrology.surface.SurfaceBounds; -import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint; -import art.arcane.iris.generation.hydrology.surface.SurfaceFootprintCompiler; -import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -final class HydrologyRegionalFalls implements HydrologyCaveVoxelViewFactory.PlannedSurface { - private static final int WINDOW_SIZE = 128; - private static final int MAXIMUM_ROOF_WINDOWS = 4; - private static final int[][] NEIGHBORS = {{0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1}}; - - private final HydrologyPlanner planner; - private final RiverCourse course; - private final HydrologySurfaceDropRaster drops; - private final HydrologyTerrainSampler receiver; - private final LongOpenHashSet witnessColumns; - private final ArrayList exposed; - private final SurfaceFootprintCompiler surface; - private final LinkedHashMap> roofWindows; - - HydrologyRegionalFalls(HydrologyPlanner planner, RiverCourse course) { - this.planner = planner; - this.course = course; - this.receiver = HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course); - this.drops = HydrologySurfaceDropRaster.compile(planner.settings, receiver, planner.geometrySampler, course); - this.witnessColumns = new LongOpenHashSet(); - this.exposed = new ArrayList<>(); - this.surface = new SurfaceFootprintCompiler(planner.settings, this::sample, planner.geometrySampler); - this.roofWindows = new LinkedHashMap<>(MAXIMUM_ROOF_WINDOWS + 1, 1F, true); - for (HydrologyColumnSample column : drops.columns()) { - for (int[] offset : NEIGHBORS) { - witnessColumns.add(RiverFootprint.pack(column.x() + offset[0], column.z() + offset[1])); - } - } - } - - void retain(SurfaceFootprint footprint) { - for (SurfaceLayerColumn column : footprint.columns()) { - if (witnessColumns.contains(RiverFootprint.pack(column.x(), column.z()))) { - exposed.add(column); - } - } - } - - List validate(HydrologyRegionalNetwork network, List diagnostics) { - if (drops.columns().isEmpty()) { - return List.of(); - } - HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(planner.settings, receiver, planner.geometrySampler); - HydrologyFootprintCompiler.ValidationRaster validation = compiler.compileSurfaceDropValidation(course, - new SurfaceFootprint(exposed, 0, null, 0, 0L), drops); - HydrologyObservedPlannedSurface planned = new HydrologyObservedPlannedSurface(this); - CaveVoxelView view = Objects.requireNonNull(planner.caveViewFactory.create(planned)); - ArrayList failures = new ArrayList<>(); - HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter(view, - new HydrologyCaveCourseFilter.Options(planner.settings.underground().connectToExistingCaves(), - planner.settings.outlets().coastalGrotto().maximumVolume(), - planner.settings.outlets().inlandGrotto().maximumVolume()), null, null, planned) - .filter(network.nodes(), network.edges(), network.outlets(), List.of(course), validation, failures); - for (HydrologyDiagnosticCandidate failure : failures) { - if (diagnostics.size() >= 64) { - break; - } - diagnostics.add(new HydrologyDiagnosticCandidate(failure.id(), HydrologyCandidateKind.REGIONAL_SOURCE, - failure.projectedType(), failure.point(), failure.rejection(), failure.detail())); - } - return result.courses().isEmpty() ? null : result.cavePlans(); - } - - static HydrologyCavePlan clip(HydrologyCavePlan plan, SurfaceBounds bounds) { - LinkedHashMap actions = new LinkedHashMap<>(); - LinkedHashMap preconditions = new LinkedHashMap<>(); - plan.forEachActionIn(bounds.minimumX(), bounds.minimumZ(), Math.addExact(bounds.maximumX(), 1), - Math.addExact(bounds.maximumZ(), 1), actions::put); - plan.allPreconditionsIn(bounds.minimumX(), bounds.minimumZ(), Math.addExact(bounds.maximumX(), 1), - Math.addExact(bounds.maximumZ(), 1), (position, precondition) -> { - preconditions.put(position, precondition); - return true; - }); - return new HydrologyCavePlan(plan.source(), plan.rejection(), actions, preconditions, plan.arbitrationWinnerSourceId()); - } - - @Override - public int resolve(int x, int z, int naturalHeight) { - HydrologyColumnSample column = column(x, z); - return column == null ? naturalHeight : column.terrainHeight(); - } - - @Override - public boolean ownsTerrain(int x, int z) { - HydrologyColumnSample column = column(x, z); - HydrologyColumnLayer layer = column == null ? null : column.primarySurfaceLayerOrNull(); - return layer != null && layer.terrainOwned(); - } - - private HydrologyColumnSample column(int x, int z) { - HydrologyTileKey key = HydrologyTileKey.fromBlock(x, z, WINDOW_SIZE); - Map columns = roofWindows.get(key); - if (columns == null) { - SurfaceBounds bounds = new SurfaceBounds(key.minimumBlockX(WINDOW_SIZE), key.minimumBlockZ(WINDOW_SIZE), - Math.addExact(key.minimumBlockX(WINDOW_SIZE), WINDOW_SIZE - 1), - Math.addExact(key.minimumBlockZ(WINDOW_SIZE), WINDOW_SIZE - 1)); - LinkedHashMap built = new LinkedHashMap<>(); - for (SurfaceLayerColumn column : surface.compile(course, bounds).columns()) { - HydrologyTerrainSample terrain = column.terrain(); - HydrologyColumnSample value = new HydrologyColumnSample(column.x(), column.z(), terrain.naturalHeight(), - planner.settings.seaLevel(), terrain.ocean(), terrain.parentBiomeKey(), List.of(column.layer())); - built.merge(RiverFootprint.pack(column.x(), column.z()), value, HydrologyRegionalPlanner::mergeColumn); - } - columns = Map.copyOf(built); - roofWindows.put(key, columns); - if (roofWindows.size() > MAXIMUM_ROOF_WINDOWS) { - roofWindows.remove(roofWindows.sequencedKeySet().getFirst()); - } - } - return columns.get(RiverFootprint.pack(x, z)); - } - - private HydrologyTerrainSample sample(int x, int z) { - return planner.naturalSampler == null ? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalFlow.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalFlow.java deleted file mode 100644 index dd6627f15..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalFlow.java +++ /dev/null @@ -1,53 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.List; - -final class HydrologyRegionalFlow { - private final List points; - private final int[] contributions; - private final int maximum; - - HydrologyRegionalFlow(List points, int[] accumulated) { - if (points.size() != accumulated.length) { - throw new IllegalArgumentException("Regional flow requires one contribution per guide point."); - } - this.points = List.copyOf(points); - this.contributions = new int[accumulated.length]; - int previous = 1; - for (int index = 0; index < accumulated.length; index++) { - previous = Math.max(previous, accumulated[index]); - contributions[index] = previous; - } - this.maximum = previous; - } - - double fraction(int x, int z) { - return StrictMath.sqrt(contribution(x, z) / maximum); - } - - int contributionAt(int x, int z) { - return Math.max(1, (int) StrictMath.round(contribution(x, z))); - } - - private double contribution(int x, int z) { - double nearestDistance = Double.POSITIVE_INFINITY; - double contribution = contributions[0]; - for (int index = 0; index + 1 < points.size(); index++) { - HydrologyPoint first = points.get(index); - HydrologyPoint next = points.get(index + 1); - double deltaX = next.x() - first.x(); - double deltaZ = next.z() - first.z(); - double length = deltaX * deltaX + deltaZ * deltaZ; - double progress = length <= 0D ? 0D : Math.max(0D, Math.min(1D, - ((x - first.x()) * deltaX + (z - first.z()) * deltaZ) / length)); - double distanceX = x - first.x() - deltaX * progress; - double distanceZ = z - first.z() - deltaZ * progress; - double distance = distanceX * distanceX + distanceZ * distanceZ; - if (distance < nearestDistance) { - nearestDistance = distance; - contribution = contributions[index] + (contributions[index + 1] - contributions[index]) * progress; - } - } - return contribution; - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalGraph.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalGraph.java deleted file mode 100644 index 0677035aa..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyRegionalGraph.java +++ /dev/null @@ -1,262 +0,0 @@ -package art.arcane.iris.generation.hydrology; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.BitSet; -import java.util.Comparator; -import java.util.List; -import java.util.PriorityQueue; - -final class HydrologyRegionalGraph { - private static final int LABELS_PER_GRID_NODE = 64; - private static final int MAXIMUM_LABELS = 1_048_576; - private static final Comparator