diff --git a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java index 4d61690c4..88fb95dab 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java +++ b/adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java @@ -62,7 +62,6 @@ public class CustomBiomeSource extends BiomeSource { private final ConcurrentHashMap> noiseBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap> structureBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap> surfaceStructureBiomeCache = new ConcurrentHashMap<>(); - private final ConcurrentHashMap> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>(); private volatile KMap> customBiomes; private volatile Map> vanillaSpawnBiomes; private volatile IrisDimension cacheDimension; @@ -332,18 +331,8 @@ public class CustomBiomeSource extends BiomeSource { if (quartStep == 1) { return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler); } - GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_ring_biome"); - if (lease == null) { - throw new IllegalStateException("Iris structure ring biome lookup was rejected during an engine transition"); - } - try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { - if (!isRuntimeAvailable()) { - throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime"); - } - ensureCachesCurrent(); - return findNaturalSurfaceBiomeHorizontal( - x, y, z, searchRadius, quartStep, allowed, random); - } + return super.findBiomeHorizontal( + x, y, z, searchRadius, quartStep, allowed, random, false, sampler); } static int horizontalBiomeSearchQuartStep(int blockY, int searchRadius) { @@ -401,60 +390,6 @@ public class CustomBiomeSource extends BiomeSource { return resolvedSurfaceHolder; } - private Pair> findNaturalSurfaceBiomeHorizontal( - int x, - int y, - int z, - int searchRadius, - int quartStep, - Predicate> allowed, - RandomSource random - ) { - int centerQuartX = QuartPos.fromBlock(x); - int centerQuartZ = QuartPos.fromBlock(z); - int quartRadius = QuartPos.fromBlock(searchRadius); - Pair> selected = null; - int matches = 0; - for (int radius = 0; radius <= quartRadius; radius += quartStep) { - for (int offsetZ = -radius; offsetZ <= radius; offsetZ += quartStep) { - for (int offsetX = -radius; offsetX <= radius; offsetX += quartStep) { - int quartX = centerQuartX + offsetX; - int quartZ = centerQuartZ + offsetZ; - Holder holder = getNaturalSurfaceStructureBiomeHolder(quartX, quartZ); - if (!allowed.test(holder)) { - continue; - } - if (selected == null || random.nextInt(matches + 1) == 0) { - selected = Pair.of(new BlockPos( - QuartPos.toBlock(quartX), - y, - QuartPos.toBlock(quartZ)), holder); - } - matches++; - } - } - } - return selected; - } - - private Holder getNaturalSurfaceStructureBiomeHolder(int x, int z) { - long columnKey = packColumnKey(x, z); - Holder cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey); - if (cachedHolder != null) { - return cachedHolder; - } - Holder resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z); - Holder existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent( - columnKey, resolvedHolder); - if (existingHolder != null) { - return existingHolder; - } - if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { - naturalSurfaceStructureBiomeCache.clear(); - } - return resolvedHolder; - } - private boolean isGuaranteedSurfaceBiome(int quartY) { if (engine == null || engine.isClosed() || engine.getComplex() == null) { return false; @@ -482,23 +417,6 @@ public class CustomBiomeSource extends BiomeSource { return holder; } - private Holder resolveNaturalSurfaceStructureBiomeHolder(int x, int z) { - int blockX = x << 2; - int blockZ = z << 2; - IrisBiome irisBiome = engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ); - if (irisBiome == null) { - throw new IllegalStateException("Iris returned no natural structure biome at block " - + blockX + "," + blockZ); - } - Holder holder = resolveBiomeHolder(biomeRegistry, irisBiome.getStructureDerivativeKey()); - if (holder == null) { - throw new IllegalStateException("Iris natural structure biome derivative '" - + irisBiome.getStructureDerivativeKey() + "' is not registered at block " - + blockX + "," + blockZ); - } - return holder; - } - public Holder getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome"); if (lease == null) { @@ -588,7 +506,6 @@ public class CustomBiomeSource extends BiomeSource { noiseBiomeCache.clear(); structureBiomeCache.clear(); surfaceStructureBiomeCache.clear(); - naturalSurfaceStructureBiomeCache.clear(); customBiomes = refreshedCustomBiomes; vanillaSpawnBiomes = refreshedSpawnBiomes; cacheDimension = dimension; diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java index 3d4d48e82..4dc02779f 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSourceStructureContractTest.java @@ -45,36 +45,9 @@ public class CustomBiomeSourceStructureContractTest { assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_Y = 0")); assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112")); assertTrue(source.contains("private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4")); + assertTrue(source.contains("x, y, z, searchRadius, quartStep, allowed, random, false, sampler")); assertTrue(source.contains( "return super.findBiomeHorizontal(x, y, z, searchRadius, allowed, random, sampler)")); - assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_ring_biome\")")); - assertTrue(source.contains("findNaturalSurfaceBiomeHorizontal(")); - assertTrue(source.contains("radius += quartStep")); - assertTrue(source.contains("offsetZ += quartStep")); - assertTrue(source.contains("offsetX += quartStep")); - assertTrue(source.contains("random.nextInt(matches + 1) == 0")); - } - - @Test - public void onlyConcentricRingSuitabilityUsesTheNaturalTerrainStream() throws IOException { - String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource"))); - int realResolutionStart = source.indexOf("private Holder resolveSurfaceStructureBiomeHolder("); - int naturalResolutionStart = source.indexOf( - "private Holder resolveNaturalSurfaceStructureBiomeHolder("); - int resolutionEnd = source.indexOf("public Holder getVisibleNoiseBiome(", naturalResolutionStart); - - assertTrue(realResolutionStart >= 0); - assertTrue(naturalResolutionStart > realResolutionStart); - assertTrue(resolutionEnd > naturalResolutionStart); - String realResolution = source.substring(realResolutionStart, naturalResolutionStart); - String naturalResolution = source.substring(naturalResolutionStart, resolutionEnd); - - assertTrue(realResolution.contains("engine.getComplex().getTrueBiomeStream().get(blockX, blockZ)")); - assertFalse(realResolution.contains("getNaturalTrueBiomeStream()")); - assertTrue(naturalResolution.contains( - "engine.getComplex().getNaturalTrueBiomeStream().get(blockX, blockZ)")); - assertFalse(naturalResolution.contains("getTrueBiomeStream()")); - assertFalse(source.contains("studioBootstrapSurfaceStructureBiomeCache")); } @Test diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java index 53b429e65..1d3748e42 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnField.java @@ -2,11 +2,6 @@ package art.arcane.iris.api.terrain; public enum IrisColumnField { SURFACE_HEIGHT, - NATURAL_HEIGHT, SURFACE_KIND, - BIOME_KEY, - RIVER_STATE, - RIVER_DISTANCE, - RIVER_FLOW, - RIVER_WATER_SURFACE_Y + BIOME_KEY } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSample.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSample.java deleted file mode 100644 index 4bbabe286..000000000 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSample.java +++ /dev/null @@ -1,64 +0,0 @@ -package art.arcane.iris.api.terrain; - -import java.util.Objects; - -public record IrisColumnSample( - int blockX, - int blockZ, - int surfaceHeight, - int naturalHeight, - IrisSurfaceKind surfaceKind, - String biomeKey, - IrisRiverState riverState, - double riverDistance, - int riverFlow, - int riverWaterSurfaceY -) { - public static final int UNAVAILABLE_HEIGHT = Integer.MIN_VALUE; - public static final double UNAVAILABLE_RIVER_DISTANCE = Double.NaN; - public static final int UNAVAILABLE_RIVER_FLOW = -1; - - public IrisColumnSample { - Objects.requireNonNull(surfaceKind, "surfaceKind"); - Objects.requireNonNull(riverState, "riverState"); - biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey; - if (!Double.isNaN(riverDistance) && (!Double.isFinite(riverDistance) || riverDistance < 0D)) { - throw new IllegalArgumentException("riverDistance must be non-negative, finite, or unavailable"); - } - if (riverFlow < UNAVAILABLE_RIVER_FLOW) { - throw new IllegalArgumentException("riverFlow must be non-negative or unavailable"); - } - } - - public boolean hasSurfaceHeight() { - return surfaceHeight != UNAVAILABLE_HEIGHT; - } - - public boolean hasNaturalHeight() { - return naturalHeight != UNAVAILABLE_HEIGHT; - } - - public boolean hasSurfaceKind() { - return surfaceKind != IrisSurfaceKind.UNKNOWN; - } - - public boolean hasBiomeKey() { - return biomeKey != null; - } - - public boolean hasRiverState() { - return riverState != IrisRiverState.NONE; - } - - public boolean hasRiverDistance() { - return !Double.isNaN(riverDistance); - } - - public boolean hasRiverFlow() { - return riverFlow != UNAVAILABLE_RIVER_FLOW; - } - - public boolean hasRiverWaterSurfaceY() { - return riverWaterSurfaceY != UNAVAILABLE_HEIGHT; - } -} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java index ac6bb40ab..4c6bccffe 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisColumnSink.java @@ -2,5 +2,5 @@ package art.arcane.iris.api.terrain; @FunctionalInterface public interface IrisColumnSink { - void accept(IrisColumnSample sample); + void accept(int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey); } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisRiverState.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisRiverState.java deleted file mode 100644 index c8bdbdec0..000000000 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisRiverState.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.api.terrain; - -public enum IrisRiverState { - NONE, - WET, - DRY -} diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java index 8d81f7c2e..be60360b3 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/api/terrain/IrisSurfaceKind.java @@ -4,9 +4,6 @@ public enum IrisSurfaceKind { UNKNOWN, LAND, SHORE, - RIVER, - RIVER_SHORE, - DRY_CHANNEL, OCEAN, VOID } diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java index ff21f5bc4..a5fdb12dd 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/gui/BukkitVisionOverlay.java @@ -318,7 +318,7 @@ public final class BukkitVisionOverlay implements GuiOverlay { public String openInEditor(double worldX, double worldZ, RenderType type) { IrisComplex complex = engine.getComplex(); File file = switch (type) { - case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER -> + case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode(); case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode(); case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode(); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java index 5c8d57713..ee1ed09ec 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/IrisTerrainSVC.java @@ -2,9 +2,7 @@ package art.arcane.iris.core.service; import art.arcane.iris.api.terrain.IrisColumnField; import art.arcane.iris.api.terrain.IrisColumnQuery; -import art.arcane.iris.api.terrain.IrisColumnSample; import art.arcane.iris.api.terrain.IrisColumnSink; -import art.arcane.iris.api.terrain.IrisRiverState; import art.arcane.iris.api.terrain.IrisSurfaceKind; import art.arcane.iris.api.terrain.IrisTerrainService; import art.arcane.iris.api.terrain.IrisWorldInfo; @@ -19,8 +17,6 @@ import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.platform.PlatformChunkGenerator; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisServices; @@ -105,14 +101,13 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { try { int surface = engine.getHeight(blockX, blockZ); - IrisRiverSurfaceSample riverSurface = engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ); - int fluid = (int) Math.round(riverSurface.waterSurfaceY()); + int fluid = engine.getDimension().getFluidHeight(); 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, riverSurface); + return IrisSurfaceClassifier.classify(surface, fluid, inferredType); } catch (Throwable error) { reportQueryFault("surfaceKind", world, error); return IrisSurfaceKind.UNKNOWN; @@ -229,72 +224,26 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { EnumSet fields = query.fields(); boolean wantHeight = fields.contains(IrisColumnField.SURFACE_HEIGHT); - boolean wantNaturalHeight = fields.contains(IrisColumnField.NATURAL_HEIGHT); boolean wantKind = fields.contains(IrisColumnField.SURFACE_KIND); boolean wantBiome = fields.contains(IrisColumnField.BIOME_KEY); - boolean wantRiverState = fields.contains(IrisColumnField.RIVER_STATE); - boolean wantRiverDistance = fields.contains(IrisColumnField.RIVER_DISTANCE); - boolean wantRiverFlow = fields.contains(IrisColumnField.RIVER_FLOW); - boolean wantRiverWaterSurface = fields.contains(IrisColumnField.RIVER_WATER_SURFACE_Y); - boolean wantRiver = wantKind || wantRiverState || wantRiverDistance || wantRiverFlow - || wantRiverWaterSurface; try { int minHeight = engine.getMinHeight(); + int fluid = engine.getDimension().getFluidHeight(); long visited = IrisColumnWalk.walk(query, (int blockX, int blockZ) -> { if (engine.isClosed()) { return false; } - IrisRiverSurfaceSample riverSurface = wantRiver - ? engine.getComplex().getRiverSurfaceStream().get(blockX, blockZ) - : null; int surface = wantHeight || wantKind ? engine.getHeight(blockX, blockZ) : 0; - int fluid = riverSurface == null - ? engine.getDimension().getFluidHeight() - : (int) Math.round(riverSurface.waterSurfaceY()); boolean needsBiome = wantBiome || (wantKind && IrisSurfaceClassifier.requiresSurfaceBiome(surface, fluid)); IrisBiome biome = needsBiome ? engine.getSurfaceBiome(blockX, blockZ) : null; IrisSurfaceKind kind = wantKind - ? IrisSurfaceClassifier.classify( - surface, - fluid, - biome == null ? null : biome.getInferredType(), - riverSurface - ) + ? IrisSurfaceClassifier.classify(surface, fluid, biome == null ? null : biome.getInferredType()) : 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; - boolean riverPresent = riverSurface != null && riverSurface.river().present(); - IrisRiverState riverState = wantRiverState && riverSurface != null - ? riverState(riverSurface) - : IrisRiverState.NONE; - double riverDistance = wantRiverDistance && riverPresent - ? riverSurface.river().distance() - : IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE; - int riverFlow = wantRiverFlow && riverPresent - ? riverSurface.river().flow() - : IrisColumnSample.UNAVAILABLE_RIVER_FLOW; - int riverWaterSurfaceY = wantRiverWaterSurface - && riverPresent - && riverSurface.river().state() == RiverRouteState.WET - ? fluid + minHeight - : IrisColumnSample.UNAVAILABLE_HEIGHT; - sink.accept(new IrisColumnSample( - blockX, - blockZ, - wantHeight ? surface + minHeight : IrisColumnSample.UNAVAILABLE_HEIGHT, - natural, - kind, - biomeKey, - riverState, - riverDistance, - riverFlow, - riverWaterSurfaceY - )); + sink.accept(blockX, blockZ, wantHeight ? surface + minHeight : -1, kind, biomeKey); return true; }); return visited == query.columnCount(); @@ -304,17 +253,6 @@ public class IrisTerrainSVC implements IrisService, IrisTerrainService { } } - static IrisRiverState riverState(IrisRiverSurfaceSample surface) { - if (!surface.river().present()) { - return IrisRiverState.NONE; - } - return switch (surface.river().state()) { - case WET -> IrisRiverState.WET; - case DRY -> IrisRiverState.DRY; - case SUPPRESSED -> IrisRiverState.NONE; - }; - } - private static Optional key(IrisBiome biome) { String loadKey = biome == null ? null : biome.getLoadKey(); return loadKey == null || loadKey.isEmpty() ? Optional.empty() : Optional.of(loadKey); diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java index 520dfb81e..75be7325a 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifier.java @@ -2,9 +2,6 @@ package art.arcane.iris.core.service.terrain; import art.arcane.iris.api.terrain.IrisSurfaceKind; import art.arcane.iris.engine.object.InferredType; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; public final class IrisSurfaceClassifier { private IrisSurfaceClassifier() { @@ -25,32 +22,4 @@ public final class IrisSurfaceClassifier { return inferredType == InferredType.SHORE ? IrisSurfaceKind.SHORE : IrisSurfaceKind.LAND; } - - public static IrisSurfaceKind classify( - int engineSurfaceHeight, - int engineFluidHeight, - InferredType inferredType, - IrisRiverSurfaceSample riverSurface - ) { - if (engineSurfaceHeight <= 0) { - return IrisSurfaceKind.VOID; - } - if (riverSurface != null && riverSurface.river().present() && !riverSurface.subterranean()) { - if (riverSurface.river().state() == RiverRouteState.DRY) { - return riverSurface.river().section() == RiverSection.DRY_CHANNEL - ? IrisSurfaceKind.DRY_CHANNEL - : IrisSurfaceKind.LAND; - } - if (riverSurface.river().state() == RiverRouteState.WET) { - RiverSection section = riverSurface.river().section(); - if (section == RiverSection.BANK) { - return IrisSurfaceKind.RIVER_SHORE; - } - if (section == RiverSection.CHANNEL || section == RiverSection.MOUTH) { - return IrisSurfaceKind.RIVER; - } - } - } - return classify(engineSurfaceHeight, engineFluidHeight, inferredType); - } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisColumnSampleTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisColumnSampleTest.java deleted file mode 100644 index 6947f39f3..000000000 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/api/terrain/IrisColumnSampleTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package art.arcane.iris.api.terrain; - -import org.junit.Test; - -import java.util.concurrent.atomic.AtomicReference; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class IrisColumnSampleTest { - @Test - public void unavailableFieldsHaveUnambiguousSentinels() { - IrisColumnSample sample = sample( - IrisColumnSample.UNAVAILABLE_HEIGHT, - IrisColumnSample.UNAVAILABLE_HEIGHT, - IrisSurfaceKind.UNKNOWN, - null, - IrisRiverState.NONE, - IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE, - IrisColumnSample.UNAVAILABLE_RIVER_FLOW, - IrisColumnSample.UNAVAILABLE_HEIGHT - ); - - assertFalse(sample.hasSurfaceHeight()); - assertFalse(sample.hasNaturalHeight()); - assertFalse(sample.hasSurfaceKind()); - assertFalse(sample.hasBiomeKey()); - assertFalse(sample.hasRiverState()); - assertFalse(sample.hasRiverDistance()); - assertFalse(sample.hasRiverFlow()); - assertFalse(sample.hasRiverWaterSurfaceY()); - } - - @Test - public void negativeWorldHeightsRemainAvailableValues() { - IrisColumnSample sample = sample( - -1, - -64, - IrisSurfaceKind.DRY_CHANNEL, - "test:river", - IrisRiverState.DRY, - 0D, - 0, - -1 - ); - - assertTrue(sample.hasSurfaceHeight()); - assertTrue(sample.hasNaturalHeight()); - assertTrue(sample.hasSurfaceKind()); - assertTrue(sample.hasBiomeKey()); - assertTrue(sample.hasRiverState()); - assertTrue(sample.hasRiverDistance()); - assertTrue(sample.hasRiverFlow()); - assertTrue(sample.hasRiverWaterSurfaceY()); - } - - @Test - public void blankBiomeKeysNormalizeToUnavailable() { - IrisColumnSample sample = sample( - 64, - 65, - IrisSurfaceKind.LAND, - " ", - IrisRiverState.NONE, - IrisColumnSample.UNAVAILABLE_RIVER_DISTANCE, - IrisColumnSample.UNAVAILABLE_RIVER_FLOW, - IrisColumnSample.UNAVAILABLE_HEIGHT - ); - - assertNull(sample.biomeKey()); - assertFalse(sample.hasBiomeKey()); - } - - @Test - public void theSinkReceivesTheTypedSample() { - IrisColumnSample sample = sample( - 64, - 65, - IrisSurfaceKind.RIVER, - "test:river", - IrisRiverState.WET, - 0.5D, - 3, - 67 - ); - AtomicReference received = new AtomicReference<>(); - IrisColumnSink sink = received::set; - - sink.accept(sample); - - assertSame(sample, received.get()); - } - - @Test - public void invalidHydrologyValuesAreRejected() { - assertInvalid(Double.POSITIVE_INFINITY, 1); - assertInvalid(-0.1D, 1); - assertInvalid(0D, -2); - } - - private static void assertInvalid(double distance, int flow) { - try { - sample( - 64, - 65, - IrisSurfaceKind.RIVER, - "test:river", - IrisRiverState.WET, - distance, - flow, - 67 - ); - fail("Expected invalid hydrology values to be rejected"); - } catch (IllegalArgumentException expected) { - assertTrue(expected.getMessage().startsWith("river")); - } - } - - private static IrisColumnSample sample( - int surfaceHeight, - int naturalHeight, - IrisSurfaceKind surfaceKind, - String biomeKey, - IrisRiverState riverState, - double riverDistance, - int riverFlow, - int riverWaterSurfaceY - ) { - return new IrisColumnSample( - 12, - -7, - surfaceHeight, - naturalHeight, - surfaceKind, - biomeKey, - riverState, - riverDistance, - riverFlow, - riverWaterSurfaceY - ); - } -} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectFluidHeightTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectFluidHeightTest.java index ec5cecbe9..81febecb2 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectFluidHeightTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/commands/CommandObjectFluidHeightTest.java @@ -1,10 +1,8 @@ package art.arcane.iris.core.commands; -import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IObjectPlacer; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.util.project.stream.ProceduralStream; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; @@ -15,31 +13,22 @@ import java.util.Map; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class CommandObjectFluidHeightTest { @Test - public void absoluteObjectPlacementShiftsTheColumnRiverHeadFromNegativeMinY() { + public void absoluteObjectPlacementShiftsFluidHeightFromNegativeMinY() { 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); 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); IObjectPlacer placer = CommandObject.createPlacer(world, future, engine); assertEquals(63, placer.getFluidHeight()); - assertEquals(67, placer.getFluidHeight(12, -7)); - verify(riverHead).get(12, -7); } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java index 90368cd39..5e9a2dec1 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/IrisTerrainSVCTest.java @@ -2,15 +2,8 @@ package art.arcane.iris.core.service; import art.arcane.iris.api.terrain.IrisColumnField; import art.arcane.iris.api.terrain.IrisColumnQuery; -import art.arcane.iris.api.terrain.IrisRiverState; import art.arcane.iris.api.terrain.IrisSurfaceKind; import art.arcane.iris.api.terrain.IrisTerrainService; -import art.arcane.iris.engine.river.RiverEdgeId; -import art.arcane.iris.engine.river.RiverNodeId; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; import art.arcane.iris.util.common.plugin.IrisService; import org.bukkit.World; import org.junit.Test; @@ -89,7 +82,9 @@ public class IrisTerrainSVCTest { IrisTerrainSVC service = new IrisTerrainSVC(); AtomicInteger sinkCalls = new AtomicInteger(); - boolean answered = service.sampleColumns(null, SMALL, sample -> sinkCalls.incrementAndGet()); + boolean answered = service.sampleColumns(null, SMALL, + (int blockX, int blockZ, int surfaceHeight, IrisSurfaceKind kind, String biomeKey) + -> sinkCalls.incrementAndGet()); assertFalse(answered); assertEquals(0, sinkCalls.get()); @@ -102,38 +97,4 @@ public class IrisTerrainSVCTest { assertFalse(service.sampleColumns(null, null, null)); assertFalse(service.sampleColumns(null, SMALL, null)); } - - @Test - public void riverRouteStatesMapToThePublicDiagnosticStates() { - assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState( - new IrisRiverSurfaceSample(RiverSample.none(), 70D, 70D, 70D, false, false) - )); - assertEquals(IrisRiverState.WET, IrisTerrainSVC.riverState(river(RiverRouteState.WET))); - assertEquals(IrisRiverState.DRY, IrisTerrainSVC.riverState(river(RiverRouteState.DRY))); - assertEquals(IrisRiverState.NONE, IrisTerrainSVC.riverState(river(RiverRouteState.SUPPRESSED))); - } - - private static IrisRiverSurfaceSample river(RiverRouteState state) { - RiverSection section = switch (state) { - case WET -> RiverSection.CHANNEL; - case DRY -> RiverSection.DRY_CHANNEL; - case SUPPRESSED -> RiverSection.NONE; - }; - RiverSample sample = new RiverSample( - true, - state, - section, - 0D, - 0.5D, - 1D, - 1, - 1, - 8D, - 4D, - 3D, - false, - RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0)) - ); - return new IrisRiverSurfaceSample(sample, 70D, 60D, 63D, false, state == RiverRouteState.WET); - } } diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java index 90281845e..a9368289b 100644 --- a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/core/service/terrain/IrisSurfaceClassifierTest.java @@ -2,12 +2,6 @@ package art.arcane.iris.core.service.terrain; import art.arcane.iris.api.terrain.IrisSurfaceKind; import art.arcane.iris.engine.object.InferredType; -import art.arcane.iris.engine.river.RiverEdgeId; -import art.arcane.iris.engine.river.RiverNodeId; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -64,86 +58,4 @@ public class IrisSurfaceClassifierTest { } } } - - @Test - public void activeRiverGeometryOverridesGenericOceanAndLandKinds() { - assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify( - 60, - 63, - InferredType.SEA, - river(RiverRouteState.WET, RiverSection.CHANNEL) - )); - assertEquals(IrisSurfaceKind.RIVER, IrisSurfaceClassifier.classify( - 60, - 63, - InferredType.SEA, - river(RiverRouteState.WET, RiverSection.MOUTH) - )); - assertEquals(IrisSurfaceKind.RIVER_SHORE, IrisSurfaceClassifier.classify( - 64, - 63, - InferredType.SHORE, - river(RiverRouteState.WET, RiverSection.BANK) - )); - assertEquals(IrisSurfaceKind.DRY_CHANNEL, IrisSurfaceClassifier.classify( - 60, - 60, - InferredType.LAND, - river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL) - )); - assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify( - 60, - 60, - InferredType.LAND, - river(RiverRouteState.DRY, RiverSection.DRY_BANK) - )); - } - - @Test - public void voidClassificationWinsOverRiverGeometry() { - for (RiverSection section : RiverSection.values()) { - if (section == RiverSection.NONE) { - continue; - } - RiverRouteState state = section == RiverSection.DRY_CHANNEL || section == RiverSection.DRY_BANK - ? RiverRouteState.DRY - : RiverRouteState.WET; - assertEquals(IrisSurfaceKind.VOID, IrisSurfaceClassifier.classify( - 0, - 63, - InferredType.LAND, - river(state, section) - )); - } - } - - @Test - public void suppressedRoutesDoNotCreatePublicRiverSurfaceKinds() { - assertEquals(IrisSurfaceKind.LAND, IrisSurfaceClassifier.classify( - 64, - 63, - InferredType.LAND, - river(RiverRouteState.SUPPRESSED, RiverSection.CHANNEL) - )); - } - - private static IrisRiverSurfaceSample river(RiverRouteState state, RiverSection section) { - RiverSample sample = new RiverSample( - true, - state, - section, - 0D, - 0.5D, - 1D, - 1, - 1, - 8D, - 4D, - 3D, - false, - RiverEdgeId.of(new RiverNodeId(0, 0), new RiverNodeId(1, 0)) - ); - double waterSurface = state == RiverRouteState.WET ? 63D : 60D; - return new IrisRiverSurfaceSample(sample, 70D, 60D, waterSurface, false, state == RiverRouteState.WET); - } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java index fd0f55219..c42519c48 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedVisionOverlay.java @@ -126,7 +126,7 @@ public final class ModdedVisionOverlay implements GuiOverlay { } IrisComplex complex = engine.getComplex(); File file = switch (type) { - case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT, RIVER -> + case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT -> complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode(); case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode(); case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode(); diff --git a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java index 352a1462d..2a05ec80c 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java +++ b/core/src/main/java/art/arcane/iris/core/gui/VisionGUI.java @@ -26,7 +26,6 @@ import art.arcane.iris.engine.framework.render.RenderType; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.river.RiverSection; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.localization.MessageArgument; @@ -70,7 +69,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -475,33 +473,11 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi } private void renderLegend(Graphics2D canvas) { - if (currentType == RenderType.RIVER) { - renderRiverLegend(canvas); - } else if (currentType == RenderType.HEIGHT) { + if (currentType == RenderType.HEIGHT) { renderHeightLegend(canvas); } } - private void renderRiverLegend(Graphics2D canvas) { - RiverSection[] sections = RiverSection.values(); - int lineHeight = 18; - int width = 148; - int height = sections.length * lineHeight + CARD_PADDING * 2; - int x = getWidth() - width - CARD_PADDING; - int y = getHeight() - STATUS_HEIGHT - PROGRESS_HEIGHT - height - CARD_PADDING; - drawCardBackground(canvas, x, y, width, height); - canvas.setFont(BODY_FONT); - for (int index = 0; index < sections.length; index++) { - RiverSection section = sections[index]; - int rowY = y + CARD_PADDING + index * lineHeight; - canvas.setColor(new Color(IrisRenderer.riverColor(section))); - canvas.fillRoundRect(x + CARD_PADDING, rowY + 2, 12, 12, 4, 4); - canvas.setColor(TEXT_SECONDARY); - String label = section.name().toLowerCase(Locale.ROOT).replace('_', ' '); - canvas.drawString(label, x + CARD_PADDING + 20, rowY + 13); - } - } - private void renderHeightLegend(Graphics2D canvas) { int width = 244; int height = 54; @@ -1027,7 +1003,6 @@ public final class VisionGUI extends JPanel implements MouseWheelListener, KeyLi case BIOME_SEA -> DesktopUiMessages.VISION_MODE_BIOME_SEA; case REGION -> DesktopUiMessages.VISION_MODE_REGION; case CAVE_LAND -> DesktopUiMessages.VISION_MODE_CAVE_LAND; - case RIVER -> DesktopUiMessages.VISION_MODE_RIVER; case HEIGHT -> DesktopUiMessages.VISION_MODE_HEIGHT; case OBJECT_LOAD -> DesktopUiMessages.VISION_MODE_OBJECT_LOAD; case DECORATOR_LOAD -> DesktopUiMessages.VISION_MODE_DECORATOR_LOAD; diff --git a/core/src/main/java/art/arcane/iris/core/gui/VisionRenderController.java b/core/src/main/java/art/arcane/iris/core/gui/VisionRenderController.java index 9702a829b..836a7830d 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/VisionRenderController.java +++ b/core/src/main/java/art/arcane/iris/core/gui/VisionRenderController.java @@ -275,8 +275,7 @@ final class VisionRenderController implements AutoCloseable { if (!isCurrent(work)) { return; } - int admissionLimit = work.frame().spec().type() == RenderType.RIVER ? 1 : renderWorkerCount; - while (work.inFlight() < admissionLimit) { + while (work.inFlight() < renderWorkerCount) { VisibleTile tile = work.nextMissing(); if (tile == null) { return; diff --git a/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java b/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java index 545928c7e..14d58a86d 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/DesktopUiMessages.java @@ -49,7 +49,6 @@ public final class DesktopUiMessages { public static final TextKey VISION_MODE_BIOME_SEA = TextKey.of("iris.desktop.vision.mode.biome_sea", "Biome sea"); public static final TextKey VISION_MODE_REGION = TextKey.of("iris.desktop.vision.mode.region", "Region"); public static final TextKey VISION_MODE_CAVE_LAND = TextKey.of("iris.desktop.vision.mode.cave_land", "Cave land"); - public static final TextKey VISION_MODE_RIVER = TextKey.of("iris.desktop.vision.mode.river", "River network"); public static final TextKey VISION_MODE_HEIGHT = TextKey.of("iris.desktop.vision.mode.height", "Height"); public static final TextKey VISION_MODE_OBJECT_LOAD = TextKey.of("iris.desktop.vision.mode.object_load", "Object load"); public static final TextKey VISION_MODE_DECORATOR_LOAD = TextKey.of("iris.desktop.vision.mode.decorator_load", "Decorator load"); @@ -168,7 +167,7 @@ public final class DesktopUiMessages { VISION_HELP_RESET_ZOOM, VISION_HELP_CYCLE_MODE, VISION_HELP_FPS, VISION_HELP_GRID, VISION_HELP_BIOME, VISION_HELP_TELEPORT, VISION_HELP_EDITOR, VISION_OPENED, VISION_TELEPORTING, VISION_MODE_BIOME, VISION_MODE_BIOME_LAND, VISION_MODE_BIOME_SEA, - VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_RIVER, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD, + VISION_MODE_REGION, VISION_MODE_CAVE_LAND, VISION_MODE_HEIGHT, VISION_MODE_OBJECT_LOAD, VISION_MODE_DECORATOR_LOAD, VISION_MODE_CONTINENT, VISION_MODE_LAYER_LOAD, NOISE_TITLE, NOISE_TITLE_GENERATOR, NOISE_SEARCH, NOISE_STATUS, NOISE_CATEGORY_CUSTOM, NOISE_CATEGORY_PACK_GENERATORS, NOISE_CATEGORY_SIMPLEX, NOISE_CATEGORY_PERLIN, diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackRiverValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackRiverValidator.java deleted file mode 100644 index 50ef7899f..000000000 --- a/core/src/main/java/art/arcane/iris/core/pack/PackRiverValidator.java +++ /dev/null @@ -1,1300 +0,0 @@ -package art.arcane.iris.core.pack; - -import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.iris.engine.river.RiverTopologyComplexity; -import art.arcane.volmlib.util.json.JSONArray; -import art.arcane.volmlib.util.json.JSONObject; - -import java.io.File; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -final class PackRiverValidator { - private static final Set WATER_MODES = Set.of("FIXED", "TERRACED"); - private static final Set TERMINAL_MODES = Set.of("SUPPRESS", "DRY_CHANNEL", "SINKHOLE_GROTTO"); - private static final Set ROUTING_POLICIES = Set.of("ALLOW", "AVOID", "BLOCK"); - private static final Set CAVE_MODES = Set.of( - "SEALED", - "FLOOD_CLOSED_COMPONENT", - "GENERATE_GROTTO", - "GROTTO_OR_CLOSED_COMPONENT", - "WATERFALL_POOL" - ); - private static final Set CAVE_FALLBACKS = Set.of("SEALED", "GENERATE_GROTTO"); - private static final Set EXISTING_FLUID_POLICIES = Set.of("REJECT", "ALLOW_SAME", "REPLACE"); - private static final Set UNSAFE_RIVER_STREAMS = Set.of("HEIGHT", "HEIGHT_OR_FLUID", "SLOPE"); - private static final Set NOISE_STYLES = noiseStyles(); - - private PackRiverValidator() { - } - - static Validation validate(File packFolder, File[] dimensionFiles) { - List errors = new ArrayList<>(); - List warnings = new ArrayList<>(); - if (packFolder == null || !packFolder.isDirectory() || dimensionFiles == null) { - return new Validation(errors, warnings); - } - - boolean enabled = false; - List contexts = new ArrayList<>(); - List sortedDimensions = new ArrayList<>(List.of(dimensionFiles)); - sortedDimensions.sort(Comparator.comparing(File::getPath)); - for (File dimensionFile : sortedDimensions) { - JSONObject dimension = PackValidationIo.readJson(dimensionFile); - if (dimension == null || !dimension.has("rivers")) { - continue; - } - String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName()); - String path = "Dimension '" + dimensionKey + "' rivers"; - JSONObject rivers = requireObject(dimension, "rivers", path, errors); - if (rivers == null) { - continue; - } - PackJsonFieldChecks.validateOptionalBoolean(path, rivers, "enabled", errors); - if (!booleanValue(rivers, "enabled", false)) { - continue; - } - enabled = true; - DimensionRiverContext context = new DimensionRiverContext( - dimensionKey, - dimension, - rivers, - referencedKeys(dimension.optJSONArray("regions")) - ); - contexts.add(context); - validateNetwork(packFolder, path, context, errors, warnings); - } - - if (enabled) { - validateOverrides(packFolder, new File(packFolder, "regions"), "Region", contexts, errors, warnings); - validateOverrides(packFolder, new File(packFolder, "biomes"), "Biome", contexts, errors, warnings); - } - return new Validation(errors, warnings); - } - - private static void validateNetwork(File packFolder, String path, DimensionRiverContext context, - List errors, List warnings) { - JSONObject rivers = context.rivers(); - JSONObject topology = nestedObject(rivers, "topology", path, errors); - JSONObject terrain = nestedObject(rivers, "terrain", path, errors); - JSONObject water = nestedObject(rivers, "water", path, errors); - JSONObject biomes = nestedObject(rivers, "biomes", path, errors); - JSONObject caves = nestedObject(rivers, "caves", path, errors); - - if (topology != null) { - validateTopology(packFolder, path + ".topology", topology, errors, warnings); - } - if (terrain != null) { - validateTerrain(packFolder, path + ".terrain", terrain, errors, warnings); - } - if (water != null) { - validateWater(path + ".water", water, context.dimension(), errors); - } - if (biomes != null) { - validateBiomePools( - packFolder, - path + ".biomes", - biomes, - false, - usesOverworldNativeStructureRoles(context), - errors, - warnings - ); - } - boolean sinkholeTerminal = terrain != null - && "SINKHOLE_GROTTO".equals(stringValue(terrain, "terminalMode", "DRY_CHANNEL")); - if (caves != null) { - validateCaves( - packFolder, - path + ".caves", - caves, - context.dimension(), - sinkholeTerminal, - errors, - warnings - ); - } - - if (topology != null && terrain != null) { - WormEnvelope wormEnvelope = wormEnvelope(terrain); - int cellSize = integerValue(topology, "cellSize", 512); - if (wormEnvelope.maximumOffset() > cellSize) { - warnings.add(path + ".terrain.worms maxOffset exceeds topology.cellSize; reaches may require large cache halos."); - } - validateTopologyComplexity(packFolder, path, topology, terrain, errors); - } - if (sinkholeTerminal && caves != null) { - validateSinkholeCapability( - path + ".terrain.terminalMode", - context, - caves, - path + ".caves", - errors - ); - } - } - - private static void validateTopology(File packFolder, String path, JSONObject topology, - List errors, List warnings) { - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "cellSize", 64, 4096, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "tileCells", 1, 64, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "siteJitter", 0D, 0.49D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "maxRouteReaches", 1, 256, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "minimumSourcesPerTile", 0, 64, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "sinkSearchReaches", 0, 7, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingBasinCells", 8, 256, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, topology, "routingDeviationScaleCells", 8, 256, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingDeviationStrengthCells", 0D, 32D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingPlateauHeight", 1D, 64D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "routingNoiseWeight", 0D, 1024D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "flowAlignmentWeight", 0D, 1024D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "confluenceWeight", 0D, 1024D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainHeightWeight", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "terrainSlopeWeight", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, topology, "oceanAttraction", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalBoolean(path, topology, "requireOcean", errors); - validateNoiseChance(packFolder, topology, "source", path, errors); - validateNoiseChance(packFolder, topology, "continuation", path, errors); - validateStyle(packFolder, topology, "routingStyle", path, errors); - - int tileCells = integerValue(topology, "tileCells", 4); - int minimumSourcesPerTile = integerValue(topology, "minimumSourcesPerTile", 0); - if (tileCells >= 1 && tileCells <= 64 - && minimumSourcesPerTile >= 0 - && minimumSourcesPerTile > tileCells * tileCells) { - errors.add(path + ".minimumSourcesPerTile must not exceed tileCells squared."); - } - - } - - private static void validateTerrain(File packFolder, String path, JSONObject terrain, - List errors, List warnings) { - validateStyledRange(packFolder, terrain, "channelWidth", path, 1D, 2048D, errors, warnings); - validateStyledRange(packFolder, terrain, "bankWidth", path, 0D, 2048D, errors, warnings); - validateStyledRange(packFolder, terrain, "depth", path, 1D, 512D, errors, warnings); - validateStyledRange(packFolder, terrain, "tunnelWidthMultiplier", path, 1D, 8D, errors, warnings); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "channelRadiusBonus", 0D, 64D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxChannelWidth", 1D, 2048D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxBankWidth", 0D, 2048D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "maxDepth", 1D, 512D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderWidthFactor", 0D, 8D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "orderDepthFactor", 0D, 8D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "maxIncision", 0, 512, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bankExponent", 0.125D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelMouthBlend", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelFloorVariation", 0D, 8D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "tunnelRoofVariation", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "bedRoughness", 0D, 8D, errors); - PackJsonFieldChecks.validateOptionalEnum(path, terrain, "terminalMode", TERMINAL_MODES, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, terrain, "terminalTaper", 8, 1024, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, terrain, "dryContinuationChance", 0D, 1D, errors); - validateNoiseChance(packFolder, terrain, "incision", path, errors); - validateStyle(packFolder, terrain, "tunnelFloorStyle", path, errors); - validateStyle(packFolder, terrain, "tunnelRoofStyle", path, errors); - validateStyle(packFolder, terrain, "bedRoughnessStyle", path, errors); - validateWorms(path, terrain, errors); - double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D); - double maximumTunnelWidthMultiplier = styledRangeMaximum( - packFolder, - terrain, - "tunnelWidthMultiplier", - path, - 1D - ); - double tunnelMouthBlend = doubleValue(terrain, "tunnelMouthBlend", 2D); - if (Double.isFinite(maximumChannelWidth) && maximumChannelWidth >= 1D && maximumChannelWidth <= 2048D - && Double.isFinite(maximumTunnelWidthMultiplier) - && maximumTunnelWidthMultiplier >= 1D && maximumTunnelWidthMultiplier <= 8D - && Double.isFinite(tunnelMouthBlend) && tunnelMouthBlend >= 0D && tunnelMouthBlend <= 16D) { - String violation = RiverTopologyComplexity.tunnelPlanViolation( - maximumChannelWidth, - maximumTunnelWidthMultiplier, - tunnelMouthBlend - ); - if (violation != null) { - errors.add(path + " exceeds the safe derived hydrology budget. " + violation); - } - } - } - - private static void validateWater( - String path, - JSONObject water, - JSONObject dimension, - List errors - ) { - PackJsonFieldChecks.validateOptionalEnum(path, water, "mode", WATER_MODES, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "poolLength", 8, 4096, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "maximumPoolRise", 0, 64, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "dropHeight", 1, 32, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, water, "fluidHeight", -2048, 2048, errors); - validateFluidPalette(path, water, errors); - - String mode = stringValue(water, "mode", "FIXED"); - int fluidHeight = integerValue(water, "fluidHeight", 63); - int maximumPoolRise = integerValue(water, "maximumPoolRise", 4); - int dropHeight = integerValue(water, "dropHeight", 1); - if ("TERRACED".equals(mode) && dropHeight > maximumPoolRise) { - errors.add(path + ".dropHeight must not exceed maximumPoolRise in TERRACED mode."); - } - JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight"); - int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64); - int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320); - if (fluidHeight < minimumHeight || fluidHeight > maximumHeight) { - errors.add(path + ".fluidHeight must remain inside dimensionHeight."); - } - if ("TERRACED".equals(mode) && fluidHeight + maximumPoolRise > maximumHeight) { - errors.add(path + ".fluidHeight plus maximumPoolRise must remain inside dimensionHeight."); - } - } - - private static void validateFluidPalette(String path, JSONObject water, List errors) { - if (!water.has("fluidPalette")) { - return; - } - Object rawPalette = water.opt("fluidPalette"); - if (!(rawPalette instanceof JSONObject palette)) { - errors.add(path + ".fluidPalette must be an object."); - return; - } - Object rawBlocks = palette.opt("palette"); - if (!(rawBlocks instanceof JSONArray blocks) || blocks.length() < 1) { - errors.add(path + ".fluidPalette.palette must contain at least one fluid block."); - } - } - - private static void validateTopologyComplexity( - File packFolder, - String path, - JSONObject topology, - JSONObject terrain, - List errors - ) { - int cellSize = integerValue(topology, "cellSize", 512); - int tileCells = integerValue(topology, "tileCells", 4); - double siteJitter = doubleValue(topology, "siteJitter", 0.35D); - int maxRouteReaches = integerValue(topology, "maxRouteReaches", 16); - WormEnvelope wormEnvelope = wormEnvelope(terrain); - double maximumChannelWidth = doubleValue(terrain, "maxChannelWidth", 10D); - double maximumBankWidth = doubleValue(terrain, "maxBankWidth", 4D); - double maximumTunnelWidthMultiplier = styledRangeMaximum( - packFolder, - terrain, - "tunnelWidthMultiplier", - path, - 1D - ); - double tunnelMouthBlend = doubleValue(terrain, "tunnelMouthBlend", 2D); - if (cellSize < 64 || cellSize > 4096 - || tileCells < 1 || tileCells > 64 - || !Double.isFinite(siteJitter) || siteJitter < 0D || siteJitter > 0.49D - || maxRouteReaches < 1 || maxRouteReaches > 256 - || wormEnvelope.maximumOffset() < 0D || wormEnvelope.maximumOffset() > 1024D - || wormEnvelope.maximumSegments() < 1 || wormEnvelope.maximumSegments() > 64 - || !Double.isFinite(maximumChannelWidth) || maximumChannelWidth < 1D || maximumChannelWidth > 2048D - || !Double.isFinite(maximumBankWidth) || maximumBankWidth < 0D || maximumBankWidth > 2048D - || !Double.isFinite(maximumTunnelWidthMultiplier) - || maximumTunnelWidthMultiplier < 1D || maximumTunnelWidthMultiplier > 8D - || !Double.isFinite(tunnelMouthBlend) || tunnelMouthBlend < 0D || tunnelMouthBlend > 16D) { - return; - } - double maximumSurfaceRadius = maximumChannelWidth * 0.5D + maximumBankWidth; - double maximumTunnelRadius = maximumChannelWidth * 0.5D * maximumTunnelWidthMultiplier - + tunnelMouthBlend; - double maximumReachRadius = Math.max(maximumSurfaceRadius, maximumTunnelRadius); - RiverTopologyComplexity.Estimate estimate = RiverTopologyComplexity.estimate( - cellSize, - tileCells, - siteJitter, - maxRouteReaches, - maximumReachRadius, - wormEnvelope.maximumOffset(), - wormEnvelope.maximumSegments() - ); - for (String violation : estimate.violations()) { - errors.add(path + " exceeds the safe derived complexity budget. " + violation); - } - } - - private static void validateWorms(String path, JSONObject terrain, List errors) { - Object rawWorms = terrain.opt("worms"); - if (!(rawWorms instanceof JSONArray worms)) { - errors.add(path + ".worms must be an array with at least one Perlin-worm profile."); - return; - } - if (worms.length() < 1) { - errors.add(path + ".worms must contain at least one Perlin-worm profile."); - return; - } - if (worms.length() > 16) { - errors.add(path + ".worms must contain at most 16 root profiles."); - } - Set ids = new HashSet(); - Set seeds = new HashSet(); - int profileCount = validateWormArray(path + ".worms", worms, 1, ids, seeds, errors); - if (profileCount > 128) { - errors.add(path + ".worms hierarchy must contain at most 128 profiles."); - } - } - - private static int validateWormArray( - String path, - JSONArray worms, - int depth, - Set ids, - Set seeds, - List errors - ) { - if (worms.length() > 16) { - errors.add(path + " must contain at most 16 profiles."); - } - double totalWeight = 0D; - int profileCount = 0; - for (int index = 0; index < worms.length(); index++) { - JSONObject worm = worms.optJSONObject(index); - String wormPath = path + "[" + index + "]"; - if (worm == null) { - errors.add(wormPath + " must be an object."); - continue; - } - profileCount++; - validateWormId(wormPath, worm, ids, errors); - long seed = validateWormSeed(wormPath, worm, errors); - if (!seeds.add(seed)) { - errors.add(wormPath + ".seed must be unique inside the worm hierarchy."); - } - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "weight", 0.000001D, 1000000D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "wavelength", 8D, 16384D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "detailWavelength", 8D, 16384D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "tortuosity", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "detailTortuosity", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "maxOffset", 0D, 1024D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(wormPath, worm, "segments", 1, 64, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "widthMultiplier", 0.125D, 8D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "bankMultiplier", 0.125D, 8D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "depthMultiplier", 0.125D, 8D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "bodyWavelength", 8D, 16384D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "bodyDetailWavelength", 8D, 16384D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "bodyDetailInfluence", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "widthVariation", 0D, 0.875D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "bankVariation", 0D, 0.875D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "depthVariation", 0D, 0.875D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "roofVariation", 0D, 0.875D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(wormPath, worm, "branchCap", 1, 8, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "branchDecay", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "confluenceMultiplier", 0D, 8D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "childChance", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - wormPath, worm, "branchChildChance", 0D, 1D, errors); - totalWeight += doubleValue(worm, "weight", 1D); - Object rawChildren = worm.opt("children"); - if (rawChildren == null || rawChildren == JSONObject.NULL) { - continue; - } - if (!(rawChildren instanceof JSONArray children)) { - errors.add(wormPath + ".children must be an array."); - continue; - } - if (children.length() > 0 && depth >= 4) { - errors.add(wormPath + ".children exceeds the maximum hierarchy depth of 4."); - continue; - } - profileCount += validateWormArray( - wormPath + ".children", - children, - depth + 1, - ids, - seeds, - errors - ); - } - if (worms.length() > 0 && (!Double.isFinite(totalWeight) || totalWeight <= 0D)) { - errors.add(path + " total weight must be finite and positive."); - } - return profileCount; - } - - private static void validateWormId(String path, JSONObject worm, Set ids, List errors) { - Object rawId = worm.opt("id"); - if (!(rawId instanceof String id) || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) { - errors.add(path + ".id must use 1 to 64 lowercase letters, digits, underscores, or hyphens."); - return; - } - if (!ids.add(id)) { - errors.add(path + ".id must be unique inside the worm hierarchy."); - } - } - - private static long validateWormSeed(String path, JSONObject worm, List errors) { - Object rawSeed = worm.opt("seed"); - if (rawSeed == null || rawSeed == JSONObject.NULL) { - return 1L; - } - if (!(rawSeed instanceof Number number) - || !Double.isFinite(number.doubleValue()) - || number.doubleValue() != StrictMath.rint(number.doubleValue())) { - errors.add(path + ".seed must be an integer."); - return 1L; - } - return number.longValue(); - } - - private static WormEnvelope wormEnvelope(JSONObject terrain) { - JSONArray worms = terrain.optJSONArray("worms"); - if (worms == null || worms.length() == 0) { - return new WormEnvelope(320D, 48); - } - double maximumOffset = 0D; - int maximumSegments = 1; - WormEnvelope envelope = wormEnvelope(worms, maximumOffset, maximumSegments); - maximumOffset = envelope.maximumOffset(); - maximumSegments = envelope.maximumSegments(); - return new WormEnvelope(maximumOffset, maximumSegments); - } - - private static WormEnvelope wormEnvelope(JSONArray worms, double maximumOffset, int maximumSegments) { - double resolvedOffset = maximumOffset; - int resolvedSegments = maximumSegments; - for (int index = 0; index < worms.length(); index++) { - JSONObject worm = worms.optJSONObject(index); - if (worm == null) { - continue; - } - resolvedOffset = Math.max(resolvedOffset, doubleValue(worm, "maxOffset", 320D)); - resolvedSegments = Math.max(resolvedSegments, integerValue(worm, "segments", 48)); - JSONArray children = worm.optJSONArray("children"); - if (children != null) { - WormEnvelope childEnvelope = wormEnvelope(children, resolvedOffset, resolvedSegments); - resolvedOffset = Math.max(resolvedOffset, childEnvelope.maximumOffset()); - resolvedSegments = Math.max(resolvedSegments, childEnvelope.maximumSegments()); - } - } - return new WormEnvelope(resolvedOffset, resolvedSegments); - } - - private static void validateCaves(File packFolder, String path, JSONObject caves, - JSONObject dimension, - boolean forceGeneratedGrotto, - List errors, List warnings) { - PackJsonFieldChecks.validateOptionalEnum(path, caves, "mode", CAVE_MODES, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "minimumSpacing", 16, 4096, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maximumPerReach", 0, 16, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxBoreDepth", 1, 256, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "throatRadius", 1, 16, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "waterLevelOffset", -64, 64, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "dryHeadroom", 0, 64, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoHorizontalRadius", 2, 128, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "grottoVerticalRadius", 2, 128, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "grottoWarpStrength", 0D, 32D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, caves, "parentBiomeInheritance", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodRadius", 4, 256, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodDepth", 4, 256, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, caves, "maxFloodVolume", 64, 1048576, errors); - PackJsonFieldChecks.validateOptionalEnum(path, caves, "fallback", CAVE_FALLBACKS, errors); - PackJsonFieldChecks.validateOptionalEnum(path, caves, "existingFluidPolicy", EXISTING_FLUID_POLICIES, errors); - validateNoiseChance(packFolder, caves, "entry", path, errors); - validateStyle(packFolder, caves, "grottoShapeStyle", path, errors); - validateStyle(packFolder, caves, "grottoWarpStyle", path, errors); - JSONObject deepPools = caves.has("deepPools") - ? requireObject(caves, "deepPools", path + ".deepPools", errors) - : null; - if (deepPools != null) { - validateDeepPools( - packFolder, - path + ".deepPools", - deepPools, - dimension, - errors, - warnings - ); - } - - String mode = stringValue(caves, "mode", "SEALED"); - if ("SEALED".equals(mode) && !forceGeneratedGrotto) { - return; - } - - int maximumPerReach = integerValue(caves, "maximumPerReach", 1); - double entryChance = noiseChanceValue(caves, "entry", 0.12D); - if (maximumPerReach == 0 || (!forceGeneratedGrotto && entryChance == 0D)) { - warnings.add(path + " enables cave hydrology but its entry gate cannot accept any connections."); - } - - int maxBoreDepth = integerValue(caves, "maxBoreDepth", 48); - int throatRadius = integerValue(caves, "throatRadius", 2); - int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48); - int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32); - if (throatRadius >= maxFloodRadius) { - errors.add(path + ".throatRadius must be smaller than maxFloodRadius so the proof boundary can contain the throat."); - } - if (throatRadius >= maxFloodDepth) { - errors.add(path + ".throatRadius must be smaller than maxFloodDepth so the proof boundary can contain the throat."); - } - if (maxBoreDepth > maxFloodDepth) { - warnings.add(path + ".maxBoreDepth exceeds maxFloodDepth; deeper cave targets found by the bore search will be rejected by containment proof."); - } - - String fallback = stringValue(caves, "fallback", "SEALED"); - if (forceGeneratedGrotto || usesGeneratedGrotto(mode, fallback)) { - validateGrotto(path, caves, errors); - } - } - - private static void validateDeepPools( - File packFolder, - String path, - JSONObject deepPools, - JSONObject dimension, - List errors, - List warnings - ) { - PackJsonFieldChecks.validateOptionalBoolean(path, deepPools, "enabled", errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "minimumSpacing", 16, 4096, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "maximumPerReach", 0, 16, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "minimumFluidY", -2048, 2048, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "maximumFluidY", -2048, 2048, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "searchRadius", 0, 256, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "searchAttempts", 1, 64, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "horizontalRadius", 2, 128, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "verticalRadius", 2, 64, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "dryHeadroom", 1, 63, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - path, deepPools, "shapeVariation", 0D, 0.75D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange( - path, deepPools, "warpStrength", 0D, 64D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange( - path, deepPools, "maximumVolume", 64, 1048576, errors); - validateNoiseChance(packFolder, deepPools, "reach", path, errors); - validateStyle(packFolder, deepPools, "shapeStyle", path, errors); - validateStyle(packFolder, deepPools, "warpStyle", path, errors); - validateFluidPalette(path, deepPools, errors); - - int minimumFluidY = integerValue(deepPools, "minimumFluidY", -224); - int maximumFluidY = integerValue(deepPools, "maximumFluidY", -104); - int searchRadius = integerValue(deepPools, "searchRadius", 16); - int horizontalRadius = integerValue(deepPools, "horizontalRadius", 18); - int verticalRadius = integerValue(deepPools, "verticalRadius", 8); - int dryHeadroom = integerValue(deepPools, "dryHeadroom", 4); - int maximumVolume = integerValue(deepPools, "maximumVolume", 32768); - if (minimumFluidY > maximumFluidY) { - errors.add(path + ".minimumFluidY must not exceed maximumFluidY."); - } - if (dryHeadroom >= verticalRadius) { - errors.add(path + ".dryHeadroom must be smaller than verticalRadius."); - } - if (searchRadius + horizontalRadius > 128) { - errors.add(path + ".searchRadius plus horizontalRadius must not exceed 128 blocks."); - } - long minimumVolume = grottoVolume(horizontalRadius, verticalRadius); - if (minimumVolume > maximumVolume) { - errors.add(path + ".maximumVolume must be at least " + minimumVolume - + " to contain the base deep-pool chamber."); - } - - if (!booleanValue(deepPools, "enabled", false)) { - return; - } - - JSONObject dimensionHeight = dimension.optJSONObject("dimensionHeight"); - int minimumHeight = dimensionHeight == null ? -64 : integerValue(dimensionHeight, "min", -64); - int maximumHeight = dimensionHeight == null ? 320 : integerValue(dimensionHeight, "max", 320); - int lowestBoundaryY = minimumFluidY - (verticalRadius * 2 - dryHeadroom) - 1; - int highestBoundaryY = maximumFluidY + dryHeadroom + 1; - if (lowestBoundaryY <= minimumHeight || highestBoundaryY >= maximumHeight) { - errors.add(path + " fluid range and chamber envelope must remain inside dimensionHeight."); - } - - int maximumPerReach = integerValue(deepPools, "maximumPerReach", 1); - double reachChance = noiseChanceValue(deepPools, "reach", 1D / 3D); - if (maximumPerReach == 0 || reachChance == 0D) { - warnings.add(path + " is enabled but its reach gate cannot accept any pools."); - } - } - - private static void validateGrotto(String path, JSONObject caves, List errors) { - int throatRadius = integerValue(caves, "throatRadius", 2); - int dryHeadroom = integerValue(caves, "dryHeadroom", 4); - int horizontalRadius = integerValue(caves, "grottoHorizontalRadius", 12); - int verticalRadius = integerValue(caves, "grottoVerticalRadius", 7); - double warpStrength = doubleValue(caves, "grottoWarpStrength", 2D); - int maxFloodRadius = integerValue(caves, "maxFloodRadius", 48); - int maxFloodDepth = integerValue(caves, "maxFloodDepth", 32); - int maxFloodVolume = integerValue(caves, "maxFloodVolume", 8192); - if (throatRadius >= horizontalRadius || throatRadius >= verticalRadius) { - addDistinct(errors, path + ".throatRadius must be smaller than both grotto radii so a sealed chamber can surround the inlet."); - } - if (dryHeadroom >= (verticalRadius * 2) + 1) { - addDistinct(errors, path + ".dryHeadroom must fit inside the generated grotto height."); - } - - int warpEnvelope = Double.isFinite(warpStrength) ? (int) Math.ceil(warpStrength) : 0; - int requiredRadius = horizontalRadius + warpEnvelope + 1; - int requiredDepth = verticalRadius + warpEnvelope + 1; - if (maxFloodRadius < requiredRadius) { - addDistinct(errors, path + ".maxFloodRadius must be at least " + requiredRadius - + " to prove the configured grotto and its sealed shell."); - } - if (maxFloodDepth < requiredDepth) { - addDistinct(errors, path + ".maxFloodDepth must be at least " + requiredDepth - + " to prove the configured grotto and its sealed shell."); - } - - long volume = grottoVolume(horizontalRadius, verticalRadius); - if (volume > maxFloodVolume) { - addDistinct(errors, path + ".maxFloodVolume must be at least " + volume - + " to contain the configured grotto before its throat and shell are considered."); - } - } - - private static long grottoVolume(int horizontalRadius, int verticalRadius) { - long volume = 0L; - double horizontalSquared = (double) horizontalRadius * horizontalRadius; - double verticalSquared = (double) verticalRadius * verticalRadius; - for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) { - for (int dy = -verticalRadius; dy <= verticalRadius; dy++) { - double remaining = 1D - ((double) dx * dx / horizontalSquared) - - ((double) dy * dy / verticalSquared); - if (remaining < 0D) { - continue; - } - int maximumZ = (int) Math.floor(horizontalRadius * Math.sqrt(remaining)); - volume += (maximumZ * 2L) + 1L; - } - } - return volume; - } - - private static boolean usesGeneratedGrotto(String mode, String fallback) { - return "GENERATE_GROTTO".equals(mode) - || "GROTTO_OR_CLOSED_COMPONENT".equals(mode) - || "WATERFALL_POOL".equals(mode) - || "GENERATE_GROTTO".equals(fallback); - } - - private static void validateSinkholeCapability( - String terminalPath, - DimensionRiverContext context, - JSONObject caves, - String cavesPath, - List errors - ) { - String suffix = " in Dimension '" + context.dimensionKey() + "'."; - if (!booleanValue(context.dimension(), "carvingEnabled", true)) { - addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires carvingEnabled to be true" + suffix); - } - if (!booleanValue(context.dimension(), "useMantle", true)) { - addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires useMantle to be true" + suffix); - } - if (disabled(context.dimension(), "CARVED")) { - addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires CARVED to remain enabled" + suffix); - } - if (disabled(context.dimension(), "RIVER_HYDROLOGY")) { - addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires RIVER_HYDROLOGY to remain enabled" + suffix); - } - if ("SEALED".equals(stringValue(caves, "mode", "SEALED"))) { - addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires a non-SEALED caves.mode" + suffix); - } - if (integerValue(caves, "maximumPerReach", 1) <= 0) { - addDistinct(errors, terminalPath + " SINKHOLE_GROTTO requires caves.maximumPerReach above zero" + suffix); - } - validateGrotto(cavesPath, caves, errors); - } - - private static void validateOverrideSinkhole( - File packFolder, - String terminalPath, - String resourceKey, - String resourceType, - List contexts, - List errors, - List warnings - ) { - boolean referenced = false; - for (DimensionRiverContext context : contexts) { - boolean reachable = "Region".equals(resourceType) - ? context.regionKeys().contains(resourceKey) - : referencedSurfaceBiomes(packFolder, context.regionKeys()).contains(resourceKey); - if (!reachable) { - continue; - } - referenced = true; - JSONObject caves = nestedObject(context.rivers(), "caves", "Dimension '" - + context.dimensionKey() + "' rivers", errors); - if (caves != null) { - validateSinkholeCapability( - terminalPath, - context, - caves, - "Dimension '" + context.dimensionKey() + "' rivers.caves", - errors - ); - } - } - if (!referenced) { - addDistinct(warnings, terminalPath - + " selects SINKHOLE_GROTTO but no enabled river dimension reaches this " - + resourceType.toLowerCase() + "."); - } - } - - private static Set referencedSurfaceBiomes(File packFolder, Set regionKeys) { - Set biomes = new HashSet<>(); - File regionsFolder = new File(packFolder, "regions"); - for (String regionKey : regionKeys) { - JSONObject region = PackValidationIo.readJson(new File(regionsFolder, regionKey + ".json")); - if (region == null) { - continue; - } - collectBiomeKeys(region.optJSONArray("landBiomes"), biomes); - collectBiomeKeys(region.optJSONArray("seaBiomes"), biomes); - collectBiomeKeys(region.optJSONArray("shoreBiomes"), biomes); - } - Set roots = Set.copyOf(biomes); - for (String biomeKey : roots) { - collectBiomeChildren(packFolder, biomeKey, 0, biomes); - } - return biomes; - } - - private static void collectBiomeChildren(File packFolder, String biomeKey, int depth, Set biomes) { - if (depth >= 4) { - return; - } - JSONObject biome = PackValidationIo.readJson(new File(packFolder, "biomes/" + biomeKey + ".json")); - if (biome == null) { - return; - } - JSONArray children = biome.optJSONArray("children"); - if (children == null) { - return; - } - for (int index = 0; index < children.length(); index++) { - String child = children.optString(index, null); - if (child == null || child.isBlank()) { - continue; - } - boolean added = biomes.add(child); - if (added) { - collectBiomeChildren(packFolder, child, depth + 1, biomes); - } - } - } - - private static Set referencedKeys(JSONArray keys) { - Set referenced = new HashSet<>(); - collectBiomeKeys(keys, referenced); - return Set.copyOf(referenced); - } - - private static void collectBiomeKeys(JSONArray keys, Set destination) { - if (keys == null) { - return; - } - for (int index = 0; index < keys.length(); index++) { - String key = keys.optString(index, null); - if (key != null && !key.isBlank()) { - destination.add(key); - } - } - } - - private static boolean disabled(JSONObject dimension, String flag) { - JSONArray disabled = dimension.optJSONArray("disabledComponents"); - if (disabled == null) { - return false; - } - for (int index = 0; index < disabled.length(); index++) { - if (flag.equals(disabled.optString(index, null))) { - return true; - } - } - return false; - } - - private static void addDistinct(List destination, String value) { - if (!destination.contains(value)) { - destination.add(value); - } - } - - private static void validateOverrides(File packFolder, File resourceFolder, String resourceType, - List contexts, - List errors, List warnings) { - if (!resourceFolder.isDirectory()) { - return; - } - List files = PackValidationIo.listJsonRecursive(resourceFolder); - files.sort(Comparator.comparing(File::getPath)); - for (File file : files) { - JSONObject resource = PackValidationIo.readJson(file); - if (resource == null || !resource.has("riverOverride")) { - continue; - } - String key = PackValidationIo.deriveKey(resourceFolder, file); - String path = resourceType + " '" + key + "' riverOverride"; - Object rawOverride = resource.opt("riverOverride"); - if (rawOverride == JSONObject.NULL) { - continue; - } - if (!(rawOverride instanceof JSONObject override)) { - errors.add(path + " must be an object or null."); - continue; - } - validateOverride(packFolder, path, key, resourceType, override, contexts, errors, warnings); - } - } - - private static void validateOverride(File packFolder, String path, String resourceKey, String resourceType, - JSONObject override, List contexts, - List errors, List warnings) { - PackJsonFieldChecks.validateOptionalBoolean(path, override, "allowSources", errors); - PackJsonFieldChecks.validateOptionalEnum(path, override, "routingPolicy", ROUTING_POLICIES, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "routingCostMultiplier", 0D, 64D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "widthMultiplier", 0.0001D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "bankWidthMultiplier", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "depthMultiplier", 0.0001D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "maxIncisionMultiplier", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "continuationChanceMultiplier", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, override, "caveEntryMultiplier", 0D, 16D, errors); - PackJsonFieldChecks.validateOptionalEnum(path, override, "terminalMode", TERMINAL_MODES, errors); - boolean validateSuitability = reachesOverworldNativeStructureRoles( - packFolder, resourceKey, resourceType, contexts); - validateBiomePool(packFolder, path, override, "channelBiomes", RiverBiomeRole.CHANNEL, true, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, override, "bankBiomes", RiverBiomeRole.BANK, true, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, override, "mouthBiomes", RiverBiomeRole.MOUTH, true, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, override, "dryBiomes", RiverBiomeRole.DRY, true, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, override, "floodedCaveBiomes", RiverBiomeRole.FLOODED_CAVE, true, - validateSuitability, errors, warnings); - if ("SINKHOLE_GROTTO".equals(stringValue(override, "terminalMode", null))) { - validateOverrideSinkhole( - packFolder, - path + ".terminalMode", - resourceKey, - resourceType, - contexts, - errors, - warnings - ); - } - } - - private static void validateBiomePools(File packFolder, String path, JSONObject biomes, boolean allowNull, - boolean validateSuitability, - List errors, List warnings) { - validateStyle(packFolder, biomes, "selectionStyle", path, errors); - validateBiomePool(packFolder, path, biomes, "channel", RiverBiomeRole.CHANNEL, allowNull, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, biomes, "bank", RiverBiomeRole.BANK, allowNull, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, biomes, "mouth", RiverBiomeRole.MOUTH, allowNull, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, biomes, "dry", RiverBiomeRole.DRY, allowNull, - validateSuitability, errors, warnings); - validateBiomePool(packFolder, path, biomes, "floodedCave", RiverBiomeRole.FLOODED_CAVE, allowNull, - validateSuitability, errors, warnings); - } - - private static void validateBiomePool(File packFolder, String path, JSONObject owner, String field, - RiverBiomeRole role, boolean allowNull, - boolean validateSuitability, - List errors, List warnings) { - if (!owner.has(field)) { - return; - } - Object rawPool = owner.opt(field); - if (rawPool == JSONObject.NULL && allowNull) { - return; - } - if (!(rawPool instanceof JSONArray pool)) { - errors.add(path + "." + field + " must be an array" + (allowNull ? " or null" : "") + "."); - return; - } - - Set seen = new HashSet<>(); - File biomesFolder = new File(packFolder, "biomes"); - for (int index = 0; index < pool.length(); index++) { - Object rawKey = pool.opt(index); - String entryPath = path + "." + field + "[" + index + "]"; - if (!(rawKey instanceof String key) || key.isBlank()) { - errors.add(entryPath + " must name a biome resource."); - continue; - } - if (!seen.add(key)) { - warnings.add(entryPath + " duplicates biome '" + key + "' in the same river pool."); - continue; - } - File biomeFile = new File(biomesFolder, key + ".json"); - if (!biomeFile.isFile()) { - errors.add(entryPath + " references missing biome '" + key + "'."); - continue; - } - if (validateSuitability) { - validateBiomeSuitability(entryPath, key, role, PackValidationIo.readJson(biomeFile), warnings); - } - } - } - - private static boolean usesOverworldNativeStructureRoles(DimensionRiverContext context) { - return "NORMAL".equals(stringValue(context.dimension(), "environment", "NORMAL")); - } - - private static boolean reachesOverworldNativeStructureRoles( - File packFolder, - String resourceKey, - String resourceType, - List contexts - ) { - for (DimensionRiverContext context : contexts) { - if (!usesOverworldNativeStructureRoles(context)) { - continue; - } - boolean reachable = "Region".equals(resourceType) - ? context.regionKeys().contains(resourceKey) - : referencedSurfaceBiomes(packFolder, context.regionKeys()).contains(resourceKey); - if (reachable) { - return true; - } - } - return false; - } - - private static void validateBiomeSuitability(String path, String biomeKey, RiverBiomeRole role, - JSONObject biome, List warnings) { - if (biome == null || role == RiverBiomeRole.DRY || role == RiverBiomeRole.FLOODED_CAVE) { - return; - } - String derivative = stringValue(biome, "vanillaDerivative", null); - if (derivative == null || derivative.isBlank()) { - derivative = stringValue(biome, "derivative", "minecraft:the_void"); - } - String normalized = derivative.indexOf(':') >= 0 ? derivative : "minecraft:" + derivative; - if (!normalized.startsWith("minecraft:")) { - return; - } - boolean suitable = switch (role) { - case CHANNEL, MOUTH -> normalized.contains("ocean") || normalized.endsWith("river"); - case BANK -> normalized.endsWith("beach") || normalized.endsWith("shore"); - default -> true; - }; - if (!suitable) { - warnings.add(path + " assigns biome '" + biomeKey + "' the inferred river role " + role.label - + " but its vanilla derivative '" + normalized - + "' does not match that role; native structure selection will use Iris's safe role fallback."); - } - } - - private static void validateNoiseChance(File packFolder, JSONObject owner, String field, String path, - List errors) { - if (!owner.has(field)) { - return; - } - JSONObject chance = requireObject(owner, field, path + "." + field, errors); - if (chance == null) { - return; - } - String chancePath = path + "." + field; - PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "chance", 0D, 1D, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(chancePath, chance, "influence", 0D, 1D, errors); - validateStyle(packFolder, chance, "style", chancePath, errors); - } - - private static void validateStyledRange(File packFolder, JSONObject owner, String field, String path, - double minimum, double maximum, - List errors, List warnings) { - if (!owner.has(field)) { - return; - } - String rangePath = path + "." + field; - JSONObject range = resolveObject(packFolder, owner.opt(field), "snippet/style-range/", rangePath, errors); - if (range == null) { - return; - } - boolean hasMinimum = range.has("min") && range.opt("min") != JSONObject.NULL; - boolean hasMaximum = range.has("max") && range.opt("max") != JSONObject.NULL; - if (!hasMinimum && !hasMaximum) { - errors.add(rangePath + " must set min and max explicitly."); - } else if (!hasMinimum || !hasMaximum) { - warnings.add(rangePath - + " should set both min and max explicitly; the omitted bound uses the shared style-range default."); - } - PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "min", minimum, maximum, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(rangePath, range, "max", minimum, maximum, errors); - double minimumValue = doubleValue(range, "min", 16D); - double maximumValue = doubleValue(range, "max", 32D); - if (Double.isFinite(minimumValue) && Double.isFinite(maximumValue) && minimumValue > maximumValue) { - errors.add(rangePath + ".min must not exceed " + rangePath + ".max."); - } - validateStyle(packFolder, range, "style", rangePath, errors); - } - - private static double styledRangeMaximum( - File packFolder, - JSONObject owner, - String field, - String path, - double fallback - ) { - if (!owner.has(field)) { - return fallback; - } - JSONObject range = resolveObject( - packFolder, - owner.opt(field), - "snippet/style-range/", - path + "." + field, - new ArrayList<>() - ); - return range == null ? fallback : doubleValue(range, "max", fallback); - } - - private static void validateStyle(File packFolder, JSONObject owner, String field, String path, - List errors) { - if (!owner.has(field)) { - return; - } - validateStyle(packFolder, owner.opt(field), path + "." + field, errors, new HashSet<>()); - } - - private static void validateStyle(File packFolder, Object rawStyle, String path, - List errors, Set dependencyStack) { - String styleMarker = rawStyle instanceof String reference ? "style:" + reference : null; - if (styleMarker != null && !dependencyStack.add(styleMarker)) { - errors.add(path + " has a cyclic river-noise style snippet dependency."); - return; - } - try { - JSONObject style = resolveObject(packFolder, rawStyle, "snippet/style/", path, errors); - if (style != null) { - validateResolvedStyle(packFolder, style, path, errors, dependencyStack); - } - } finally { - if (styleMarker != null) { - dependencyStack.remove(styleMarker); - } - } - } - - private static void validateResolvedStyle(File packFolder, JSONObject style, String path, - List errors, Set dependencyStack) { - PackJsonFieldChecks.validateOptionalEnum(path, style, "style", NOISE_STYLES, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularFrequency", 0D, Double.MAX_VALUE, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "cellularZoom", 0.00001D, Double.MAX_VALUE, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "zoom", 0.00001D, Double.MAX_VALUE, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "multiplier", - -Double.MAX_VALUE, Double.MAX_VALUE, errors); - PackJsonFieldChecks.validateOptionalDoubleRange(path, style, "exponent", 0.01562D, 64D, errors); - PackJsonFieldChecks.validateOptionalIntegerRange(path, style, "cacheSize", 0, 8192, errors); - - if (style.has("expression") && style.opt("expression") != JSONObject.NULL) { - Object rawExpression = style.opt("expression"); - if (!(rawExpression instanceof String expressionKey) || expressionKey.isBlank()) { - errors.add(path + ".expression must name an expression resource."); - } else { - validateExpression(packFolder, expressionKey, path, errors, dependencyStack); - } - } - if (style.has("fracture") && style.opt("fracture") != JSONObject.NULL) { - validateStyle(packFolder, style.opt("fracture"), path + ".fracture", errors, dependencyStack); - } - } - - private static void validateExpression(File packFolder, String expressionKey, String usePath, - List errors, Set dependencyStack) { - String expressionMarker = "expression:" + expressionKey; - if (!dependencyStack.add(expressionMarker)) { - errors.add(usePath + " has a cyclic river-noise expression dependency through '" + expressionKey + "'."); - return; - } - try { - File expressionFile = new File(new File(packFolder, "expressions"), expressionKey + ".json"); - JSONObject expression = PackValidationIo.readJson(expressionFile); - if (expression == null) { - return; - } - scanExpressionEntries(packFolder, expression, "variables", expressionKey, usePath, errors, dependencyStack); - scanExpressionEntries(packFolder, expression, "functions", expressionKey, usePath, errors, dependencyStack); - } finally { - dependencyStack.remove(expressionMarker); - } - } - - private static void scanExpressionEntries(File packFolder, JSONObject expression, String field, - String expressionKey, String usePath, - List errors, Set dependencyStack) { - JSONArray entries = expression.optJSONArray(field); - if (entries == null) { - return; - } - for (int index = 0; index < entries.length(); index++) { - Object rawEntry = entries.opt(index); - String snippetFolder = "variables".equals(field) - ? "snippet/expression-load/" - : "snippet/expression-function/"; - JSONObject entry = resolveExpressionEntry(packFolder, rawEntry, snippetFolder); - if (entry == null) { - continue; - } - String stream = stringValue(entry, "engineStreamValue", null); - if (stream != null && isUnsafeRiverStream(stream)) { - errors.add(usePath + " uses expression '" + expressionKey + "' " + field + "[" + index - + "].engineStreamValue '" + stream - + "', which depends on final river-shaped terrain and would recurse during river generation."); - } - if (entry.has("styleValue")) { - validateStyle(packFolder, entry.opt("styleValue"), usePath + " -> expression '" + expressionKey - + "' " + field + "[" + index + "].styleValue", errors, dependencyStack); - } - } - } - - private static JSONObject resolveExpressionEntry(File packFolder, Object rawEntry, String snippetFolder) { - if (rawEntry instanceof JSONObject entry) { - return entry; - } - if (!(rawEntry instanceof String reference) || !reference.startsWith("snippet/")) { - return null; - } - String resolved = reference.startsWith(snippetFolder) - ? reference - : snippetFolder + reference.substring("snippet/".length()); - return PackValidationIo.readJson(new File(packFolder, resolved + ".json")); - } - - private static boolean isUnsafeRiverStream(String stream) { - return UNSAFE_RIVER_STREAMS.contains(stream) || stream.startsWith("RIVER_"); - } - - private static Set noiseStyles() { - Set styles = new HashSet<>(); - for (NoiseStyle style : NoiseStyle.values()) { - styles.add(style.name()); - } - return Set.copyOf(styles); - } - - private static JSONObject nestedObject(JSONObject owner, String field, String path, List errors) { - if (!owner.has(field)) { - return new JSONObject(); - } - return requireObject(owner, field, path + "." + field, errors); - } - - private static JSONObject requireObject(JSONObject owner, String field, String path, List errors) { - Object raw = owner.opt(field); - if (!(raw instanceof JSONObject object)) { - errors.add(path + " must be an object."); - return null; - } - return object; - } - - private static JSONObject resolveObject(File packFolder, Object raw, String snippetFolder, - String path, List errors) { - if (raw instanceof JSONObject object) { - return object; - } - if (raw instanceof String reference && reference.startsWith("snippet/")) { - String resolved = reference.startsWith(snippetFolder) - ? reference - : snippetFolder + reference.substring("snippet/".length()); - return PackValidationIo.readJson(new File(packFolder, resolved + ".json")); - } - errors.add(path + " must be an object or snippet reference."); - return null; - } - - private static boolean booleanValue(JSONObject object, String field, boolean defaultValue) { - Object raw = object.opt(field); - return raw instanceof Boolean value ? value : defaultValue; - } - - private static int integerValue(JSONObject object, String field, int defaultValue) { - Object raw = object.opt(field); - if (!(raw instanceof Number number) || !Double.isFinite(number.doubleValue())) { - return defaultValue; - } - return number.intValue(); - } - - private static double doubleValue(JSONObject object, String field, double defaultValue) { - Object raw = object.opt(field); - return raw instanceof Number number ? number.doubleValue() : defaultValue; - } - - private static String stringValue(JSONObject object, String field, String defaultValue) { - Object raw = object.opt(field); - return raw instanceof String value ? value : defaultValue; - } - - private static double noiseChanceValue(JSONObject owner, String field, double defaultValue) { - JSONObject chance = owner.optJSONObject(field); - return chance == null ? defaultValue : doubleValue(chance, "chance", defaultValue); - } - - private record WormEnvelope(double maximumOffset, int maximumSegments) { - } - - record Validation(List errors, List warnings) { - Validation { - errors = List.copyOf(errors); - warnings = List.copyOf(warnings); - } - } - - private record DimensionRiverContext( - String dimensionKey, - JSONObject dimension, - JSONObject rivers, - Set regionKeys - ) { - } - - private enum RiverBiomeRole { - CHANNEL("SEA"), - BANK("SHORE"), - MOUTH("SEA"), - DRY("LAND"), - FLOODED_CAVE("CAVE"); - - private final String label; - - RiverBiomeRole(String label) { - this.label = label; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java index c06873cc7..81ef731ac 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackValidator.java @@ -84,9 +84,6 @@ public final class PackValidator { packFolder, dimensionFiles, validateLiveRegistries); addDistinct(blockingErrors, imageMaps.errors()); addDistinct(warnings, imageMaps.warnings()); - PackRiverValidator.Validation riverValidation = PackRiverValidator.validate(packFolder, dimensionFiles); - addDistinct(blockingErrors, riverValidation.errors()); - addDistinct(warnings, riverValidation.warnings()); blockingErrors.addAll(PackCaveProfileValidator.validateLegacyFields(packFolder)); PackLootValidator.LootGraphIssues lootIssues = PackLootValidator.validateLootGraph(packFolder); addDistinct(blockingErrors, lootIssues.errors()); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 75fad5035..7a29aaa33 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -29,22 +29,8 @@ import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisGenerator; import art.arcane.iris.engine.object.IrisInterpolator; -import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverCaves; -import art.arcane.iris.engine.object.IrisRiverDeepPools; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.object.IrisRiverOverride; -import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; -import art.arcane.iris.engine.object.IrisRiverWater; import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; -import art.arcane.iris.engine.river.runtime.IrisRiverRuntime; -import art.arcane.iris.engine.river.runtime.IrisRiverRuntimeContext; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBiome; @@ -67,6 +53,8 @@ import lombok.ToString; import java.io.File; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; @@ -75,28 +63,37 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiFunction; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"}) -@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "riverRuntime", "imageMapRuntime"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"}) +@ToString(exclude = {"data", "gridBoundsCache", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int HEIGHT_BOUNDS_GRID = 4; + private static final Comparator INTERPOLATOR_ORDER = Comparator + .comparing((IrisInterpolator interpolator) -> interpolator.getFunction().name()) + .thenComparingDouble(IrisInterpolator::getHorizontalScale); + private static final Comparator GENERATOR_ORDER = Comparator.comparing( + IrisGenerator::getLoadKey, + Comparator.nullsFirst(Comparator.naturalOrder()) + ); + private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = { + InferredType.LAND, + InferredType.CAVE, + InferredType.SEA, + InferredType.SHORE + }; @Getter(AccessLevel.NONE) private final transient ThreadLocal gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new); - /** - * Immutable snapshot of {@link #generators} taken once at the end of construction, in the exact - * iteration order the map produces. The per-column height paths walk these arrays instead of - * allocating map/set iterators, and the frozen order keeps the floating point accumulation order - * identical to the map iteration it replaces. Mutating {@link #generators} after construction is - * not reflected here. - */ @Getter(AccessLevel.NONE) private final transient IrisInterpolator[] frozenInterpolators; @Getter(AccessLevel.NONE) private final transient IrisGenerator[][] frozenGenerators; + @Getter(AccessLevel.NONE) + private final transient Map>> inferredBiomeStreams; private RNG rng; private double fluidHeight; private IrisData data; @@ -112,22 +109,14 @@ public class IrisComplex implements DataProvider { private ProceduralStream shoreBiomeStream; private ProceduralStream baseBiomeStream; private ProceduralStream baseBiomeIDStream; - private ProceduralStream naturalTrueBiomeStream; private ProceduralStream trueBiomeStream; private ProceduralStream trueBiomeDerivativeStream; - private ProceduralStream naturalHeightStream; private ProceduralStream heightStream; private ProceduralStream roundedHeighteightStream; private ProceduralStream maxHeightStream; private ProceduralStream overlayStream; private ProceduralStream heightFluidStream; - private ProceduralStream naturalSlopeStream; private ProceduralStream slopeStream; - private ProceduralStream riverSurfaceStream; - private ProceduralStream riverDistanceStream; - private ProceduralStream riverFlowStream; - private ProceduralStream riverCarveWeightStream; - private ProceduralStream riverWaterSurfaceStream; private ProceduralStream topSurfaceStream; private ProceduralStream terrainSurfaceDecoration; private ProceduralStream terrainCeilingDecoration; @@ -138,13 +127,10 @@ public class IrisComplex implements DataProvider { private ProceduralStream shoreSurfaceDecoration; private ProceduralStream rockStream; private ProceduralStream fluidStream; - private ProceduralStream riverFluidStream; - private ProceduralStream riverDeepPoolFluidStream; private IrisBiome focusBiome; private IrisRegion focusRegion; private Map> generatorBounds; private Set generatorBiomes; - private IrisRiverRuntime riverRuntime; private transient IrisImageMapRuntime imageMapRuntime; // Copy-on-write: reads happen per column on every burst thread; the synchronizedMap // monitor was taken on every HIT. Writes are once per biome and bounded, so a fresh map @@ -170,6 +156,7 @@ public class IrisComplex implements DataProvider { focusBiome = engine.getFocus(); focusRegion = engine.getFocusRegion(); Map> inferredStreams = new HashMap<>(); + KList preparedRegions = new KList<>(); if (focusBiome != null) { focusBiome = focusBiome.withInferredType(InferredType.LAND); @@ -178,31 +165,34 @@ public class IrisComplex implements DataProvider { //@builder if (focusRegion != null) { - prepareInferredBiomes(focusRegion); - focusRegion.getNaturalBiomes(this).forEach(this::registerGenerators); + prepareInferredBiomes(focusRegion, preparedRegions); + focusRegion.getAllBiomes(this).forEach(this::registerGenerators); } else { engine.getDimension().getRegions().forEach(regionKey -> { IrisRegion region = data.getRegionLoader().load(regionKey); if (region == null) { return; } - prepareInferredBiomes(region); - region.getNaturalBiomes(this).forEach(this::registerGenerators); + prepareInferredBiomes(region, preparedRegions); + region.getAllBiomes(this).forEach(this::registerGenerators); }); for (IrisRegion region : imageMapRuntime.getMappedRegions()) { - prepareInferredBiomes(region); - region.getNaturalBiomes(this).forEach(this::registerGenerators); + prepareInferredBiomes(region, preparedRegions); + region.getAllBiomes(this).forEach(this::registerGenerators); } imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators); } - int interpolatorCount = generators.size(); + inferredBiomeStreams = compileInferredBiomeStreams( + preparedRegions, + (region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome) + ); + GeneratorGroup[] generatorGroups = freezeGeneratorGroups(generators); + int interpolatorCount = generatorGroups.length; frozenInterpolators = new IrisInterpolator[interpolatorCount]; frozenGenerators = new IrisGenerator[interpolatorCount][]; - int frozenIndex = 0; - for (Map.Entry> entry : generators.entrySet()) { - frozenInterpolators[frozenIndex] = entry.getKey(); - frozenGenerators[frozenIndex] = entry.getValue().toArray(new IrisGenerator[0]); - frozenIndex++; + for (int frozenIndex = 0; frozenIndex < generatorGroups.length; frozenIndex++) { + frozenInterpolators[frozenIndex] = generatorGroups[frozenIndex].interpolator(); + frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators(); } generatorBounds = buildGeneratorBounds(engine); KList overlayNoise = engine.getDimension().getOverlayNoise(); @@ -221,29 +211,6 @@ public class IrisComplex implements DataProvider { .select(engine.getDimension().getRockPalette().getBlockData(data)); fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() .select(engine.getDimension().getFluidPalette().getBlockData(data)); - IrisRiverNetwork configuredRivers = engine.getDimension().getRivers(); - IrisRiverWater configuredRiverWater = configuredRivers == null ? null : configuredRivers.getWater(); - riverFluidStream = configuredRivers != null && configuredRivers.isEnabled() - ? configuredFluidStream( - Objects.requireNonNull(configuredRiverWater).getFluidPalette(), - rng.nextParallelRNG(79), - "River water" - ) - : fluidStream; - IrisRiverCaves configuredRiverCaves = configuredRivers == null ? null : configuredRivers.getCaves(); - IrisRiverDeepPools configuredDeepPools = configuredRiverCaves == null - ? null - : configuredRiverCaves.getDeepPools(); - riverDeepPoolFluidStream = configuredRivers != null - && configuredRivers.isEnabled() - && configuredDeepPools != null - && configuredDeepPools.isEnabled() - ? configuredFluidStream( - configuredDeepPools.getFluidPalette(), - rng.nextParallelRNG(80), - "River deep-pool" - ) - : riverFluidStream; regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream() .zoom(engine.getDimension().getRegionZoom()); regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE); @@ -262,41 +229,22 @@ public class IrisComplex implements DataProvider { regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i), String.valueOf(i * 38445).hashCode() * 3245556666L)); caveBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getCaveBiomeStyle().create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(r.getCaveBiomeZoom()) - .selectRarity(loadInferredBiomes(r.getCaveBiomes(), InferredType.CAVE)) - .onNull(emptyBiome) - ).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize); + .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE)) + .convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize); inferredStreams.put(InferredType.CAVE, caveBiomeStream); landBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(engine.getDimension().getLandZoom()) - .zoom(r.getLandBiomeZoom()) - .selectRarity(loadInferredBiomes(r.getLandBiomes(), InferredType.LAND)) - ).convertAware2D(ProceduralStream::get) + .convert((r) -> createInferredBiomeStream(r, InferredType.LAND)) + .convertAware2D(ProceduralStream::get) .cache2D("landBiomeStream", engine, cacheSize); inferredStreams.put(InferredType.LAND, landBiomeStream); seaBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(engine.getDimension().getSeaZoom()) - .zoom(r.getSeaBiomeZoom()) - .selectRarity(loadInferredBiomes(r.getSeaBiomes(), InferredType.SEA)) - ).convertAware2D(ProceduralStream::get) + .convert((r) -> createInferredBiomeStream(r, InferredType.SEA)) + .convertAware2D(ProceduralStream::get) .cache2D("seaBiomeStream", engine, cacheSize); inferredStreams.put(InferredType.SEA, seaBiomeStream); shoreBiomeStream = regionStream.contextInjecting(engine, (c, x, z) -> c.getRegion().get(x, z)) - .convert((r) - -> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() - .zoom(engine.getDimension().getBiomeZoom()) - .zoom(r.getShoreBiomeZoom()) - .selectRarity(loadInferredBiomes(r.getShoreBiomes(), InferredType.SHORE)) - ).convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize); + .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE)) + .convertAware2D(ProceduralStream::get).cache2D("shoreBiomeStream", engine, cacheSize); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(), Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) : @@ -315,90 +263,28 @@ public class IrisComplex implements DataProvider { return mapped == null ? biome : mapped; }) .cache2D("imageMappedBaseBiomeStream", engine, cacheSize); - naturalHeightStream = ProceduralStream.of((x, z) -> { - IrisBiome b = focusBiome != null ? focusBiome : baseBiomeStream.get(x, z); - double proceduralHeight = getHeight(engine, b, x, z, engine.getSeedManager().getHeight()); + heightStream = ProceduralStream.of((x, z) -> { + double proceduralHeight = getHeight(engine, x, z, engine.getSeedManager().getHeight()); return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); - }, Interpolated.DOUBLE).cache2DDouble("naturalHeightStream", engine, cacheSize); - naturalSlopeStream = naturalHeightStream.slope(3) - .cache2DDouble("naturalSlopeStream", engine, cacheSize); - naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, + }, Interpolated.DOUBLE).cache2DDouble("heightStream", engine, cacheSize); + slopeStream = heightStream.slope(3) + .cache2DDouble("slopeStream", engine, cacheSize); + trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, b -> focusBiome)) - .cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream + .cache2D("trueBiomeStream-focus", engine, cacheSize) : heightStream .convertAware2D((h, x, z) -> { IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); return mapped == null ? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z, fluidHeight) : mapped; }) - .cache2D("naturalTrueBiomeStream", engine, cacheSize); - if (engine.getDimension().getRivers() != null && engine.getDimension().getRivers().isEnabled()) { - ProceduralStream naturalOceanStream = createNaturalOceanStream( - bridgeStream, - focusBiome - ).cache2D("naturalOceanStream", engine, cacheSize); - int riverFluidHeight = engine.getDimension().getRivers().getWater().getFluidHeight() - - engine.getDimension().getMinHeight(); - riverRuntime = new IrisRiverRuntime(new IrisRiverRuntimeContext( - engine.getSeedManager().getBodies(), - engine.getDimension().getRivers(), - data, - riverFluidHeight, - (int) Math.round(fluidHeight), - IrisEngineMantle.isRiverHydrologyEnabled(engine.getDimension()), - IrisEngineMantle.isRiverCaveHydrologyEnabled(engine.getDimension()), - blockingRiverRoutingPossible(engine), - variableMaxIncisionPossible(engine), - biomeRiverOverridesPossible(focusBiome, generatorBiomes), - (blockX, blockZ) -> naturalHeightBounds(engine, overlayNoise, blockX, blockZ), - naturalHeightStream, - naturalSlopeStream, - naturalOceanStream, - naturalTrueBiomeStream, - regionStream - )); - riverSurfaceStream = ProceduralStream.of( - (x, z) -> riverRuntime.sample(x, z), - Interpolated.of( - IrisRiverSurfaceSample::terrainHeight, - value -> IrisRiverSurfaceSample.none(value, fluidHeight) - ) - ) - .cache2D("riverSurfaceStream", engine, cacheSize); - } else { - riverSurfaceStream = naturalHeightStream.convert( - value -> IrisRiverSurfaceSample.none(value, fluidHeight) - ) - .cache2D("riverSurfaceStream-disabled", engine, cacheSize); - } - heightStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::terrainHeight) - .cache2DDouble("heightStream", engine, cacheSize); + .cache2D("trueBiomeStream", engine, cacheSize); roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) .round(); - slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) - .slope(3).cache2DDouble("slopeStream", engine, cacheSize); - trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D, - b -> focusBiome)) - .cache2D("trueBiomeStream-focus", engine, cacheSize) : riverSurfaceStream - .convertAware2D((sample, x, z) -> resolveRiverSurfaceBiome(sample, x, z)) - .cache2D("trueBiomeStream", engine, cacheSize); trueBiomeDerivativeStream = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) .convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())).cache2D("trueBiomeDerivativeStream", engine, cacheSize); - riverDistanceStream = riverSurfaceStream.convert(sample -> sample.river().present() - ? sample.river().distance() - : Double.MAX_VALUE) - .cache2DDouble("riverDistanceStream", engine, cacheSize); - riverFlowStream = riverSurfaceStream.convert(sample -> (double) sample.river().flow()) - .cache2DDouble("riverFlowStream", engine, cacheSize); - riverCarveWeightStream = riverSurfaceStream.convert(sample -> sample.river().carveWeight()) - .cache2DDouble("riverCarveWeightStream", engine, cacheSize); - riverWaterSurfaceStream = riverSurfaceStream.convert(IrisRiverSurfaceSample::waterSurfaceY) - .cache2DDouble("riverWaterSurfaceStream", engine, cacheSize); - heightFluidStream = ProceduralStream.ofDouble((x, z) -> Math.max( - heightStream.get(x, z), - riverWaterSurfaceStream.get(x, z) - )) - .cache2DDouble("heightFluidStream", engine, cacheSize); + heightFluidStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z)) + .max(fluidHeight).cache2DDouble("heightFluidStream", engine, cacheSize); maxHeightStream = ProceduralStream.ofDouble((x, z) -> height); terrainSurfaceDecoration = trueBiomeStream.contextInjecting(engine, (c, x, z) -> c.getBiome().get(x, z)) .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)).cache2D("terrainSurfaceDecoration", engine, cacheSize); @@ -424,116 +310,6 @@ public class IrisComplex implements DataProvider { //@done } - private boolean blockingRiverRoutingPossible(Engine engine) { - if (blocksRiverRouting(focusRegion == null ? null : focusRegion.getRiverOverride()) - || blocksRiverRouting(focusBiome == null ? null : focusBiome.getRiverOverride())) { - return true; - } - KList regions = engine.getDimension().getAllRegions(engine); - for (IrisRegion loadedRegion : regions) { - if (loadedRegion != null && blocksRiverRouting(loadedRegion.getRiverOverride())) { - return true; - } - } - KList biomes = engine.getDimension().getReachableBiomes(engine); - for (IrisBiome biome : biomes) { - if (biome != null && blocksRiverRouting(biome.getRiverOverride())) { - return true; - } - } - return false; - } - - private static boolean blocksRiverRouting(IrisRiverOverride override) { - return override != null && override.getRoutingPolicy() == IrisRiverRoutingPolicy.BLOCK; - } - - private boolean variableMaxIncisionPossible(Engine engine) { - if (changesMaxIncision(focusRegion == null ? null : focusRegion.getRiverOverride()) - || changesMaxIncision(focusBiome == null ? null : focusBiome.getRiverOverride())) { - return true; - } - KList regions = engine.getDimension().getAllRegions(engine); - for (IrisRegion loadedRegion : regions) { - if (loadedRegion != null && changesMaxIncision(loadedRegion.getRiverOverride())) { - return true; - } - } - KList biomes = engine.getDimension().getReachableBiomes(engine); - for (IrisBiome biome : biomes) { - if (biome != null && changesMaxIncision(biome.getRiverOverride())) { - return true; - } - } - return false; - } - - static boolean changesMaxIncision(IrisRiverOverride override) { - if (override == null || override.getMaxIncisionMultiplier() == null) { - return false; - } - double multiplier = override.getMaxIncisionMultiplier(); - return Double.isFinite(multiplier) && Double.compare(Math.max(0D, multiplier), 1D) != 0; - } - - static boolean biomeRiverOverridesPossible(IrisBiome focusBiome, Iterable naturalBiomes) { - if (focusBiome != null) { - return focusBiome.getRiverOverride() != null; - } - for (IrisBiome biome : naturalBiomes) { - if (biome != null && biome.getRiverOverride() != null) { - return true; - } - } - return false; - } - - static ProceduralStream createNaturalOceanStream( - ProceduralStream bridgeStream, - IrisBiome focusBiome - ) { - if (focusBiome != null) { - boolean ocean = focusBiome.getInferredType() == InferredType.SEA; - return ProceduralStream.of((x, z) -> ocean, Interpolated.BOOLEAN); - } - return bridgeStream.convert(type -> type == InferredType.SEA); - } - - private ProceduralStream configuredFluidStream( - IrisMaterialPalette palette, - RNG fluidRng, - String configurationName - ) { - Objects.requireNonNull(palette, configurationName + " fluidPalette must be configured"); - KList blocks = palette.getBlockData(data); - if (blocks.isEmpty()) { - throw new IllegalArgumentException( - configurationName + " fluidPalette must resolve at least one fluid block"); - } - for (PlatformBlockState block : blocks) { - if (block == null || !block.isFluid()) { - throw new IllegalArgumentException( - configurationName + " fluidPalette may contain only fluid blocks"); - } - } - return palette.getLayerGenerator(fluidRng, data).stream().select(blocks); - } - - public PlatformBlockState resolveRiverCaveFluid(RiverCaveFluidKind fluidKind, double x, double z) { - return switch (Objects.requireNonNull(fluidKind)) { - case RIVER -> riverFluidStream.get(x, z); - case DEEP_POOL -> riverDeepPoolFluidStream.get(x, z); - }; - } - - public PlatformBlockState resolveSurfaceFluid(double x, double z) { - IrisRiverSurfaceSample sample = riverSurfaceStream.get(x, z); - if (sample.river().present() && sample.surfaceFluid()) { - return riverFluidStream.get(x, z); - } - return fluidStream.get(x, z); - } - public ProceduralStream getBiomeStream(InferredType type) { switch (type) { case CAVE: @@ -555,7 +331,7 @@ public class IrisComplex implements DataProvider { if (engine == null) { throw new IllegalArgumentException("Engine is required to sample procedural terrain height"); } - return getHeight(engine, null, worldX, worldZ, engine.getSeedManager().getHeight()); + return getHeight(engine, worldX, worldZ, engine.getSeedManager().getHeight()); } private IrisRegion findRegion(IrisBiome focus, Engine engine) { @@ -594,67 +370,6 @@ public class IrisComplex implements DataProvider { return null; } - private IrisBiome resolveRiverSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) { - IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); - if (mapped != null && (riverRuntime == null || !sample.river().present())) { - return mapped; - } - if (riverRuntime != null) { - if (sample.subterranean()) { - return fixBiomeType( - sample.naturalHeight(), - baseBiomeStream.get(x, z), - regionStream.get(x, z), - x, - z, - fluidHeight - ); - } - IrisBiome riverBiome = riverRuntime.selectSurfaceBiome(sample, x, z); - if (riverBiome != null) { - return implode(riverBiome, x, z); - } - InferredType directFallback = directRiverFallback(sample.river()); - if (directFallback != null) { - IrisBiome baseBiome = baseBiomeStream.get(x, z); - return implode(baseBiome.withInferredType(directFallback), x, z); - } - if (sample.river().present() - && sample.river().state() == RiverRouteState.WET - && sample.river().section() == RiverSection.BANK) { - return fixBiomeType( - sample.terrainHeight(), - baseBiomeStream.get(x, z), - regionStream.get(x, z), - x, - z, - sample.waterSurfaceY() - ); - } - } - return fixBiomeType( - sample.terrainHeight(), - baseBiomeStream.get(x, z), - regionStream.get(x, z), - x, - z, - fluidHeight - ); - } - - static InferredType directRiverFallback(RiverSample river) { - if (!river.present()) { - return null; - } - if (river.state() == RiverRouteState.DRY) { - return InferredType.LAND; - } - return switch (river.section()) { - case CHANNEL, MOUTH -> InferredType.SEA; - default -> null; - }; - } - private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) { IrisBiome resolved = resolveSurfaceBiome( height, @@ -713,13 +428,7 @@ public class IrisComplex implements DataProvider { double hi = sampledBounds.max(); double lo = sampledBounds.min(); - double d = 0; - - for (IrisGenerator i : generators) { - d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945)); - } - - return d / generators.length; + return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945); } private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) { @@ -742,6 +451,22 @@ public class IrisComplex implements DataProvider { return new NoiseBounds(boundsLow(b00), boundsHigh(b00)); } + if (fz == 0D) { + long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz); + return new NoiseBounds( + biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz), + biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz) + ); + } + + if (fx == 0D) { + long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid); + return new NoiseBounds( + biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz), + biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz) + ); + } + long b10 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz); long b01 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz + grid); long b11 = cornerBounds(cache, engine, interpolator, interpolatorIndex, generators, gx + grid, gz + grid); @@ -803,47 +528,94 @@ public class IrisComplex implements DataProvider { return h; } - private NoiseBounds naturalHeightBounds( - Engine engine, - KList overlayNoise, - double x, - double z - ) { - double minimum = fluidHeight; - double maximum = fluidHeight; - for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) { - NoiseBounds bounds = gridSampleBounds( - engine, - frozenInterpolators[interpolatorIndex], - interpolatorIndex, - frozenGenerators[interpolatorIndex], - x, - z - ); - minimum += Math.min(bounds.min(), bounds.max()); - maximum += Math.max(bounds.min(), bounds.max()); - } - for (IrisShapedGeneratorStyle style : overlayNoise) { - minimum += Math.min(style.getMin(), style.getMax()); - maximum += Math.max(style.getMin(), style.getMax()); - } - minimum = imageMapRuntime.sampleTerrainHeight(x, z, minimum); - maximum = imageMapRuntime.sampleTerrainHeight(x, z, maximum); - return new NoiseBounds( - Math.max(0D, Math.min(engine.getHeight(), Math.min(minimum, maximum))), - Math.max(0D, Math.min(engine.getHeight(), Math.max(minimum, maximum))) - ); - } - - private double getHeight(Engine engine, IrisBiome b, double x, double z, long seed) { + private double getHeight(Engine engine, double x, double z, long seed) { return Math.max(Math.min(getInterpolatedHeight(engine, x, z, seed) + fluidHeight + overlayStream.get(x, z), engine.getHeight()), 0); } - private void prepareInferredBiomes(IrisRegion region) { + private void prepareInferredBiomes(IrisRegion region, KList preparedRegions) { loadInferredBiomes(region.getLandBiomes(), InferredType.LAND); loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE); loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA); loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE); + preparedRegions.add(region); + } + + private ProceduralStream createInferredBiomeStream( + IrisRegion region, + InferredType inferredType + ) { + return preparedInferredBiomeStream(inferredBiomeStreams, region, inferredType); + } + + private ProceduralStream compileInferredBiomeStream( + Engine engine, + IrisRegion region, + InferredType inferredType, + IrisBiome emptyBiome + ) { + return switch (inferredType) { + case CAVE -> engine.getDimension().getCaveBiomeStyle() + .create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(region.getCaveBiomeZoom()) + .selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE)) + .onNull(emptyBiome); + case LAND -> engine.getDimension().getLandBiomeStyle() + .create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(engine.getDimension().getLandZoom()) + .zoom(region.getLandBiomeZoom()) + .selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND)); + case SEA -> engine.getDimension().getSeaBiomeStyle() + .create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(engine.getDimension().getSeaZoom()) + .zoom(region.getSeaBiomeZoom()) + .selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA)); + case SHORE -> engine.getDimension().getShoreBiomeStyle() + .create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() + .zoom(engine.getDimension().getBiomeZoom()) + .zoom(region.getShoreBiomeZoom()) + .selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE)); + }; + } + + static Map>> compileInferredBiomeStreams( + Iterable regions, + BiFunction> compiler + ) { + IdentityHashMap>> compiled = new IdentityHashMap<>(); + for (IrisRegion region : regions) { + if (compiled.containsKey(region)) { + continue; + } + EnumMap> regionStreams = new EnumMap<>(InferredType.class); + for (InferredType inferredType : INFERRED_BIOME_PREPARATION_ORDER) { + regionStreams.put(inferredType, Objects.requireNonNull(compiler.apply(region, inferredType))); + } + compiled.put(region, Collections.unmodifiableMap(regionStreams)); + } + return Collections.unmodifiableMap(compiled); + } + + static ProceduralStream preparedInferredBiomeStream( + Map>> streams, + IrisRegion region, + InferredType inferredType + ) { + Map> regionStreams = streams.get(region); + if (regionStreams == null) { + String regionKey = region == null || region.getLoadKey() == null || region.getLoadKey().isBlank() + ? "" + : region.getLoadKey(); + throw new IllegalStateException("Inferred-biome streams were not prepared for region '" + + regionKey + "'."); + } + ProceduralStream stream = regionStreams.get(inferredType); + if (stream == null) { + throw new IllegalStateException("Inferred-biome stream was not prepared for type " + inferredType + "."); + } + return stream; } private KList loadInferredBiomes(KList keys, InferredType type) { @@ -863,6 +635,37 @@ public class IrisComplex implements DataProvider { generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator); } + static GeneratorGroup[] freezeGeneratorGroups(Map> generators) { + GeneratorGroup[] groups = new GeneratorGroup[generators.size()]; + int groupIndex = 0; + for (Map.Entry> entry : generators.entrySet()) { + IrisGenerator[] groupGenerators = entry.getValue().toArray(new IrisGenerator[0]); + Arrays.sort(groupGenerators, GENERATOR_ORDER); + groups[groupIndex] = new GeneratorGroup(entry.getKey(), groupGenerators); + groupIndex++; + } + Arrays.sort(groups, Comparator.comparing(GeneratorGroup::interpolator, INTERPOLATOR_ORDER)); + return groups; + } + + static double averageGeneratorHeights( + IrisGenerator[] generators, + double low, + double high, + double x, + double z, + long seed + ) { + if (generators.length == 0) { + return 0D; + } + double height = 0D; + for (IrisGenerator generator : generators) { + height += M.lerp(low, high, generator.getHeight(x, z, seed)); + } + return height / generators.length; + } + private Map> buildGeneratorBounds(Engine engine) { Map> bounds = new HashMap<>(); KList allBiomes = new KList<>(generatorBiomes); @@ -1265,9 +1068,10 @@ public class IrisComplex implements DataProvider { } } - public void close() { - if (riverRuntime != null) { - riverRuntime.close(); - } + record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) { } + + public void close() { + } + } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java index ad3197840..6e26dc4bf 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java @@ -30,9 +30,7 @@ import art.arcane.iris.engine.mantle.MantlePass; import art.arcane.iris.engine.mantle.components.MantleCarvingComponent; import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent; import art.arcane.iris.engine.mantle.components.MantleObjectComponent; -import art.arcane.iris.engine.mantle.components.MantleRiverHydrologyComponent; import art.arcane.iris.engine.mantle.components.IrisStructureComponent; -import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.matter.IrisMatterContext; @@ -90,7 +88,6 @@ public class IrisEngineMantle implements EngineMantle { this.mantle = createMantle(engine); components = new KMap<>(); registerComponent(new MantleCarvingComponent(this)); - registerComponent(new MantleRiverHydrologyComponent(this)); object = new MantleObjectComponent(this); registerComponent(object); registerComponent(new MantleFloatingObjectComponent(this)); @@ -179,22 +176,10 @@ public class IrisEngineMantle implements EngineMantle { if (!getDimension().isCarvingEnabled()) { disabled.addIfMissing(ReservedFlag.CARVED); } - if (disabled.contains(ReservedFlag.CARVED) - || !isRiverHydrologyEnabled(getDimension())) { - disabled.addIfMissing(ReservedFlag.RIVER_HYDROLOGY); - } return Set.copyOf(disabled); }); } - static boolean isRiverHydrologyEnabled(IrisDimension dimension) { - return MantleRiverHydrologyComponent.isEnabledFor(dimension); - } - - static boolean isRiverCaveHydrologyEnabled(IrisDimension dimension) { - return MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension); - } - @Override public MantleObjectComponent getObjectComponent() { return object; diff --git a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java index c05446024..f19a54c99 100644 --- a/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java +++ b/core/src/main/java/art/arcane/iris/engine/UpperDimensionContext.java @@ -14,7 +14,6 @@ import art.arcane.iris.engine.object.IrisShapedGeneratorStyle; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.DataProvider; -import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.interpolation.NoiseBounds; import art.arcane.iris.spi.PlatformBlockState; @@ -67,8 +66,8 @@ public class UpperDimensionContext implements DataProvider { engine.getDimension(), engine.getData(), chunkHeight, - complex.getNaturalHeightStream(), - complex.getNaturalTrueBiomeStream(), + complex.getHeightStream(), + complex.getTrueBiomeStream(), complex.getRegionStream(), complex.getRockStream(), complex.getImageMapRuntime(), @@ -100,25 +99,26 @@ public class UpperDimensionContext implements DataProvider { upperDim.getRegions().forEach(regionKey -> { IrisRegion region = upperData.getRegionLoader().load(regionKey); if (region != null) { - region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators( + region.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators( biome, dataProvider, allBiomes, generators)); } }); for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) { - mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators( + mappedRegion.getAllBiomes(dataProvider).forEach(biome -> registerBiomeGenerators( biome, dataProvider, allBiomes, generators)); } for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) { registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators); } + IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators); Map> generatorBounds = new HashMap<>(); - for (Map.Entry> entry : generators.entrySet()) { + for (IrisComplex.GeneratorGroup group : generatorGroups) { IdentityHashMap interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16)); for (IrisBiome biome : allBiomes) { double min = 0D; double max = 0D; - for (IrisGenerator gen : entry.getValue()) { + for (IrisGenerator gen : group.generators()) { String key = gen.getLoadKey(); if (key == null || key.isBlank()) { continue; @@ -128,7 +128,7 @@ public class UpperDimensionContext implements DataProvider { } interpolatorBounds.put(biome, new NoiseBounds(min, max)); } - generatorBounds.put(entry.getKey(), interpolatorBounds); + generatorBounds.put(group.interpolator(), interpolatorBounds); } ProceduralStream regionStyleStream = upperDim.getRegionStyle() @@ -204,10 +204,10 @@ public class UpperDimensionContext implements DataProvider { return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z); } double interpolatedHeight = 0; - for (Map.Entry> entry : generators.entrySet()) { - IrisInterpolator interpolator = entry.getKey(); - Set gens = entry.getValue(); - if (gens.isEmpty()) { + for (IrisComplex.GeneratorGroup group : generatorGroups) { + IrisInterpolator interpolator = group.interpolator(); + IrisGenerator[] groupGenerators = group.generators(); + if (groupGenerators.length == 0) { continue; } IdentityHashMap cachedBounds = generatorBounds.get(interpolator); @@ -223,7 +223,7 @@ public class UpperDimensionContext implements DataProvider { } double bMin = 0D; double bMax = 0D; - for (IrisGenerator gen : gens) { + for (IrisGenerator gen : groupGenerators) { String key = gen.getLoadKey(); if (key == null || key.isBlank()) { continue; @@ -239,11 +239,14 @@ public class UpperDimensionContext implements DataProvider { }); double hi = sampledBounds.max(); double lo = sampledBounds.min(); - double d = 0; - for (IrisGenerator gen : gens) { - d += M.lerp(lo, hi, gen.getHeight(x, z, heightSeed + 239945)); - } - interpolatedHeight += d / gens.size(); + interpolatedHeight += IrisComplex.averageGeneratorHeights( + groupGenerators, + lo, + hi, + x, + z, + heightSeed + 239945 + ); } double proceduralHeight = Math.max( Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight), diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java index e7f1dae4d..15b3ed91b 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisDecorantActuator.java @@ -27,8 +27,6 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.framework.EngineDecorator; import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; import art.arcane.iris.util.common.data.B; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.BlockCoordinates; @@ -66,12 +64,6 @@ public class IrisDecorantActuator extends EngineAssignedActuator output, boolean multicore, ChunkContext context) { @@ -94,25 +86,23 @@ public class IrisDecorantActuator extends EngineAssignedActuator biomeCache = context.getBiome(); ChunkedDataCache regionCache = context.getRegion(); + ChunkedDataCache fluidCache = context.getFluid(); ChunkedDataCache rockCache = context.getRock(); int realX = xf + x; UpperDimensionContext upperContext = getEngine().getUpperContext(); @@ -107,17 +110,13 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators(); diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java index 2dfd9b2ad..d49f384e1 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisShoreLineDecorator.java @@ -40,17 +40,16 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator { @Override public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, int height, int max) { - double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ); - if (height != Math.round(localFluidHeight)) { + if (height != getDimension().getFluidHeight()) { return; } + double complexFluidHeight = getComplex().getFluidHeight(); ProceduralStream heightStream = getComplex().getHeightStream(); - ProceduralStream fluidStream = getComplex().getRiverWaterSurfaceStream(); - if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ)) - && Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ)) - && Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1)) - && Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) { + if (Math.round(heightStream.get(realX1, realZ)) >= complexFluidHeight + && Math.round(heightStream.get(realX_1, realZ)) >= complexFluidHeight + && Math.round(heightStream.get(realX, realZ1)) >= complexFluidHeight + && Math.round(heightStream.get(realX, realZ_1)) >= complexFluidHeight) { return; } diff --git a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java index cf743a475..225567af6 100644 --- a/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java +++ b/core/src/main/java/art/arcane/iris/engine/decorator/IrisSurfaceDecorator.java @@ -58,7 +58,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator { @BlockCoordinates public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk data, IrisBiome biome, InferredType inferredType, int height, int max) { - int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ)); + int fluidHeight = getDimension().getFluidHeight(); if (inferredType == InferredType.SHORE && height < fluidHeight) { return; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index e77b35d36..6f2adc84f 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -42,8 +42,6 @@ import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisStructure; import art.arcane.iris.engine.object.IrisWorld; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBlockState; @@ -246,14 +244,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, @BlockCoordinates default IrisBiome getCaveOrMantleBiome(int x, int y, int z) { - RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent( - getMantle().getMantle(), x, y, z); - if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) { - IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey()); - if (biome != null) { - return biome; - } - } MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class); if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java index ed44a5490..153da99d4 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlanner.java @@ -157,10 +157,7 @@ public final class NativeStructurePlacementPlanner { } static boolean isSubmerged(Engine engine, int blockX, int blockZ) { - int localFluidHeight = engine.getComplex() == null - ? engine.getDimension().getFluidHeight() - : (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ)); - return engine.getHeight(blockX, blockZ, true) < localFluidHeight; + return engine.getHeight(blockX, blockZ, true) < engine.getDimension().getFluidHeight(); } private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java b/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java index 2aba47f07..2c120b114 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolver.java @@ -3,8 +3,6 @@ package art.arcane.iris.engine.framework; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisStructureAnchorMode; import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; @@ -288,12 +286,10 @@ public final class StructureCaveAnchorResolver { int mantleY, int blockZ ) { - RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ); MatterCavern cavern = cavernAt(engine, blockX, mantleY, blockZ); return acceptsAnchorFluid( placement.isUnderwater(), cavern, - hydrology, mantleY, engine.getDimension().getCaveLavaHeight()); } @@ -304,19 +300,6 @@ public final class StructureCaveAnchorResolver { int mantleY, int defaultLavaHeight ) { - return acceptsAnchorFluid(underwater, cavern, null, mantleY, defaultLavaHeight); - } - - static boolean acceptsAnchorFluid( - boolean underwater, - MatterCavern cavern, - RiverCaveHydrology hydrology, - int mantleY, - int defaultLavaHeight - ) { - if (hydrology != null && hydrology.protectsPlacement()) { - return false; - } if (cavern == null || !cavern.isCavern()) { return false; } @@ -330,15 +313,7 @@ public final class StructureCaveAnchorResolver { } private static MatterCavern cavernAt(Engine engine, int blockX, int mantleY, int blockZ) { - MatterCavern baseline = engine.getMantle().getMantle() - .get(blockX, mantleY, blockZ, MatterCavern.class); - RiverCaveHydrology hydrology = hydrologyAt(engine, blockX, mantleY, blockZ); - return hydrology == null ? baseline : hydrology.asCavern(); - } - - private static RiverCaveHydrology hydrologyAt(Engine engine, int blockX, int mantleY, int blockZ) { - return RiverCaveHydrologyStorage.getIfPresent( - engine.getMantle().getMantle(), blockX, mantleY, blockZ); + return engine.getMantle().getMantle().get(blockX, mantleY, blockZ, MatterCavern.class); } static int toMantleY(int worldY, int worldMinHeight) { diff --git a/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java index f49a0a676..0d706e139 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/placer/WorldObjectPlacer.java @@ -21,6 +21,7 @@ import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.IrisCustomData; import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.MatterCavern; import lombok.EqualsAndHashCode; import lombok.Getter; import org.bukkit.Bukkit; @@ -111,7 +112,7 @@ public class WorldObjectPlacer implements IObjectPlacer { @Override public boolean isCarved(int x, int y, int z) { - return mantle.isCarved(x, y, z); + return mantle.getMantle().get(x, y, z, MatterCavern.class) != null; } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java b/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java index efad60ace..346626c5c 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/IrisRenderer.java @@ -24,9 +24,6 @@ import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeGeneratorLink; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.runtime.IrisRiverRuntime; import art.arcane.iris.util.project.stream.ProceduralStream; import java.awt.Color; @@ -43,12 +40,6 @@ public final class IrisRenderer { private static final int BLUE = new Color(45, 91, 156).getRGB(); private static final int YELLOW = new Color(211, 164, 67).getRGB(); private static final int GREEN = new Color(78, 137, 83).getRGB(); - private static final int RIVER_CHANNEL = new Color(48, 112, 190).getRGB(); - private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB(); - private static final int RIVER_BANK = new Color(92, 146, 78).getRGB(); - private static final int DRY_CHANNEL = new Color(171, 128, 68).getRGB(); - private static final int DRY_BANK = new Color(132, 105, 62).getRGB(); - private static final int NO_RIVER = new Color(28, 31, 38).getRGB(); private static final int DEEP_WATER = new Color(20, 48, 92).getRGB(); private static final int SHALLOW_WATER = new Color(50, 112, 154).getRGB(); private static final int LOWLAND = new Color(78, 128, 76).getRGB(); @@ -114,17 +105,9 @@ public final class IrisRenderer { renderHeightAtlas(pixels, resolution, sx, sz, step, renderer, cancelled); return image; } - PixelShader shader = shader(currentType, step, studio); - if (studio && currentType == RenderType.RIVER) { - Arrays.fill(pixels, NO_RIVER); - renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, false); - return image; - } + PixelShader shader = shader(currentType, studio); if (studio && adaptiveStudioType(currentType)) { renderAdaptiveAtlas(pixels, resolution, sx, sz, step, shader, cancelled); - if (currentType == RenderType.BIOME) { - renderRiverAtlas(pixels, resolution, sx, sz, step, renderer.getComplex(), cancelled, true); - } return image; } int groupSize = sampleGroup(step, resolution); @@ -150,18 +133,6 @@ public final class IrisRenderer { return image; } - public static int riverColor(RiverSection section) { - Objects.requireNonNull(section, "section"); - return switch (section) { - case CHANNEL -> RIVER_CHANNEL; - case MOUTH -> RIVER_MOUTH; - case BANK -> RIVER_BANK; - case DRY_CHANNEL -> DRY_CHANNEL; - case DRY_BANK -> DRY_BANK; - case NONE -> NO_RIVER; - }; - } - public static int heightColor(double height, double maximumHeight, double fluidHeight) { double boundedMaximum = Math.max(1D, maximumHeight); double boundedHeight = clamp(height, 0D, boundedMaximum); @@ -189,7 +160,7 @@ public final class IrisRenderer { return Math.max(1, Math.min(resolution, (int) Math.floor(16D / absoluteStep))); } - private PixelShader shader(RenderType currentType, double step, boolean studio) { + private PixelShader shader(RenderType currentType, boolean studio) { IrisComplex complex = renderer.getComplex(); return switch (currentType) { case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD -> biomeShader( @@ -198,8 +169,7 @@ public final class IrisRenderer { case BIOME_SEA -> biomeShader(complex.getSeaBiomeStream(), currentType); case REGION -> regionShader(complex, currentType); case CAVE_LAND -> biomeShader(complex.getCaveBiomeStream(), currentType); - case HEIGHT -> heightShader(studio ? complex.getNaturalHeightStream() : complex.getHeightStream()); - case RIVER -> (double x, double z) -> riverColor(complex, x, z, step); + case HEIGHT -> heightShader(complex.getHeightStream()); case CONTINENT -> studio ? continentShader(complex.getBaseBiomeStream()) : this::continentColor; @@ -210,7 +180,7 @@ public final class IrisRenderer { return switch (type) { case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, CONTINENT -> true; - case HEIGHT, RIVER -> false; + case HEIGHT -> false; }; } @@ -254,7 +224,7 @@ public final class IrisRenderer { startX, startZ, step, - complex.getNaturalHeightStream(), + complex.getHeightStream(), engine.getHeight(), fluidHeight, cancelled @@ -267,103 +237,6 @@ public final class IrisRenderer { } } - private static void renderRiverAtlas( - int[] pixels, - int resolution, - double startX, - double startZ, - double step, - IrisComplex complex, - BooleanSupplier cancelled, - boolean composite - ) { - IrisRiverRuntime runtime = complex.getRiverRuntime(); - if (runtime == null) { - return; - } - int maximumPixels = Math.max(1, Math.min(16, (int) Math.floor(64D / step))); - int blockPixels = Integer.highestOneBit(maximumPixels); - for (int pixelZ = 0; pixelZ < resolution; pixelZ += blockPixels) { - int height = Math.min(blockPixels, resolution - pixelZ); - for (int pixelX = 0; pixelX < resolution; pixelX += blockPixels) { - renderRiverBlock( - pixels, - resolution, - startX, - startZ, - step, - runtime, - cancelled, - composite, - pixelX, - pixelZ, - Math.min(blockPixels, resolution - pixelX), - height - ); - } - } - } - - private static void renderRiverBlock( - int[] pixels, - int resolution, - double startX, - double startZ, - double step, - IrisRiverRuntime runtime, - BooleanSupplier cancelled, - boolean composite, - int pixelX, - int pixelZ, - int width, - int height - ) { - checkCancelled(cancelled); - RiverSample sample = runtime.sampleFootprint( - startX + pixelX * step, - startZ + pixelZ * step, - startX + (pixelX + width) * step, - startZ + (pixelZ + height) * step - ); - if (!sample.present()) { - return; - } - if (width == 1 && height == 1) { - int index = pixelZ * resolution + pixelX; - int color = riverColor(sample.section()); - pixels[index] = composite ? riverCompositeColor(pixels[index], sample.section(), color) : color; - return; - } - int leftWidth = Math.max(1, width / 2); - int rightWidth = width - leftWidth; - int topHeight = Math.max(1, height / 2); - int bottomHeight = height - topHeight; - renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, - pixelX, pixelZ, leftWidth, topHeight); - if (rightWidth > 0) { - renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, - pixelX + leftWidth, pixelZ, rightWidth, topHeight); - } - if (bottomHeight > 0) { - renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, - pixelX, pixelZ + topHeight, leftWidth, bottomHeight); - if (rightWidth > 0) { - renderRiverBlock(pixels, resolution, startX, startZ, step, runtime, cancelled, composite, - pixelX + leftWidth, pixelZ + topHeight, rightWidth, bottomHeight); - } - } - } - - private static int riverCompositeColor(int base, RiverSection section, int river) { - return switch (section) { - case CHANNEL, MOUTH -> river; - case BANK -> blend(base, river, 0.68D); - case DRY_CHANNEL -> blend(base, river, 0.88D); - case DRY_BANK -> blend(base, river, 0.58D); - case NONE -> base; - }; - } - private PixelShader biomeShader(ProceduralStream stream, RenderType currentType) { IdentityHashMap colors = new IdentityHashMap<>(); return (double x, double z) -> { @@ -398,22 +271,6 @@ public final class IrisRenderer { return (double x, double z) -> heightColor(stream.getDouble(x, z), maximumHeight, fluidHeight); } - private int riverColor(IrisComplex complex, double x, double z, double step) { - IrisRiverRuntime runtime = complex.getRiverRuntime(); - if (runtime == null) { - return riverColor(RiverSection.NONE); - } - double endX = x + step; - double endZ = z + step; - RiverSample sample = runtime.sampleFootprint( - StrictMath.min(x, endX), - StrictMath.min(z, endZ), - StrictMath.max(x, endX), - StrictMath.max(z, endZ) - ); - return riverColor(sample.section()); - } - private int continentColor(double x, double z) { IrisBiome biome = renderer.getBiome( (int) Math.round(x), diff --git a/core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java b/core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java index 50b79ce47..2ec4a17d1 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/render/RenderType.java @@ -19,5 +19,5 @@ package art.arcane.iris.engine.framework.render; public enum RenderType { - BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, RIVER, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD + BIOME, BIOME_LAND, BIOME_SEA, REGION, CAVE_LAND, HEIGHT, OBJECT_LOAD, DECORATOR_LOAD, CONTINENT, LAYER_LOAD } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java index 489a867a8..348ef617e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/EngineMantle.java @@ -27,8 +27,6 @@ import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.TreeBlockMaterial; import art.arcane.iris.engine.mantle.components.MantleObjectComponent; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.engine.river.cave.RiverCaveHydrologyStorage; import art.arcane.iris.engine.object.IrisPosition; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.common.data.B; @@ -104,7 +102,7 @@ public interface EngineMantle extends MatterGenerator { } default int getHighest(int x, int z, IrisData data, boolean ignoreFluid) { - return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getFluidHeight(x, z)); + return ignoreFluid ? trueHeight(x, z) : Math.max(trueHeight(x, z), getEngine().getDimension().getFluidHeight()); } default int trueHeight(int x, int z) { @@ -112,10 +110,6 @@ public interface EngineMantle extends MatterGenerator { } default boolean isCarved(int x, int h, int z) { - RiverCaveHydrology hydrology = RiverCaveHydrologyStorage.getIfPresent(getMantle(), x, h, z); - if (hydrology != null) { - return hydrology.carves(); - } return getMantle().get(x, h, z, MatterCavern.class) != null; } @@ -131,17 +125,13 @@ public interface EngineMantle extends MatterGenerator { } default boolean isUnderwater(int x, int z) { - return getHighest(x, z, true) < getFluidHeight(x, z); + return getHighest(x, z, true) <= getFluidHeight(); } default int getFluidHeight() { return getEngine().getDimension().getFluidHeight(); } - default int getFluidHeight(int x, int z) { - return (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z)); - } - default boolean isDebugSmartBore() { return getEngine().getDimension().isDebugSmartBore(); } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 43cd97fae..557b9b2d1 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -31,7 +31,6 @@ import art.arcane.iris.engine.object.IObjectPlacer; import art.arcane.iris.engine.object.IrisGeneratorStyle; import art.arcane.iris.engine.object.IrisPosition; import art.arcane.iris.engine.object.TileData; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.function.Function3; @@ -207,10 +206,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { if (chunk == null) return; Matter matter = chunk.getOrCreate(y >> 4); - if ((t instanceof PlatformBlockState || t instanceof MatterCavern) - && hasProtectedHydrology(matter, x, y, z)) { - return; - } if (t instanceof PlatformBlockState) { clearDeferredPlacement(matter, x, y, z); } @@ -236,9 +231,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { } Matter matter = chunk.getOrCreate(y >> 4); - if (hasProtectedHydrology(matter, x, y, z)) { - return false; - } MatterCavern existing = matter.slice(MatterCavern.class).get(x & 15, y & 15, z & 15); if (existing != null) { return false; @@ -259,9 +251,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { } Matter matter = chunk.getOrCreate(y >> 4); - if (hasProtectedHydrology(matter, x, y, z)) { - return false; - } if (matter.hasSlice(PlatformBlockState.class)) { matter.getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null); } @@ -284,9 +273,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { + x + "," + y + "," + z); } Matter matter = chunk.getOrCreate(y >> 4); - if (hasProtectedHydrology(matter, x, y, z)) { - return; - } if (matter.hasSlice(PlatformBlockState.class)) { matter.getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null); } @@ -310,9 +296,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { if (matter == null) { return; } - if (hasProtectedHydrology(matter, x, y, z)) { - return; - } if (matter.hasSlice(PlatformBlockState.class)) { matter.getSlice(PlatformBlockState.class).set(x & 15, y & 15, z & 15, null); } @@ -370,22 +353,9 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { if (matter == null || !matter.hasSlice(type)) { return; } - if ((type == PlatformBlockState.class || type == MatterCavern.class) - && hasProtectedHydrology(matter, x, y, z)) { - return; - } matter.getSlice(type).set(x & 15, y & 15, z & 15, null); } - private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) { - if (!matter.hasSlice(RiverCaveHydrology.class)) { - return false; - } - RiverCaveHydrology hydrology = matter.getSlice(RiverCaveHydrology.class) - .get(x & 15, y & 15, z & 15); - return hydrology != null && hydrology.protectsPlacement(); - } - private static void clearDeferredPlacement(Matter matter, int x, int y, int z) { if (matter.hasSlice(Identifier.class)) { matter.getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null); @@ -480,10 +450,6 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { @Override public boolean isCarved(int x, int y, int z) { - RiverCaveHydrology hydrology = getDataIfPresent(x, y, z, RiverCaveHydrology.class); - if (hydrology != null) { - return hydrology.carves(); - } return getDataIfPresent(x, y, z, MatterCavern.class) != null; } @@ -508,28 +474,14 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { } Matter matter = chunk.get(section); - if (matter == null) { - continue; - } - - MatterSlice cavernSlice = matter.hasSlice(MatterCavern.class) - ? matter.getSlice(MatterCavern.class) - : null; - MatterSlice hydrologySlice = matter.hasSlice(RiverCaveHydrology.class) - ? matter.getSlice(RiverCaveHydrology.class) - : null; - if (cavernSlice == null && hydrologySlice == null) { + if (matter == null || !matter.hasSlice(MatterCavern.class)) { continue; } + MatterSlice slice = matter.getSlice(MatterCavern.class); int sectionBaseY = section << 4; int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16); for (int y = sectionBaseY; y < sectionMaxY; y++) { - RiverCaveHydrology hydrology = hydrologySlice == null - ? null - : hydrologySlice.get(localX, y & 15, localZ); - if (hydrology != null) { - carvedColumn[y] = hydrology.carves() ? (byte) 1 : 0; - } else if (cavernSlice != null && cavernSlice.get(localX, y & 15, localZ) != null) { + if (slice.get(localX, y & 15, localZ) != null) { carvedColumn[y] = 1; } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java index e6d8addad..a9e964f2e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CarveOrphanSweep.java @@ -61,14 +61,15 @@ public final class CarveOrphanSweep { int[] surfaceHeights, int maxSurfaceBreakDepth, int worldCeilingY, - long[] surfaceFluidBoundaries + int[] surfaceFluidBoundaryStartY, + int fluidHeight ) { if (chunk == null) { return 0; } return sweep(surfaceHeights, maxSurfaceBreakDepth, 0, worldCeilingY, - new MantleCarveAccess(chunk, surfaceFluidBoundaries)); + new MantleCarveAccess(chunk, surfaceFluidBoundaryStartY, fluidHeight)); } public static int sweep(int[] surfaceHeights, int maxSurfaceBreakDepth, int worldFloorY, int worldCeilingY, CarveAccess access) { @@ -228,13 +229,15 @@ public final class CarveOrphanSweep { private static final class MantleCarveAccess implements CarveAccess { private final MantleChunk chunk; - private final long[] surfaceFluidBoundaries; + private final int[] surfaceFluidBoundaryStartY; + private final int fluidHeight; private MatterSlice cachedSlice; private int cachedSectionIndex = -1; - private MantleCarveAccess(MantleChunk chunk, long[] surfaceFluidBoundaries) { + private MantleCarveAccess(MantleChunk chunk, int[] surfaceFluidBoundaryStartY, int fluidHeight) { this.chunk = chunk; - this.surfaceFluidBoundaries = surfaceFluidBoundaries; + this.surfaceFluidBoundaryStartY = surfaceFluidBoundaryStartY; + this.fluidHeight = fluidHeight; } @Override @@ -258,7 +261,7 @@ public final class CarveOrphanSweep { @Override public boolean isProtected(int localX, int y, int localZ) { int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); - return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y); + return SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java index 9c3755002..fd838eb6d 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransaction.java @@ -23,7 +23,6 @@ import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IObjectPlacer; import art.arcane.iris.engine.object.TileData; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.collection.KList; @@ -72,12 +71,6 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { discard(); return CommitResult.REJECTED_BOUNDS; } - RiverCaveHydrology hydrology = delegate.getData( - mutation.x(), mutation.y(), mutation.z(), RiverCaveHydrology.class); - if (hydrology != null && hydrology.protectsPlacement()) { - discard(); - return CommitResult.REJECTED_HYDROLOGY; - } } for (BufferedMutation mutation : mutations) { @@ -252,8 +245,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { enum CommitResult { COMMITTED, EMPTY, - REJECTED_BOUNDS, - REJECTED_HYDROLOGY + REJECTED_BOUNDS } private interface BufferedMutation { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/ConfiguredRiverGrottoShape.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/ConfiguredRiverGrottoShape.java deleted file mode 100644 index 70c765349..000000000 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/ConfiguredRiverGrottoShape.java +++ /dev/null @@ -1,69 +0,0 @@ -package art.arcane.iris.engine.mantle.components; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.IrisGeneratorStyle; -import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.iris.engine.river.cave.RiverCaveGrottoShape; -import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings; -import art.arcane.iris.engine.river.cave.RiverCaveSource; -import art.arcane.iris.util.project.noise.CNG; -import art.arcane.volmlib.util.math.RNG; - -final class ConfiguredRiverGrottoShape implements RiverCaveGrottoShape { - private static final long SHAPE_SALT = 0x3C6EF372FE94F82BL; - private static final long WARP_X_SALT = 0xA54FF53A5F1D36F1L; - private static final long WARP_Y_SALT = 0x510E527FADE682D1L; - private static final long WARP_Z_SALT = 0x9B05688C2B3E6C1FL; - - private final CNG shape; - private final CNG warpX; - private final CNG warpY; - private final CNG warpZ; - private final double warpStrength; - private final double boundaryVariation; - - ConfiguredRiverGrottoShape( - long seed, - IrisData data, - IrisGeneratorStyle shapeStyle, - IrisGeneratorStyle warpStyle, - double warpStrength, - double boundaryVariation - ) { - IrisGeneratorStyle resolvedShape = shapeStyle == null - ? new IrisGeneratorStyle(NoiseStyle.FLAT) - : shapeStyle; - IrisGeneratorStyle resolvedWarp = warpStyle == null - ? new IrisGeneratorStyle(NoiseStyle.FLAT) - : warpStyle; - shape = resolvedShape.createNoCache(new RNG(seed ^ SHAPE_SALT), data); - warpX = resolvedWarp.createNoCache(new RNG(seed ^ WARP_X_SALT), data); - warpY = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Y_SALT), data); - warpZ = resolvedWarp.createNoCache(new RNG(seed ^ WARP_Z_SALT), data); - this.warpStrength = Math.max(0D, warpStrength); - this.boundaryVariation = Math.max(0D, Math.min(0.75D, boundaryVariation)); - } - - @Override - public boolean contains( - RiverCaveSource source, - RiverCavePlannerSettings settings, - int offsetX, - int offsetY, - int offsetZ - ) { - double worldX = source.target().x() + offsetX; - double worldY = source.target().y() + offsetY; - double worldZ = source.target().z() + offsetZ; - double warpedX = offsetX + warpX.fitDouble(-warpStrength, warpStrength, worldX, worldY, worldZ); - double warpedY = offsetY + warpY.fitDouble(-warpStrength, warpStrength, worldY, worldZ, worldX); - double warpedZ = offsetZ + warpZ.fitDouble(-warpStrength, warpStrength, worldZ, worldX, worldY); - double horizontalRadius = settings.grottoHorizontalRadius(); - double verticalRadius = settings.grottoVerticalRadius(); - double normalized = (warpedX * warpedX / (horizontalRadius * horizontalRadius)) - + (warpedY * warpedY / (verticalRadius * verticalRadius)) - + (warpedZ * warpedZ / (horizontalRadius * horizontalRadius)); - double boundary = shape.fitDouble(-boundaryVariation, boundaryVariation, worldX, worldY, worldZ); - return normalized <= 1D + boundary; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 880c9a432..d67e1d452 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -199,7 +199,7 @@ public class IrisCaveCarver3D { double thresholdPenalty, IrisRange worldYRange, int[] precomputedSurfaceHeights, - long[] surfaceFluidBoundaries, + int[] surfaceFluidBoundaryStartY, IrisRange overrideVerticalRange, CaveFluidSupportPlan fluidSupportPlan ) { @@ -324,7 +324,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, - surfaceFluidBoundaries, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -347,7 +347,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, - surfaceFluidBoundaries, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -373,7 +373,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, - surfaceFluidBoundaries, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -397,7 +397,7 @@ public class IrisCaveCarver3D { surfaceBreakThresholdBoost, columnMaxY, fluidMaxY, - surfaceFluidBoundaries, + surfaceFluidBoundaryStartY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, @@ -428,7 +428,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, - long[] surfaceFluidBoundaries, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -484,7 +484,7 @@ public class IrisCaveCarver3D { } int columnIndex = activeColumnIndices[activeIndex]; - if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) { + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) { continue; } planeColumnIndices[planeCount] = columnIndex; @@ -560,7 +560,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, - long[] surfaceFluidBoundaries, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -622,7 +622,7 @@ public class IrisCaveCarver3D { } int columnIndex = activeColumnIndices[activeIndex]; - if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) { + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, columnIndex, y, fluidHeight)) { continue; } planeColumnIndices[planeCount] = columnIndex; @@ -720,7 +720,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, - long[] surfaceFluidBoundaries, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -822,7 +822,7 @@ public class IrisCaveCarver3D { } int index = tileIndices[columnIndex]; - if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) { + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) { continue; } double localThreshold = passThreshold[index]; @@ -870,7 +870,7 @@ public class IrisCaveCarver3D { double surfaceBreakThresholdBoost, int[] columnMaxY, int[] fluidMaxY, - long[] surfaceFluidBoundaries, + int[] surfaceFluidBoundaryStartY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, @@ -909,7 +909,7 @@ public class IrisCaveCarver3D { double density = sampleDensityOptimized(scratch, x, y, z); int carveMaxY = Math.min(columnTopY, y + sampleStep - 1); for (int yy = y; yy <= carveMaxY; yy++) { - if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, index, yy)) { + if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaryStartY, index, yy, fluidHeight)) { continue; } double localThreshold = threshold; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java index b497ede03..8764bf127 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -70,7 +70,7 @@ public class IrisStructureComponent extends IrisMantleComponent { private static final MatterCavern CARVE_CAVERN = new MatterCavern(true, "", (byte) 3); public IrisStructureComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.JIGSAW, 4); + super(engineMantle, ReservedFlag.JIGSAW, 3); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 44ab58f22..3afd69d85 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -104,19 +104,20 @@ public class MantleCarvingComponent extends IrisMantleComponent { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); List weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); - long[] surfaceFluidBoundaries = blendScratch.surfaceFluidBoundaries; + int fluidHeight = getDimension().getFluidHeight(); + int[] surfaceFluidBoundaryStartY = blendScratch.surfaceFluidBoundaryStartY; SurfaceFluidBoundaryPlan.fill( chunkSurfaceHeights, blendScratch.fieldSurfaceHeights, blendScratch.fieldHasFluid, - blendScratch.fieldFluidHeights, FIELD_SIZE, BLEND_RADIUS, - surfaceFluidBoundaries + fluidHeight, + surfaceFluidBoundaryStartY ); CaveFluidSupportPlan fluidSupportPlan = new CaveFluidSupportPlan(); for (WeightedProfile weightedProfile : weightedProfiles) { - carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaries, fluidSupportPlan); + carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights, surfaceFluidBoundaryStartY, fluidSupportPlan); } UpperDimensionContext upperCtx = getEngineMantle().getEngine().getUpperContext(); @@ -131,7 +132,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { chunkSurfaceHeights, maxSurfaceBreakDepth(weightedProfiles), writer.getMantle().getWorldHeight() - 1, - surfaceFluidBoundaries + surfaceFluidBoundaryStartY, + fluidHeight ); } } @@ -146,11 +148,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { @ChunkCoordinates private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, - int[] chunkSurfaceHeights, long[] surfaceFluidBoundaries, + int[] chunkSurfaceHeights, int[] surfaceFluidBoundaryStartY, CaveFluidSupportPlan fluidSupportPlan) { IrisCaveCarver3D carver = getCarver(weightedProfile.profile); carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, - weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaries, null, fluidSupportPlan); + weightedProfile.worldYRange, chunkSurfaceHeights, surfaceFluidBoundaryStartY, null, fluidSupportPlan); } private void carveUpperTerrain(UpperDimensionContext upperCtx, List normalProfiles, @@ -473,9 +475,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { fillFieldHeights(complex.getHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights); - fillFieldHeights(complex.getRiverWaterSurfaceStream(), startX, startZ, blendScratch.fieldFluidHeights); - fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights, - blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid); + fillFieldFluidPresence(complex.getFluidStream(), startX, startZ, blendScratch.fieldHasFluid); fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions); fillFieldObjects(complex.getTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes); fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes); @@ -499,20 +499,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { } } - private void fillFieldFluidPresence( - IrisComplex complex, - int startX, - int startZ, - double[] surfaceHeights, - double[] fluidHeights, - boolean[] target - ) { + private void fillFieldFluidPresence(ProceduralStream stream, int startX, int startZ, boolean[] target) { for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { int worldX = startX + fieldX; for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { - int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ; - target[fieldIndex] = B.isFluid(complex.resolveSurfaceFluid(worldX, startZ + fieldZ)) - && Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]); + target[(fieldX * FIELD_SIZE) + fieldZ] = B.isFluid(stream.get(worldX, startZ + fieldZ)); } } } @@ -706,13 +697,12 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); private final List profileOrder = new ArrayList<>(); private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; - private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE]; private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE]; - private final long[] surfaceFluidBoundaries = new long[CHUNK_AREA]; private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE]; private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; + private final int[] surfaceFluidBoundaryStartY = new int[CHUNK_AREA]; private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA]; } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java index c1f611cc4..c351b8928 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleFloatingObjectComponent.java @@ -59,7 +59,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0); public MantleFloatingObjectComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.FLOATING_OBJECT, 3); + super(engineMantle, ReservedFlag.FLOATING_OBJECT, 2); } @Override diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index 58da33f51..9e070482e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -48,7 +48,6 @@ import art.arcane.iris.engine.object.IrisProceduralPlacement; import art.arcane.iris.engine.object.IrisProceduralTree; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.ObjectPlaceMode; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.spi.IrisLogging; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -84,7 +83,7 @@ public class MantleObjectComponent extends IrisMantleComponent { private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); public MantleObjectComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.OBJECT, 2); + super(engineMantle, ReservedFlag.OBJECT, 1); } private static String placementMarker(IrisObject object, int id, String context) { @@ -577,18 +576,11 @@ public class MantleObjectComponent extends IrisMantleComponent { minDepthBelowSurface, anchorCache ); - RiverCaveHydrology hydrology = candidateY < 0 - ? null - : writer.getDataIfPresent(candidateX, candidateY, candidateZ, RiverCaveHydrology.class); - MatterCavern cavern = candidateY < 0 - ? null - : writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class); if (candidateY < 0 || caveAnchorBiomeConflicts(candidateX, candidateY, candidateZ, expectedCaveBiomeKey) || !acceptsCaveAnchorFluid( underwater, - hydrology == null ? cavern : hydrology.asCavern(), - hydrology, + writer.getDataIfPresent(candidateX, candidateY, candidateZ, MatterCavern.class), candidateY, getDimension().getCaveLavaHeight())) { continue; @@ -599,19 +591,6 @@ public class MantleObjectComponent extends IrisMantleComponent { } static boolean acceptsCaveAnchorFluid(boolean underwater, MatterCavern cavern, int y, int lavaHeight) { - return acceptsCaveAnchorFluid(underwater, cavern, null, y, lavaHeight); - } - - static boolean acceptsCaveAnchorFluid( - boolean underwater, - MatterCavern cavern, - RiverCaveHydrology hydrology, - int y, - int lavaHeight - ) { - if (hydrology != null && hydrology.protectsPlacement()) { - return false; - } if (cavern == null || !cavern.isCavern()) { return false; } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleRiverCaveVoxelView.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleRiverCaveVoxelView.java deleted file mode 100644 index 68ffda007..000000000 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleRiverCaveVoxelView.java +++ /dev/null @@ -1,174 +0,0 @@ -package art.arcane.iris.engine.mantle.components; - -import art.arcane.iris.engine.river.cave.CavePosition; -import art.arcane.iris.engine.river.cave.CaveVoxel; -import art.arcane.iris.engine.river.cave.CaveVoxelView; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.engine.data.cache.Cache; -import art.arcane.iris.engine.object.IrisProceduralBlocks; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.volmlib.util.function.Function2; -import art.arcane.volmlib.util.mantle.runtime.Mantle; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; -import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; -import art.arcane.volmlib.util.matter.Matter; -import art.arcane.volmlib.util.matter.MatterCavern; -import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; - -import java.util.Objects; -import java.util.function.BiConsumer; - -final class MantleRiverCaveVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView { - private static final int CLOSED_COLUMN = Integer.MAX_VALUE; - private static final int CACHE_MISS = Integer.MIN_VALUE; - - private final Mantle mantle; - private final int worldHeight; - private final Function2 surfaceHeight; - private final Function2 compatibleFluid; - private final RiverCaveFluidKind planningFluidKind; - private final BiConsumer chunkLoader; - private final LongOpenHashSet loadedChunks; - private final Long2IntOpenHashMap openFloorCache; - private final Long2IntOpenHashMap surfaceHeightCache; - - MantleRiverCaveVoxelView( - Mantle mantle, - int worldHeight, - Function2 surfaceHeight, - Function2 compatibleFluid, - RiverCaveFluidKind planningFluidKind, - BiConsumer chunkLoader - ) { - this.mantle = Objects.requireNonNull(mantle); - this.worldHeight = worldHeight; - this.surfaceHeight = Objects.requireNonNull(surfaceHeight); - this.compatibleFluid = Objects.requireNonNull(compatibleFluid); - this.planningFluidKind = Objects.requireNonNull(planningFluidKind); - this.chunkLoader = Objects.requireNonNull(chunkLoader); - loadedChunks = new LongOpenHashSet(); - openFloorCache = new Long2IntOpenHashMap(); - openFloorCache.defaultReturnValue(CACHE_MISS); - surfaceHeightCache = new Long2IntOpenHashMap(); - surfaceHeightCache.defaultReturnValue(CACHE_MISS); - } - - @Override - public boolean isInWorld(CavePosition position) { - return position.y() > 0 && position.y() < worldHeight - 1; - } - - @Override - public CaveVoxel voxelAt(CavePosition position) { - RiverCaveHydrology hydrology = dataIfPresent(position, RiverCaveHydrology.class); - if (hydrology != null && hydrology.fluidKind() != planningFluidKind) { - return CaveVoxel.INCOMPATIBLE_FLUID; - } - MatterCavern cavern = dataIfPresent(position, MatterCavern.class); - if (cavern != null) { - if (cavern.isLava()) { - PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z()); - return expected != null - && IrisProceduralBlocks.materialKey(expected).endsWith(":lava") - ? CaveVoxel.COMPATIBLE_FLUID - : CaveVoxel.LAVA; - } - if (cavern.getLiquid() == 1) { - return CaveVoxel.COMPATIBLE_FLUID; - } - return CaveVoxel.CAVE_AIR; - } - PlatformBlockState block = dataIfPresent(position, PlatformBlockState.class); - if (block == null) { - return position.y() > surfaceY(position.x(), position.z()) - ? CaveVoxel.CAVE_AIR - : CaveVoxel.SOLID; - } - if (!block.isFluid()) { - return CaveVoxel.SOLID; - } - PlatformBlockState expected = compatibleFluid.apply(position.x(), position.z()); - if (expected != null - && IrisProceduralBlocks.materialKey(expected).equals(IrisProceduralBlocks.materialKey(block))) { - return CaveVoxel.COMPATIBLE_FLUID; - } - return IrisProceduralBlocks.materialKey(block).endsWith(":lava") - ? CaveVoxel.LAVA - : CaveVoxel.INCOMPATIBLE_FLUID; - } - - @Override - public boolean isOpenToSurface(CavePosition position) { - if (!isInWorld(position) || voxelAt(position) == CaveVoxel.SOLID) { - return false; - } - if (position.y() > surfaceY(position.x(), position.z())) { - return true; - } - long key = Cache.key(position.x(), position.z()); - int openFloor = openFloorCache.get(key); - if (openFloor == CACHE_MISS) { - openFloor = resolveOpenFloor(position.x(), position.z()); - openFloorCache.put(key, openFloor); - } - return openFloor != CLOSED_COLUMN && position.y() >= openFloor; - } - - @Override - public RiverCaveHydrology riverHydrologyAt(CavePosition position) { - return dataIfPresent(position, RiverCaveHydrology.class); - } - - private int resolveOpenFloor(int x, int z) { - int top = surfaceY(x, z); - CavePosition surface = new CavePosition(x, top, z); - if (voxelAt(surface) == CaveVoxel.SOLID) { - return CLOSED_COLUMN; - } - int y = top; - while (y > 0 && voxelAt(new CavePosition(x, y - 1, z)) != CaveVoxel.SOLID) { - y--; - } - return y; - } - - private int surfaceY(int x, int z) { - long key = Cache.key(x, z); - int cached = surfaceHeightCache.get(key); - if (cached != CACHE_MISS) { - return cached; - } - int resolved = Math.max(1, Math.min(worldHeight - 2, surfaceHeight.apply(x, z))); - surfaceHeightCache.put(key, resolved); - return resolved; - } - - private T dataIfPresent(CavePosition position, Class type) { - int chunkX = position.x() >> 4; - int chunkZ = position.z() >> 4; - long chunkKey = Mantle.key(chunkX, chunkZ); - if (loadedChunks.add(chunkKey)) { - chunkLoader.accept(chunkX, chunkZ); - } - TectonicPlate plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5)); - if (plate == null || plate.isClosed()) { - return null; - } - MantleChunk chunk = plate.get(chunkX & 31, chunkZ & 31); - int section = position.y() >> 4; - if (chunk == null || !chunk.exists(section)) { - return null; - } - Matter matter = chunk.get(section); - if (matter == null || !matter.hasSlice(type)) { - return null; - } - return matter.getSlice(type).get( - position.x() & 15, - position.y() & 15, - position.z() & 15 - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleRiverHydrologyComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleRiverHydrologyComponent.java deleted file mode 100644 index b85b60a44..000000000 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleRiverHydrologyComponent.java +++ /dev/null @@ -1,1206 +0,0 @@ -package art.arcane.iris.engine.mantle.components; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.data.cache.Cache; -import art.arcane.iris.engine.mantle.ComponentFlag; -import art.arcane.iris.engine.mantle.EngineMantle; -import art.arcane.iris.engine.mantle.IrisMantleComponent; -import art.arcane.iris.engine.mantle.MantleComponent; -import art.arcane.iris.engine.mantle.MantleWriter; -import art.arcane.iris.engine.object.IrisRiverCaveFallback; -import art.arcane.iris.engine.object.IrisRiverCaveMode; -import art.arcane.iris.engine.object.IrisRiverCaves; -import art.arcane.iris.engine.object.IrisRiverDeepPools; -import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.river.RiverAnchor; -import art.arcane.iris.engine.river.RiverNetwork; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.RiverTopologyComplexity; -import art.arcane.iris.engine.river.cave.CavePosition; -import art.arcane.iris.engine.river.cave.CaveVoxel; -import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition; -import art.arcane.iris.engine.river.cave.CaveVoxelView; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.engine.river.cave.RiverCaveMode; -import art.arcane.iris.engine.river.cave.RiverCavePlan; -import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings; -import art.arcane.iris.engine.river.cave.RiverCavePlanningResult; -import art.arcane.iris.engine.river.cave.RiverCaveSource; -import art.arcane.iris.engine.river.runtime.IrisRiverRuntime; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; -import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample; -import art.arcane.iris.util.project.context.ChunkContext; -import art.arcane.volmlib.util.mantle.flag.MantleFlag; -import art.arcane.volmlib.util.mantle.flag.ReservedFlag; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; -import art.arcane.volmlib.util.math.BlockPosition; -import art.arcane.volmlib.util.matter.Matter; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@ComponentFlag(ReservedFlag.RIVER_HYDROLOGY) -public final class MantleRiverHydrologyComponent extends IrisMantleComponent { - private static final long CANDIDATE_SALT = 0x6A09E667F3BCC909L; - private static final long DEEP_POOL_CANDIDATE_SALT = 0xBB67AE8584CAA73BL; - private static final long DEEP_POOL_POSITION_SALT = 0x3C6EF372FE94F82BL; - static final int PRIORITY = 1; - private static final int[] FALLBACK_X = {0, 1, -1, 0, 0}; - private static final int[] FALLBACK_Z = {0, 0, 0, 1, -1}; - private static final MantleFlag[] PREREQUISITES = {ReservedFlag.CARVED}; - private static final int[][] NEIGHBORS = { - {1, 0, 0}, {-1, 0, 0}, - {0, 1, 0}, {0, -1, 0}, - {0, 0, 1}, {0, 0, -1} - }; - private static final Comparator> ACTION_ORDER = Comparator - .comparingInt((Map.Entry entry) -> entry.getKey().x()) - .thenComparingInt(entry -> entry.getKey().y()) - .thenComparingInt(entry -> entry.getKey().z()); - - private final RiverCaveContainmentPlanner planner; - - public MantleRiverHydrologyComponent(EngineMantle engineMantle) { - super(engineMantle, ReservedFlag.RIVER_HYDROLOGY, PRIORITY); - planner = new RiverCaveContainmentPlanner(); - } - - @Override - public MantleFlag[] getPrerequisiteFlags() { - return PREREQUISITES; - } - - @Override - public boolean isInputGenerationLazy() { - return true; - } - - @Override - public int getInputRadius() { - if (!getDimension().isCarvingEnabled() - || getDimension().getRivers() == null - || !getDimension().getRivers().isEnabled()) { - return 0; - } - IrisRiverRuntime runtime = getComplex().getRiverRuntime(); - if (runtime == null) { - return 0; - } - return inputRadius(runtime.caveSettings(), tunnelHalo(runtime)); - } - - @Override - public int getInputRadius( - int targetChunkX, - int targetChunkZ, - int invocationChunkRadius, - ChunkContext context - ) { - if (!getDimension().isCarvingEnabled() - || getDimension().getRivers() == null - || !getDimension().getRivers().isEnabled()) { - return 0; - } - if (context == null || context.getComplex() == null) { - return getInputRadius(); - } - IrisRiverRuntime runtime = context.getComplex().getRiverRuntime(); - if (runtime == null) { - return getInputRadius(); - } - IrisRiverCaves caves = runtime.caveSettings(); - int tunnelRadius = tunnelHalo(runtime); - int radius = hasRiverFootprint( - runtime, - targetChunkX, - targetChunkZ, - invocationChunkRadius, - tunnelRadius - ) ? tunnelRadius : 0; - if (caves.getMode() != IrisRiverCaveMode.SEALED - && caves.getMaximumPerReach() > 0) { - radius = Math.max(radius, hasAcceptedCaveAnchor( - runtime, - caves, - targetChunkX, - targetChunkZ, - invocationChunkRadius - ) ? planningHalo(caves) : 0); - } - IrisRiverDeepPools deepPools = caves.getDeepPools(); - if (deepPools != null - && deepPools.isEnabled() - && deepPools.getMaximumPerReach() > 0) { - radius = Math.max(radius, hasAcceptedDeepPoolAnchor( - runtime, - deepPools, - targetChunkX, - targetChunkZ, - invocationChunkRadius - ) ? deepPoolPlanningHalo(deepPools) : 0); - } - return radius; - } - - private static boolean hasRiverFootprint( - IrisRiverRuntime runtime, - int targetChunkX, - int targetChunkZ, - int invocationChunkRadius, - int tunnelRadius - ) { - return runtime.hasRiverFootprint( - ((targetChunkX - invocationChunkRadius) << 4) - tunnelRadius, - ((targetChunkZ - invocationChunkRadius) << 4) - tunnelRadius, - ((targetChunkX + invocationChunkRadius + 1) << 4) + tunnelRadius, - ((targetChunkZ + invocationChunkRadius + 1) << 4) + tunnelRadius - ); - } - - private static boolean hasAcceptedCaveAnchor( - IrisRiverRuntime runtime, - IrisRiverCaves caves, - int targetChunkX, - int targetChunkZ, - int invocationChunkRadius - ) { - int candidateHalo = candidateHalo(caves); - List anchors = runtime.candidateAnchors( - ((targetChunkX - invocationChunkRadius) << 4) - candidateHalo, - ((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo, - ((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo, - ((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo, - caves.getMinimumSpacing(), - CANDIDATE_SALT - ); - for (RiverAnchor anchor : anchors) { - if (runtime.acceptsCaveAnchor(anchor)) { - return true; - } - } - return false; - } - - private static boolean hasAcceptedDeepPoolAnchor( - IrisRiverRuntime runtime, - IrisRiverDeepPools deepPools, - int targetChunkX, - int targetChunkZ, - int invocationChunkRadius - ) { - int candidateHalo = deepPoolCandidateHalo(deepPools); - List anchors = runtime.candidateAnchors( - ((targetChunkX - invocationChunkRadius) << 4) - candidateHalo, - ((targetChunkZ - invocationChunkRadius) << 4) - candidateHalo, - ((targetChunkX + invocationChunkRadius + 1) << 4) + candidateHalo, - ((targetChunkZ + invocationChunkRadius + 1) << 4) + candidateHalo, - deepPools.getMinimumSpacing(), - DEEP_POOL_CANDIDATE_SALT - ); - for (RiverAnchor anchor : anchors) { - if (runtime.acceptsDeepPoolAnchor(anchor)) { - return true; - } - } - return false; - } - - @Override - public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) { - IrisRiverRuntime runtime = context.getComplex().getRiverRuntime(); - if (runtime == null || !getDimension().isCarvingEnabled()) { - return; - } - publishTunnels(writer, context, runtime, chunkX, chunkZ); - - IrisRiverCaves caves = runtime.caveSettings(); - if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) { - publishCaveConnections(writer, context, runtime, caves, chunkX, chunkZ); - } - publishDeepPools(writer, context, runtime, caves.getDeepPools(), chunkX, chunkZ); - } - - private void publishCaveConnections( - MantleWriter writer, - ChunkContext context, - IrisRiverRuntime runtime, - IrisRiverCaves caves, - int chunkX, - int chunkZ - ) { - MantleRiverCaveVoxelView view = createView(writer, context, RiverCaveFluidKind.RIVER); - int candidateHalo = candidateHalo(caves); - int minimumX = (chunkX << 4) - candidateHalo; - int minimumZ = (chunkZ << 4) - candidateHalo; - int maximumX = ((chunkX + 1) << 4) + candidateHalo; - int maximumZ = ((chunkZ + 1) << 4) + candidateHalo; - List anchors = runtime.candidateAnchors( - minimumX, - minimumZ, - maximumX, - maximumZ, - caves.getMinimumSpacing(), - CANDIDATE_SALT - ); - if (anchors.isEmpty()) { - return; - } - - RiverCavePlannerSettings settings = plannerSettings(caves, seed(), getData()); - List sources = new ArrayList<>(); - Map floodedBiomes = new HashMap<>(); - for (RiverAnchor anchor : anchors) { - if (!runtime.acceptsCaveAnchor(anchor)) { - continue; - } - SourceCandidate candidate = sourceFor(runtime, view, caves, anchor); - if (candidate == null) { - continue; - } - RiverCaveSource source = candidate.source(); - RiverCavePlan initial = planner.plan(view, source, settings); - if (!initial.accepted() - && caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) { - source = fallbackSource(view, caves, settings, candidate, source); - } - if (source == null) { - continue; - } - sources.add(source); - floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor)); - } - if (sources.isEmpty()) { - return; - } - - RiverCavePlanningResult result = planner.planAll(view, sources, settings); - MantleRiverCaveVoxelView revalidationView = createView( - writer, - context, - RiverCaveFluidKind.RIVER - ); - if (!preconditionsHold(revalidationView, result.baselinePreconditions())) { - return; - } - publishLocal( - writer, - chunkX, - chunkZ, - result, - floodedBiomes, - RiverCaveFluidKind.RIVER - ); - } - - private void publishDeepPools( - MantleWriter writer, - ChunkContext context, - IrisRiverRuntime runtime, - IrisRiverDeepPools deepPools, - int chunkX, - int chunkZ - ) { - if (deepPools == null || !deepPools.isEnabled() || deepPools.getMaximumPerReach() <= 0) { - return; - } - MantleRiverCaveVoxelView view = createView( - writer, - context, - RiverCaveFluidKind.DEEP_POOL - ); - int candidateHalo = deepPoolCandidateHalo(deepPools); - int minimumX = (chunkX << 4) - candidateHalo; - int minimumZ = (chunkZ << 4) - candidateHalo; - int maximumX = ((chunkX + 1) << 4) + candidateHalo; - int maximumZ = ((chunkZ + 1) << 4) + candidateHalo; - List anchors = runtime.candidateAnchors( - minimumX, - minimumZ, - maximumX, - maximumZ, - deepPools.getMinimumSpacing(), - DEEP_POOL_CANDIDATE_SALT - ); - if (anchors.isEmpty()) { - return; - } - - RiverCavePlannerSettings settings = deepPoolPlannerSettings(deepPools, seed(), getData()); - List sources = new ArrayList<>(); - Map floodedBiomes = new HashMap<>(); - for (RiverAnchor anchor : anchors) { - if (!runtime.acceptsDeepPoolAnchor(anchor)) { - continue; - } - RiverCaveSource source = deepPoolSourceFor( - view, - deepPools, - anchor, - getDimension().getMinHeight(), - seed() - ); - if (source == null) { - continue; - } - sources.add(source); - floodedBiomes.put(source.sourceId(), runtime.selectFloodedCaveBiome(anchor)); - } - if (sources.isEmpty()) { - return; - } - - RiverCavePlanningResult result = planner.planAll(view, sources, settings); - MantleRiverCaveVoxelView revalidationView = createView( - writer, - context, - RiverCaveFluidKind.DEEP_POOL - ); - if (!preconditionsHold(revalidationView, result.baselinePreconditions())) { - return; - } - publishLocal( - writer, - chunkX, - chunkZ, - result, - floodedBiomes, - RiverCaveFluidKind.DEEP_POOL - ); - } - - static RiverCavePlannerSettings deepPoolPlannerSettings( - IrisRiverDeepPools deepPools, - long seed, - IrisData data - ) { - int verticalDepth = deepPools.getVerticalRadius() * 2 - deepPools.getDryHeadroom() + 1; - int proofRadius = (int) StrictMath.ceil( - StrictMath.sqrt(2D) * deepPools.getHorizontalRadius() - ) + 1; - return new RiverCavePlannerSettings( - proofRadius, - verticalDepth, - deepPools.getMaximumVolume(), - deepPools.getVerticalRadius(), - 1, - deepPools.getHorizontalRadius(), - deepPools.getVerticalRadius(), - deepPools.getDryHeadroom(), - RiverCaveFluidPolicy.REJECT_EXISTING, - new ConfiguredRiverGrottoShape( - seed ^ DEEP_POOL_POSITION_SALT, - data, - deepPools.getShapeStyle(), - deepPools.getWarpStyle(), - deepPools.getWarpStrength(), - deepPools.getShapeVariation() - ), - proofRadius, - verticalDepth - ); - } - - static RiverCaveSource deepPoolSourceFor( - CaveVoxelView view, - IrisRiverDeepPools deepPools, - RiverAnchor anchor, - int worldMinimumY, - long seed - ) { - int minimumHead = deepPools.getMinimumFluidY() - worldMinimumY; - int maximumHead = deepPools.getMaximumFluidY() - worldMinimumY; - int headRange = maximumHead - minimumHead + 1; - if (headRange <= 0) { - return null; - } - int searchRadius = deepPools.getSearchRadius(); - int searchWidth = searchRadius * 2 + 1; - int targetDepth = Math.max( - 1, - deepPools.getVerticalRadius() - deepPools.getDryHeadroom() - ); - for (int attempt = 0; attempt < deepPools.getSearchAttempts(); attempt++) { - long hash = RiverNetwork.mix( - seed - ^ anchor.stableId() - ^ DEEP_POOL_POSITION_SALT - ^ (long) attempt * 0x9E3779B97F4A7C15L - ); - int offsetX = searchRadius == 0 - ? 0 - : Math.floorMod((int) hash, searchWidth) - searchRadius; - int offsetZ = searchRadius == 0 - ? 0 - : Math.floorMod((int) (hash >>> 32), searchWidth) - searchRadius; - if ((long) offsetX * offsetX + (long) offsetZ * offsetZ - > (long) searchRadius * searchRadius) { - continue; - } - int x = (int) StrictMath.floor(anchor.x()) + offsetX; - int z = (int) StrictMath.floor(anchor.z()) + offsetZ; - int startOffset = (int) StrictMath.floor(unit(hash) * headRange); - for (int scanned = 0; scanned < headRange; scanned++) { - int headY = maximumHead - Math.floorMod(startOffset + scanned, headRange); - CavePosition floor = new CavePosition(x, headY, z); - CavePosition above = new CavePosition(x, headY + 1, z); - CavePosition target = new CavePosition(x, headY - targetDepth, z); - if (!view.isInWorld(target) - || !view.isInWorld(above) - || view.voxelAt(floor) != CaveVoxel.SOLID - || view.voxelAt(above) != CaveVoxel.CAVE_AIR - || view.isOpenToSurface(above)) { - continue; - } - long sourceId = RiverNetwork.mix( - anchor.stableId() - ^ BlockPosition.toLong(x, headY, z) - ^ DEEP_POOL_POSITION_SALT - ); - return new RiverCaveSource( - sourceId, - floor, - target, - headY, - RiverCaveMode.DEEP_POOL - ); - } - } - return null; - } - - private static double unit(long hash) { - return (hash >>> 11) * 0x1.0p-53; - } - - @Override - protected int computeRadius() { - return 0; - } - - public static boolean isEnabledFor(IrisDimension dimension) { - IrisRiverNetwork rivers = dimension.getRivers(); - if (!dimension.isUseMantle() - || !dimension.isCarvingEnabled() - || dimension.getDisabledComponents().contains(ReservedFlag.CARVED) - || dimension.getDisabledComponents().contains(ReservedFlag.RIVER_HYDROLOGY) - || rivers == null - || !rivers.isEnabled()) { - return false; - } - return true; - } - - public static boolean isCaveConnectionsEnabledFor(IrisDimension dimension) { - if (!isEnabledFor(dimension)) { - return false; - } - IrisRiverCaves caves = dimension.getRivers().getCaves(); - if (caves == null) { - return false; - } - boolean caveConnections = caves.getMode() != IrisRiverCaveMode.SEALED - && caves.getMaximumPerReach() > 0; - IrisRiverDeepPools deepPools = caves.getDeepPools(); - return caveConnections || deepPools != null - && deepPools.isEnabled() - && deepPools.getMaximumPerReach() > 0; - } - - static int planningHalo(IrisRiverCaves caves) { - return cavePublicationRadius(caves) * 4; - } - - static int inputRadius(IrisRiverCaves caves, int tunnelRadius) { - int radius = tunnelRadius; - if (caves.getMode() != IrisRiverCaveMode.SEALED && caves.getMaximumPerReach() > 0) { - radius = Math.max(radius, planningHalo(caves)); - } - IrisRiverDeepPools deepPools = caves.getDeepPools(); - if (deepPools != null && deepPools.isEnabled() && deepPools.getMaximumPerReach() > 0) { - radius = Math.max(radius, deepPoolPlanningHalo(deepPools)); - } - return radius; - } - - static int candidateHalo(IrisRiverCaves caves) { - return cavePublicationRadius(caves) * 3; - } - - static int deepPoolPlanningHalo(IrisRiverDeepPools deepPools) { - return deepPoolPublicationRadius(deepPools) * 4; - } - - static int deepPoolCandidateHalo(IrisRiverDeepPools deepPools) { - return deepPoolPublicationRadius(deepPools) * 3; - } - - static int deepPoolPublicationRadius(IrisRiverDeepPools deepPools) { - return deepPools.getSearchRadius() + deepPools.getHorizontalRadius() + 1; - } - - static int cavePublicationRadius(IrisRiverCaves caves) { - int generatedRadius = generatedGrottoPublicationRadius(caves); - return switch (caves.getMode()) { - case SEALED -> 0; - case GENERATE_GROTTO -> generatedRadius; - case FLOOD_CLOSED_COMPONENT, GROTTO_OR_CLOSED_COMPONENT, WATERFALL_POOL -> - Math.max(closedComponentPublicationRadius(caves), generatedRadius); - }; - } - - static int closedComponentPublicationRadius(IrisRiverCaves caves) { - return caves.getMaxFloodRadius() + 1; - } - - static int generatedGrottoPublicationRadius(IrisRiverCaves caves) { - int targetOffset = caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO - ? caves.getThroatRadius() + 2 - : 0; - long maximumX = (long) targetOffset + caves.getGrottoHorizontalRadius() + 1L; - long maximumZ = caves.getGrottoHorizontalRadius(); - int grottoRadius = (int) StrictMath.ceil(StrictMath.sqrt( - maximumX * maximumX + maximumZ * maximumZ - )); - int throatRadius = targetOffset + caves.getThroatRadius(); - return Math.max(grottoRadius, throatRadius); - } - - static int tunnelHalo(IrisRiverRuntime runtime) { - return RiverTopologyComplexity.tunnelHalo( - runtime.maximumChannelWidth(), - runtime.maximumTunnelWidthMultiplier(), - runtime.tunnelMouthBlend() - ); - } - - static int waterHeadY(IrisRiverSurfaceSample sample, IrisRiverCaves caves) { - return (int) Math.round(sample.waterSurfaceY()) + caves.getWaterLevelOffset(); - } - - static boolean owns(int chunkX, int chunkZ, CavePosition position) { - return (position.x() >> 4) == chunkX && (position.z() >> 4) == chunkZ; - } - - static RiverCaveFluidPolicy fluidPolicy(IrisRiverExistingFluidPolicy policy) { - return switch (policy) { - case REJECT -> RiverCaveFluidPolicy.REJECT_EXISTING; - case ALLOW_SAME -> RiverCaveFluidPolicy.ALLOW_COMPATIBLE; - case REPLACE -> RiverCaveFluidPolicy.REPLACE_CONTAINED; - }; - } - - static boolean preconditionsHold( - CaveVoxelView view, - Map preconditions - ) { - for (Map.Entry entry : preconditions.entrySet()) { - CaveVoxelPrecondition expected = entry.getValue(); - if (view.voxelAt(entry.getKey()) != expected.voxel() - || view.isOpenToSurface(entry.getKey()) != expected.openToSurface()) { - return false; - } - } - return true; - } - - static TunnelPlan planTunnels( - CaveVoxelView view, - int chunkX, - int chunkZ, - int halo, - int dryHeadroom, - FootprintSampler footprintSampler, - TunnelSampler tunnelSampler, - SurfaceSampler surfaceSampler - ) { - int minimumX = (chunkX << 4) - halo; - int minimumZ = (chunkZ << 4) - halo; - int maximumX = ((chunkX + 1) << 4) + halo; - int maximumZ = ((chunkZ + 1) << 4) + halo; - if (!footprintSampler.sample(minimumX, minimumZ, maximumX, maximumZ).present()) { - return TunnelPlan.empty(); - } - ArrayList solidColumns = new ArrayList<>(); - for (int x = minimumX; x < maximumX; x++) { - for (int z = minimumZ; z < maximumZ; z++) { - IrisRiverTunnelSample sample = tunnelSampler.sample(x, z); - TunnelColumn column = createTunnelColumn(view, x, z, sample); - if (column != null) { - solidColumns.add(column); - } - } - } - if (solidColumns.isEmpty()) { - return TunnelPlan.empty(); - } - - ArrayList containedColumns = new ArrayList<>(solidColumns); - boolean changed; - do { - changed = false; - Long2ObjectOpenHashMap candidateColumns = indexColumns(containedColumns); - for (int index = containedColumns.size() - 1; index >= 0; index--) { - TunnelColumn column = containedColumns.get(index); - if (!isTunnelColumnContained( - view, - column, - candidateColumns, - dryHeadroom, - surfaceSampler - )) { - containedColumns.remove(index); - changed = true; - } - } - } while (changed && !containedColumns.isEmpty()); - Map actions = mergeActions(containedColumns); - LinkedHashMap preconditions = new LinkedHashMap<>(); - for (CavePosition position : actions.keySet()) { - preconditions.put(position, new CaveVoxelPrecondition( - view.voxelAt(position), - view.isOpenToSurface(position) - )); - } - for (CavePosition position : List.copyOf(actions.keySet())) { - RiverCaveAction action = actions.get(position); - for (int[] offset : NEIGHBORS) { - CavePosition neighbor = offset(position, offset); - if (actions.containsKey(neighbor) || !view.isInWorld(neighbor)) { - continue; - } - CaveVoxel neighborVoxel = view.voxelAt(neighbor); - boolean sealsSolidBoundary = neighborVoxel == CaveVoxel.SOLID; - boolean sealsCaveWaterline = action == RiverCaveAction.WET_SOURCE - && neighborVoxel == CaveVoxel.CAVE_AIR; - if (!sealsSolidBoundary && !sealsCaveWaterline) { - if (action == RiverCaveAction.DRY_AIR - && neighborVoxel == CaveVoxel.CAVE_AIR - && !view.isOpenToSurface(neighbor)) { - preconditions.putIfAbsent( - neighbor, - new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false) - ); - } - continue; - } - actions.putIfAbsent(neighbor, RiverCaveAction.SEAL_GUARD); - preconditions.putIfAbsent(neighbor, new CaveVoxelPrecondition( - neighborVoxel, - view.isOpenToSurface(neighbor) - )); - } - } - return new TunnelPlan( - Collections.unmodifiableMap(actions), - Collections.unmodifiableMap(preconditions) - ); - } - - private static TunnelColumn createTunnelColumn( - CaveVoxelView view, - int x, - int z, - IrisRiverTunnelSample sample - ) { - if (sample == null) { - return null; - } - int minimumY = sample.bedY() + 1; - int maximumY = sample.ceilingY(); - for (int y = minimumY; y <= maximumY; y++) { - CavePosition position = new CavePosition(x, y, z); - RiverCaveAction action = y <= sample.waterHeadY() - ? RiverCaveAction.WET_SOURCE - : RiverCaveAction.DRY_AIR; - if (!view.isInWorld(position) - || (!canCarveTunnelVoxel(view, position) - && !matchesPublishedAction(view, position, action))) { - return null; - } - } - return minimumY > maximumY - ? null - : new TunnelColumn(x, z, minimumY, sample.waterHeadY(), maximumY); - } - - private static boolean isTunnelColumnContained( - CaveVoxelView view, - TunnelColumn column, - Long2ObjectOpenHashMap candidateColumns, - int dryHeadroom, - SurfaceSampler surfaceSampler - ) { - for (int y = column.minimumY(); y <= column.maximumY(); y++) { - CavePosition position = new CavePosition(column.x(), y, column.z()); - for (int[] offset : NEIGHBORS) { - CavePosition neighbor = offset(position, offset); - if (containsAction(candidateColumns, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return false; - } - CaveVoxel neighborVoxel = view.voxelAt(neighbor); - if (neighborVoxel == CaveVoxel.SOLID - || (neighborVoxel == CaveVoxel.CAVE_AIR - && !view.isOpenToSurface(neighbor)) - || isSurfaceMouth(neighbor, dryHeadroom, surfaceSampler)) { - continue; - } - return false; - } - } - return true; - } - - private static boolean canCarveTunnelVoxel(CaveVoxelView view, CavePosition position) { - CaveVoxel voxel = view.voxelAt(position); - return voxel == CaveVoxel.SOLID - || (voxel == CaveVoxel.CAVE_AIR && !view.isOpenToSurface(position)); - } - - private static boolean isSurfaceMouth( - CavePosition position, - int dryHeadroom, - SurfaceSampler surfaceSampler - ) { - IrisRiverSurfaceSample sample = surfaceSampler.sample(position.x(), position.z()); - if (!sample.river().present() - || sample.river().state() != RiverRouteState.WET - || sample.subterranean()) { - return false; - } - int bedY = (int) Math.round(sample.terrainHeight()); - int headY = (int) Math.round(sample.waterSurfaceY()); - return position.y() > bedY && position.y() <= headY + Math.max(0, dryHeadroom); - } - - private static Map mergeActions(List columns) { - LinkedHashMap actions = new LinkedHashMap<>(); - for (TunnelColumn column : columns) { - for (int y = column.minimumY(); y <= column.maximumY(); y++) { - actions.put( - new CavePosition(column.x(), y, column.z()), - y <= column.waterHeadY() - ? RiverCaveAction.WET_SOURCE - : RiverCaveAction.DRY_AIR - ); - } - } - return actions; - } - - private static Long2ObjectOpenHashMap indexColumns(List columns) { - Long2ObjectOpenHashMap indexed = new Long2ObjectOpenHashMap<>(columns.size()); - for (TunnelColumn column : columns) { - indexed.put(Cache.key(column.x(), column.z()), column); - } - return indexed; - } - - private static boolean containsAction( - Long2ObjectOpenHashMap columns, - CavePosition position - ) { - TunnelColumn column = columns.get(Cache.key(position.x(), position.z())); - return column != null - && position.y() >= column.minimumY() - && position.y() <= column.maximumY(); - } - - private static boolean matchesPublishedAction( - CaveVoxelView view, - CavePosition position, - RiverCaveAction action - ) { - if (!(view instanceof TunnelVoxelView tunnelView)) { - return false; - } - RiverCaveHydrology hydrology = tunnelView.riverHydrologyAt(position); - return hydrology != null - && hydrology.action() == action - && hydrology.fluidKind() == RiverCaveFluidKind.RIVER; - } - - private static CavePosition offset(CavePosition position, int[] offset) { - return new CavePosition( - position.x() + offset[0], - position.y() + offset[1], - position.z() + offset[2] - ); - } - - private MantleRiverCaveVoxelView createView( - MantleWriter writer, - ChunkContext context, - RiverCaveFluidKind fluidKind - ) { - return new MantleRiverCaveVoxelView( - writer.getMantle(), - writer.getMantle().getWorldHeight(), - (x, z) -> context.getComplex().getRoundedHeighteightStream().get(x, z), - (x, z) -> context.getComplex().resolveRiverCaveFluid(fluidKind, x, z), - fluidKind, - (chunkX, chunkZ) -> generateCarvingInput(writer, context, chunkX, chunkZ) - ); - } - - private void generateCarvingInput( - MantleWriter writer, - ChunkContext context, - int chunkX, - int chunkZ - ) { - MantleComponent carving = getEngineMantle().getRegisteredComponents().get(ReservedFlag.CARVED); - if (carving == null || !carving.isEnabled()) { - throw new IllegalStateException("River hydrology requires the carving component"); - } - MantleChunk chunk = writer.acquireChunk(chunkX, chunkZ); - if (chunk == null) { - throw new IllegalStateException("River hydrology read exceeded the prepared mantle radius at " - + chunkX + "," + chunkZ); - } - chunk.raiseFlagSuspend(ReservedFlag.CARVED, () -> carving.generateLayer(writer, chunkX, chunkZ, context)); - } - - private void publishTunnels( - MantleWriter writer, - ChunkContext context, - IrisRiverRuntime runtime, - int chunkX, - int chunkZ - ) { - for (int attempt = 0; attempt < 2; attempt++) { - MantleRiverCaveVoxelView view = createView( - writer, - context, - RiverCaveFluidKind.RIVER - ); - TunnelPlan plan = planTunnels( - view, - chunkX, - chunkZ, - tunnelHalo(runtime), - runtime.maximumTunnelHeadroom(), - runtime::sampleFootprint, - runtime::sampleTunnel, - runtime::sample - ); - MantleRiverCaveVoxelView revalidationView = createView( - writer, - context, - RiverCaveFluidKind.RIVER - ); - if (preconditionsHold(revalidationView, plan.preconditions())) { - publishTunnelLocal(writer, chunkX, chunkZ, plan); - return; - } - } - } - - static RiverCavePlannerSettings plannerSettings(IrisRiverCaves caves, long seed, IrisData data) { - int horizontalRadius = generatedGrottoPublicationRadius(caves); - int maximumDepth = caves.getMaxBoreDepth() + caves.getGrottoVerticalRadius() + 1; - int throatLength = caves.getMaxBoreDepth() + horizontalRadius; - return new RiverCavePlannerSettings( - horizontalRadius, - maximumDepth, - caves.getMaxFloodVolume(), - throatLength, - caves.getThroatRadius(), - caves.getGrottoHorizontalRadius(), - caves.getGrottoVerticalRadius(), - caves.getDryHeadroom(), - fluidPolicy(caves.getExistingFluidPolicy()), - new ConfiguredRiverGrottoShape( - seed, - data, - caves.getGrottoShapeStyle(), - caves.getGrottoWarpStyle(), - caves.getGrottoWarpStrength(), - 0.2D - ), - caves.getMaxFloodRadius(), - caves.getMaxFloodDepth() - ); - } - - private SourceCandidate sourceFor( - IrisRiverRuntime runtime, - CaveVoxelView view, - IrisRiverCaves caves, - RiverAnchor anchor - ) { - int x = (int) StrictMath.floor(anchor.x()); - int z = (int) StrictMath.floor(anchor.z()); - IrisRiverSurfaceSample sample = runtime.sample(x, z); - IrisRiverTunnelSample tunnel = runtime.sampleTunnel(x, z); - if (!isWetChannelBed(sample) && tunnel == null) { - return null; - } - int bedY = tunnel == null - ? (int) Math.round(sample.terrainHeight()) - : tunnel.bedY(); - int headY = tunnel == null - ? waterHeadY(sample, caves) - : tunnel.waterHeadY() + caves.getWaterLevelOffset(); - int entryY = Math.max(bedY, headY); - CavePosition entry = new CavePosition(x, entryY, z); - if (!view.isInWorld(entry)) { - return null; - } - - CavePosition existingTarget = findExistingTarget(view, caves, x, z, bedY, headY); - RiverCaveMode requestedMode = sourceMode( - caves.getMode(), - runtime.isTerminalCaveAnchor(anchor) - ); - CavePosition target; - RiverCaveMode sourceMode; - if (requestedMode == RiverCaveMode.GENERATED_GROTTO) { - target = findGeneratedTarget(view, caves, entry, headY, 0, 0); - sourceMode = RiverCaveMode.GENERATED_GROTTO; - } else if (requestedMode == RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT) { - target = existingTarget; - sourceMode = RiverCaveMode.CLOSED_COMPONENT; - if (target == null) { - target = findGeneratedTarget(view, caves, entry, headY, 0, 0); - sourceMode = RiverCaveMode.GENERATED_GROTTO; - } - } else { - target = existingTarget; - sourceMode = requestedMode; - } - if (target == null && caves.getFallback() == IrisRiverCaveFallback.GENERATE_GROTTO) { - target = findGeneratedTarget(view, caves, entry, headY, 0, 0); - sourceMode = RiverCaveMode.GENERATED_GROTTO; - } - if (target == null) { - return null; - } - RiverCaveSource source = new RiverCaveSource(anchor.stableId(), entry, target, headY, sourceMode); - return new SourceCandidate(entry, headY, source); - } - - static boolean isWetChannelBed(IrisRiverSurfaceSample sample) { - return sample.river().present() - && sample.river().state() == RiverRouteState.WET - && sample.river().section() == RiverSection.CHANNEL - && sample.surfaceFluid(); - } - - static CavePosition findExistingTarget( - CaveVoxelView view, - IrisRiverCaves caves, - int x, - int z, - int bedY, - int headY - ) { - int maximumY = Math.min(bedY - 1, headY); - int minimumY = Math.max(1, bedY - caves.getMaxBoreDepth()); - for (int y = maximumY; y >= minimumY; y--) { - CavePosition position = new CavePosition(x, y, z); - CaveVoxel voxel = view.voxelAt(position); - if (voxel != CaveVoxel.SOLID) { - return position; - } - } - return null; - } - - static CavePosition findGeneratedTarget( - CaveVoxelView view, - IrisRiverCaves caves, - CavePosition entry, - int headY, - int offsetX, - int offsetZ - ) { - int preferredY = Math.min( - headY + caves.getDryHeadroom() - caves.getGrottoVerticalRadius(), - entry.y() - caves.getGrottoVerticalRadius() - 1 - ); - int maximumY = Math.min(Math.min(entry.y() - 1, headY), preferredY); - int minimumY = Math.max(1, entry.y() - caves.getMaxBoreDepth()); - for (int y = maximumY; y >= minimumY; y--) { - CavePosition target = new CavePosition(entry.x() + offsetX, y, entry.z() + offsetZ); - if (view.isInWorld(target) && view.voxelAt(target) == CaveVoxel.SOLID) { - return target; - } - } - return null; - } - - private RiverCaveSource fallbackSource( - CaveVoxelView view, - IrisRiverCaves caves, - RiverCavePlannerSettings settings, - SourceCandidate candidate, - RiverCaveSource rejected - ) { - int fallbackDistance = caves.getThroatRadius() + 2; - for (int index = 0; index < FALLBACK_X.length; index++) { - int offsetX = FALLBACK_X[index] * fallbackDistance; - int offsetZ = FALLBACK_Z[index] * fallbackDistance; - CavePosition target = findGeneratedTarget( - view, - caves, - candidate.entry(), - candidate.waterHeadY(), - offsetX, - offsetZ - ); - if (target == null || target.equals(rejected.target())) { - continue; - } - RiverCaveSource fallback = new RiverCaveSource( - rejected.sourceId(), - candidate.entry(), - target, - candidate.waterHeadY(), - RiverCaveMode.GENERATED_GROTTO - ); - if (planner.plan(view, fallback, settings).accepted()) { - return fallback; - } - } - return null; - } - - static RiverCaveMode sourceMode(IrisRiverCaveMode mode, boolean forcedTerminal) { - if (forcedTerminal) { - return RiverCaveMode.GENERATED_GROTTO; - } - return switch (mode) { - case FLOOD_CLOSED_COMPONENT -> RiverCaveMode.CLOSED_COMPONENT; - case GENERATE_GROTTO -> RiverCaveMode.GENERATED_GROTTO; - case GROTTO_OR_CLOSED_COMPONENT -> RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT; - case WATERFALL_POOL -> RiverCaveMode.WATERFALL_POOL; - case SEALED -> throw new IllegalArgumentException("Sealed river caves do not create sources"); - }; - } - - private void publishLocal( - MantleWriter writer, - int chunkX, - int chunkZ, - RiverCavePlanningResult result, - Map floodedBiomes, - RiverCaveFluidKind fluidKind - ) { - Map owners = actionOwners(result); - ArrayList> actions = new ArrayList<>(result.actions().entrySet()); - actions.sort(ACTION_ORDER); - for (Map.Entry entry : actions) { - CavePosition position = entry.getKey(); - if (!owns(chunkX, chunkZ, position)) { - continue; - } - RiverCaveSource source = owners.get(position); - String biome = source == null ? "" : floodedBiomes.getOrDefault(source.sourceId(), ""); - if (entry.getValue() == RiverCaveAction.SEAL_GUARD) { - biome = ""; - } - writer.setData( - position.x(), - position.y(), - position.z(), - new RiverCaveHydrology(entry.getValue(), biome, fluidKind) - ); - } - } - - private void publishTunnelLocal( - MantleWriter writer, - int chunkX, - int chunkZ, - TunnelPlan plan - ) { - ArrayList> actions = new ArrayList<>(plan.actions().entrySet()); - actions.sort(ACTION_ORDER); - for (Map.Entry entry : actions) { - CavePosition position = entry.getKey(); - if (owns(chunkX, chunkZ, position)) { - writer.setData( - position.x(), - position.y(), - position.z(), - RiverCaveHydrology.of(entry.getValue(), RiverCaveFluidKind.RIVER) - ); - } - } - } - - private Map actionOwners(RiverCavePlanningResult result) { - Map owners = new LinkedHashMap<>(); - for (RiverCavePlan plan : result.plans()) { - if (!plan.accepted()) { - continue; - } - for (CavePosition position : plan.actions().keySet()) { - owners.put(position, plan.source()); - } - } - return owners; - } - - private record SourceCandidate( - CavePosition entry, - int waterHeadY, - RiverCaveSource source - ) { - } - - @FunctionalInterface - interface FootprintSampler { - RiverSample sample(double minimumX, double minimumZ, double maximumX, double maximumZ); - } - - @FunctionalInterface - interface TunnelSampler { - IrisRiverTunnelSample sample(int x, int z); - } - - @FunctionalInterface - interface SurfaceSampler { - IrisRiverSurfaceSample sample(int x, int z); - } - - interface TunnelVoxelView extends CaveVoxelView { - RiverCaveHydrology riverHydrologyAt(CavePosition position); - } - - record TunnelPlan( - Map actions, - Map preconditions - ) { - static TunnelPlan empty() { - return new TunnelPlan(Map.of(), Map.of()); - } - } - - private record TunnelColumn( - int x, - int z, - int minimumY, - int waterHeadY, - int maximumY - ) { - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java index d01588e81..ec0cf02e9 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlan.java @@ -32,17 +32,16 @@ final class SurfaceFluidBoundaryPlan { int[] chunkSurfaceHeights, double[] fieldSurfaceHeights, boolean[] fieldHasFluid, - double[] fieldFluidHeights, int fieldSize, int padding, - long[] boundaries + int fluidHeight, + int[] boundaryStartY ) { if (chunkSurfaceHeights == null || chunkSurfaceHeights.length < CHUNK_AREA - || boundaries == null || boundaries.length < CHUNK_AREA + || boundaryStartY == null || boundaryStartY.length < CHUNK_AREA || padding < 1 || fieldSize < CHUNK_SIZE + (padding * 2) || fieldSurfaceHeights == null || fieldSurfaceHeights.length < fieldSize * fieldSize - || fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize - || fieldFluidHeights == null || fieldFluidHeights.length < fieldSize * fieldSize) { + || fieldHasFluid == null || fieldHasFluid.length < fieldSize * fieldSize) { throw new IllegalArgumentException("Surface fluid boundary fields do not cover a padded chunk"); } @@ -52,67 +51,44 @@ final class SurfaceFluidBoundaryPlan { int fieldZ = localZ + padding; int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); int boundaryY = NO_BOUNDARY; - int boundaryEndY = Integer.MIN_VALUE; int surfaceY = chunkSurfaceHeights[columnIndex]; int fieldIndex = (fieldX * fieldSize) + fieldZ; - int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]); if (fieldHasFluid[fieldIndex] && surfaceY < fluidHeight) { boundaryY = surfaceY; - boundaryEndY = fluidHeight; } - long boundary = expandBoundary(boundaryY, boundaryEndY, fieldSurfaceHeights, - fieldHasFluid, fieldFluidHeights, ((fieldX - 1) * fieldSize) + fieldZ); - boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights, - fieldHasFluid, fieldFluidHeights, ((fieldX + 1) * fieldSize) + fieldZ); - boundary = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights, - fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ - 1); - boundaries[columnIndex] = expandBoundary(startY(boundary), endY(boundary), fieldSurfaceHeights, - fieldHasFluid, fieldFluidHeights, (fieldX * fieldSize) + fieldZ + 1); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + ((fieldX - 1) * fieldSize) + fieldZ, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + ((fieldX + 1) * fieldSize) + fieldZ, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + (fieldX * fieldSize) + fieldZ - 1, fluidHeight); + boundaryY = lowerBoundary(boundaryY, fieldSurfaceHeights, fieldHasFluid, + (fieldX * fieldSize) + fieldZ + 1, fluidHeight); + boundaryStartY[columnIndex] = boundaryY; } } } - static boolean protects(long[] boundaries, int columnIndex, int y) { - if (boundaries == null || columnIndex < 0 || columnIndex >= boundaries.length) { - return false; - } - long boundary = boundaries[columnIndex]; - return y >= startY(boundary) && y <= endY(boundary); + static boolean protects(int[] boundaryStartY, int columnIndex, int y, int fluidHeight) { + return boundaryStartY != null + && columnIndex >= 0 + && columnIndex < boundaryStartY.length + && y >= boundaryStartY[columnIndex] + && y <= fluidHeight; } - static int startY(long boundary) { - return (int) (boundary >> 32); - } - - static int endY(long boundary) { - return (int) boundary; - } - - private static long expandBoundary( + private static int lowerBoundary( int currentBoundaryY, - int currentBoundaryEndY, double[] fieldSurfaceHeights, boolean[] fieldHasFluid, - double[] fieldFluidHeights, - int fieldIndex + int fieldIndex, + int fluidHeight ) { - int fluidHeight = roundedHeight(fieldFluidHeights[fieldIndex]); int neighborSurfaceY = (int) Math.round(fieldSurfaceHeights[fieldIndex]); if (!fieldHasFluid[fieldIndex] || neighborSurfaceY >= fluidHeight) { - return boundary(currentBoundaryY, currentBoundaryEndY); + return currentBoundaryY; } - return boundary( - Math.min(currentBoundaryY, neighborSurfaceY + 1), - Math.max(currentBoundaryEndY, fluidHeight) - ); - } - - private static int roundedHeight(double height) { - return Double.isFinite(height) ? (int) Math.round(height) : Integer.MIN_VALUE; - } - - static long boundary(int startY, int endY) { - return ((long) startY << 32) | (endY & 0xffffffffL); + return Math.min(currentBoundaryY, neighborSurfaceY + 1); } } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 489af7a6b..01c701e8f 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -28,9 +28,6 @@ import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.engine.object.IrisProceduralBlocks; -import art.arcane.iris.engine.object.IrisRiverCaves; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.common.data.B; import art.arcane.volmlib.util.documentation.ChunkCoordinates; @@ -59,8 +56,6 @@ public class IrisCarveModifier extends EngineAssignedModifier carveResolver.apply( - xx, - yy, - zz, - cavern, - dataIfPresent(mantleChunk, xx, yy, zz, RiverCaveHydrology.class) - )); - mantleChunk.iterate(RiverCaveHydrology.class, (xx, yy, zz, hydrology) -> { - if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) { - carveResolver.apply(xx, yy, zz, null, hydrology); + mantleChunk.iterate(MatterCavern.class, (xx, yy, zz, cavern) -> { + if (cavern == null) { + return; + } + + if (yy >= worldHeightSpan || yy <= 0) { + return; + } + + int rx = xx & 15; + int rz = zz & 15; + int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz); + + if (upperSurfaceHeights != null && yy >= upperSurfaceHeights[columnIndex]) { + return; + } + + PlatformBlockState current = output.getRaw(rx, yy, rz); + boolean explicitCarveIntent = hasExplicitCarveIntent(cavern); + + if (shouldPreserveExistingFluid(cavern, current)) { + return; + } + + columnMasks[columnIndex].add(yy); + + if (!cavern.getCustomBiome().isEmpty()) { + scratch.customCaveBiomePresent = true; + } + + if (current.isAir() && !explicitCarveIntent) { + return; + } + + if (explicitCarveIntent) { + // Only a fluid cavern consumes the fluid sample, and on the maintenance path that + // sample is a full procedural stream evaluation, so never take it per voxel. + PlatformBlockState fluid = isFluidIntent(cavern) ? context.getFluid().get(rx, rz) : null; + output.setRaw(rx, yy, rz, resolveExplicitCarveState(cavern, fluid, LAVA, AIR)); + } else if (usesDefaultLava(caveLavaHeight, yy)) { + output.setRaw(rx, yy, rz, LAVA); + } else { + output.setRaw(rx, yy, rz, AIR); } }); if (scratch.customCaveBiomePresent) { @@ -144,12 +160,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { - RiverCaveHydrology hydrology = dataIfPresent( - mantleChunk, rx, yy, rz, RiverCaveHydrology.class); - if (hydrology != null && hydrology.protectsPlacement()) { - return; - } + walls.forEach((rx, yy, rz, cavern) -> { int worldX = rx + chunkBlockX; int worldZ = rz + chunkBlockZ; String customBiome = cavern.getCustomBiome(); @@ -168,36 +179,14 @@ public class IrisCarveModifier extends EngineAssignedModifier fluid; - case FALLING_FLUID -> fallingFluidState(fluid); - case DRY_AIR -> air; - case SEAL_GUARD -> normalizeWaterlogging(current, null); - }; - } - - static PlatformBlockState normalizeWaterlogging(PlatformBlockState state, PlatformBlockState resultingFluid) { - if (state == null || B.isFluid(state) || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) { - return state; - } - String target = resultingFluid != null && resultingFluid.isWater() ? "true" : "false"; - if (target.equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) { - return state; - } - return state.withProperty("waterlogged", target); - } - - static PlatformBlockState normalizeHydrologyWaterlogging( - PlatformBlockState state, - MatterCavern baseline, - RiverCaveHydrology hydrology, - PlatformBlockState columnFluid - ) { - if (hydrology == null) { - return state; - } - MatterCavern composed = composeCavern(baseline, hydrology); - PlatformBlockState resultingFluid = isFluidIntent(composed) ? columnFluid : null; - return normalizeWaterlogging(state, resultingFluid); - } - - private static PlatformBlockState fallingFluidState(PlatformBlockState fluid) { - if (fluid == null || !IrisProceduralBlocks.hasProperty(fluid, "level")) { - return fluid; - } - if ("8".equals(IrisProceduralBlocks.propertyValue(fluid, "level"))) { - return fluid; - } - return fluid.withProperty("level", "8"); - } - - private final class CarveResolver { - private final CarveResolutionContext context; - - private CarveResolver(CarveResolutionContext context) { - this.context = context; - } - - private void apply( - int x, - int y, - int z, - MatterCavern baseline, - RiverCaveHydrology hydrology - ) { - if (y >= context.worldHeightSpan() || y <= 0) { - return; - } - - int localX = x & 15; - int localZ = z & 15; - int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ); - if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) { - return; - } - - PlatformBlockState current = context.output().getRaw(localX, y, localZ); - if (hydrology != null && hydrology.action() == RiverCaveAction.SEAL_GUARD) { - PlatformBlockState normalized = resolveHydrologyState(hydrology, current, null, AIR); - if (normalized != current) { - context.output().setRaw(localX, y, localZ, normalized); - } - return; - } - - MatterCavern cavern = composeCavern(baseline, hydrology); - if (cavern == null || shouldPreserveExistingFluid(cavern, current)) { - return; - } - - context.columnMasks()[columnIndex].add(y); - if (!cavern.getCustomBiome().isEmpty()) { - context.scratch().customCaveBiomePresent = true; - } - - boolean explicitCarveIntent = hasExplicitCarveIntent(cavern); - if (current.isAir() && !explicitCarveIntent) { - return; - } - - PlatformBlockState fluid = null; - if (isFluidIntent(cavern)) { - fluid = hydrology == null - ? context.chunkContext().getFluid().get(localX, localZ) - : getComplex().resolveRiverCaveFluid( - hydrology.fluidKind(), - context.chunkBlockX() + localX, - context.chunkBlockZ() + localZ - ); - } - if (hydrology != null) { - context.output().setRaw(localX, y, localZ, - resolveHydrologyState(hydrology, current, fluid, AIR)); - return; - } - if (explicitCarveIntent) { - context.output().setRaw(localX, y, localZ, - resolveExplicitCarveState(cavern, fluid, LAVA, AIR)); - } else if (usesDefaultLava(context.caveLavaHeight(), y)) { - context.output().setRaw(localX, y, localZ, LAVA); - } else { - context.output().setRaw(localX, y, localZ, AIR); - } - } - } - - private record CarveResolutionContext( - Hunk output, - ChunkContext chunkContext, - IrisCarveScratch scratch, - CarveColumnMask[] columnMasks, - int[] upperSurfaceHeights, - int worldHeightSpan, - int caveLavaHeight, - int chunkBlockX, - int chunkBlockZ - ) { - } - private void addInternalWallsFromMasks(CarveWallBuffer walls, CarveColumnMask[] columnMasks) { for (int columnIndex = 0; columnIndex < 256; columnIndex++) { CarveColumnMask columnMask = columnMasks[columnIndex]; @@ -419,16 +264,16 @@ public class IrisCarveModifier extends EngineAssignedModifier= 0) { if (rz < 15 && !columnMasks[columnIndex + 1].contains(yy)) { - walls.put(rx, yy, rz + 1, BASIC_CAVERN, false); + walls.put(rx, yy, rz + 1, BASIC_CAVERN); } if (rx < 15 && !columnMasks[columnIndex + 16].contains(yy)) { - walls.put(rx + 1, yy, rz, BASIC_CAVERN, false); + walls.put(rx + 1, yy, rz, BASIC_CAVERN); } if (rz > 0 && !columnMasks[columnIndex - 1].contains(yy)) { - walls.put(rx, yy, rz - 1, BASIC_CAVERN, false); + walls.put(rx, yy, rz - 1, BASIC_CAVERN); } if (rx > 0 && !columnMasks[columnIndex - 16].contains(yy)) { - walls.put(rx - 1, yy, rz, BASIC_CAVERN, false); + walls.put(rx - 1, yy, rz, BASIC_CAVERN); } yy = columnMask.nextSetBit(yy + 1); } @@ -446,21 +291,19 @@ public class IrisCarveModifier extends EngineAssignedModifier= 0) { - MatterCavern cavern = composedCavernAt(mc, rx, yy, rz); + MatterCavern cavern = mc.get(rx, yy, rz, MatterCavern.class); if (cavern != null) { - RiverCaveHydrology hydrology = dataIfPresent(mc, rx, yy, rz, RiverCaveHydrology.class); - boolean riverBoundary = hydrology != null && !hydrology.floodedBiomeKey().isEmpty(); - if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) { - walls.put(rx, yy, rz + 1, cavern, riverBoundary); + if (rz < 15 && mc.get(rx, yy, rz + 1, MatterCavern.class) == null) { + walls.put(rx, yy, rz + 1, cavern); } - if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) { - walls.put(rx + 1, yy, rz, cavern, riverBoundary); + if (rx < 15 && mc.get(rx + 1, yy, rz, MatterCavern.class) == null) { + walls.put(rx + 1, yy, rz, cavern); } - if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) { - walls.put(rx, yy, rz - 1, cavern, riverBoundary); + if (rz > 0 && mc.get(rx, yy, rz - 1, MatterCavern.class) == null) { + walls.put(rx, yy, rz - 1, cavern); } - if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) { - walls.put(rx - 1, yy, rz, cavern, riverBoundary); + if (rx > 0 && mc.get(rx - 1, yy, rz, MatterCavern.class) == null) { + walls.put(rx - 1, yy, rz, cavern); } } yy = columnMask.nextSetBit(yy + 1); @@ -527,19 +370,16 @@ public class IrisCarveModifier extends EngineAssignedModifier mantleChunk, int x, int y, int z) { - MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class); - RiverCaveHydrology hydrology = dataIfPresent(mantleChunk, x, y, z, RiverCaveHydrology.class); - return composeCavern(baseline, hydrology); - } - - private static T dataIfPresent(MantleChunk mantleChunk, int x, int y, int z, Class type) { - int section = y >> 4; - if (y < 0 || !mantleChunk.exists(section)) { - return null; - } - Matter matter = mantleChunk.get(section); - if (matter == null || !matter.hasSlice(type)) { - return null; - } - return matter.getSlice(type).get(x & 15, y & 15, z & 15); - } - private void processColumnFromMask( Hunk output, MantleChunk mc, @@ -607,8 +429,7 @@ public class IrisCarveModifier extends EngineAssignedModifier output, - MantleChunk mantleChunk, CarveColumnMask boundaryMask, CarveWallBuffer walls, int columnIndex, @@ -654,21 +473,18 @@ public class IrisCarveModifier extends EngineAssignedModifier output, - MantleChunk mantleChunk, CarveWallBuffer walls, int rx, int rz, @@ -682,12 +498,10 @@ public class IrisCarveModifier extends EngineAssignedModifier= worldMaxY) { break; } - RiverCaveHydrology hydrology = dataIfPresent( - mantleChunk, rx, ceilingY, rz, RiverCaveHydrology.class); - if (hydrology != null - && hydrology.protectsPlacement() - && hydrology.action() != RiverCaveAction.SEAL_GUARD) { - continue; - } PlatformBlockState existing = output.getRaw(rx, ceilingY, rz); if (!B.isSolid(existing)) { continue; } PlatformBlockState layer = ceilingLayers.get(i); - if (!canReplaceRiverGuard(hydrology, layer, true)) { - continue; - } if (B.isOre(existing)) { output.setRaw(rx, ceilingY, rz, B.toDeepSlateOre(existing, layer)); continue; @@ -770,11 +565,7 @@ public class IrisCarveModifier extends EngineAssignedModifier output, MantleChunk mc, Mantle mantle, - CaveZone zone, int rx, int rz, int xx, int zz, - IrisDimensionCarvingResolver.State resolverState, - Long2ObjectOpenHashMap caveBiomeCache, - Map customBiomeCache) { + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { int maxY = output.getHeight(); if (zone.ceiling + 1 < maxY && B.isDecorant(output.getRaw(rx, zone.ceiling + 1, rz))) { @@ -799,7 +590,6 @@ public class IrisCarveModifier extends EngineAssignedModifier= maxY) { break; } - RiverCaveHydrology hydrology = dataIfPresent(mc, rx, cy, rz, RiverCaveHydrology.class); - if (hydrology != null - && hydrology.protectsPlacement() - && hydrology.action() != RiverCaveAction.SEAL_GUARD) { - continue; - } PlatformBlockState block = ceilingBlocks.get(i); PlatformBlockState existing = output.getRaw(rx, cy, rz); - if (!B.isSolid(existing) || !canReplaceRiverGuard(hydrology, block, true)) { + if (!B.isSolid(existing)) { continue; } if (B.isOre(existing)) { @@ -870,133 +646,33 @@ public class IrisCarveModifier extends EngineAssignedModifier 0 && zone.getCeiling() + 1 < maxY && B.isSolid(output.getRaw(rx, zone.getCeiling() + 1, rz))) { decorant.getCeilingDecorator().decorate(rx, rz, xx, xx, xx, zz, zz, zz, output, ceilingBiome, InferredType.CAVE, zone.getCeiling(), zone.airThickness()); } - - normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz); - } - - private void normalizeCaveZoneWaterlogging( - Hunk output, - MantleChunk mantleChunk, - CaveZone zone, - int localX, - int localZ, - int worldX, - int worldZ - ) { - int minimumY = Math.max(0, zone.floor - 1); - int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1); - for (int y = minimumY; y <= maximumY; y++) { - RiverCaveHydrology hydrology = dataIfPresent( - mantleChunk, localX, y, localZ, RiverCaveHydrology.class); - if (hydrology == null) { - continue; - } - MatterCavern baseline = dataIfPresent( - mantleChunk, localX, y, localZ, MatterCavern.class); - PlatformBlockState current = output.getRaw(localX, y, localZ); - PlatformBlockState columnFluid = getComplex().resolveRiverCaveFluid( - hydrology.fluidKind(), - worldX, - worldZ - ); - PlatformBlockState normalized = normalizeHydrologyWaterlogging( - current, - baseline, - hydrology, - columnFluid - ); - if (normalized != current) { - output.setRaw(localX, y, localZ, normalized); - } - } } IrisBiome resolveCaveBoundaryBiome(MantleChunk mantleChunk, int x, int y, int z, int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { - MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z); - RiverCaveHydrology hydrology = dataIfPresent( - mantleChunk, x, y, z, RiverCaveHydrology.class); + MatterCavern cavern = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class); return resolveCaveBoundaryBiome( - cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache, - hydrology != null && !hydrology.floodedBiomeKey().isEmpty()); + cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache); + } + + private static T dataIfPresent(MantleChunk mantleChunk, int x, int y, int z, Class type) { + int section = y >> 4; + if (y < 0 || !mantleChunk.exists(section)) { + return null; + } + Matter matter = mantleChunk.get(section); + if (matter == null || !matter.hasSlice(type)) { + return null; + } + return matter.getSlice(type).get(x & 15, y & 15, z & 15); } IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache) { - return resolveCaveBoundaryBiome( - cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache, false); - } - - private IrisBiome resolveCaveBoundaryBiome(MatterCavern cavern, int worldX, int y, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache, Map customBiomeCache, boolean riverBoundary) { if (cavern != null && !cavern.getCustomBiome().isEmpty()) { - if (riverBoundary && selectsParentRiverBiome( - getEngine().getSeedManager().getCarve(), - worldX, - worldZ, - riverParentBiomeInheritance())) { - IrisBiome parent = resolveRiverParentBiome( - caveBiomeCache, worldX, y, worldZ, resolverState); - if (parent != null) { - return parent; - } - } return resolveCustomBiome(customBiomeCache, cavern.getCustomBiome()); } return resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState); } - static boolean selectsParentRiverBiome(long seed, int worldX, int worldZ, double inheritance) { - if (inheritance <= 0D) { - return false; - } - if (inheritance >= 1D) { - return true; - } - int cellX = Math.floorDiv(worldX, RIVER_BIOME_INHERITANCE_CELL_SIZE); - int cellZ = Math.floorDiv(worldZ, RIVER_BIOME_INHERITANCE_CELL_SIZE); - long hash = (seed + RIVER_BIOME_INHERITANCE_SALT) ^ BlockPosition.toLong(cellX, 0, cellZ); - hash = (hash ^ (hash >>> 30)) * 0xBF58476D1CE4E5B9L; - hash = (hash ^ (hash >>> 27)) * 0x94D049BB133111EBL; - hash ^= hash >>> 31; - double roll = (hash >>> 11) * 0x1.0p-53; - return roll < inheritance; - } - - static boolean canReplaceRiverGuard( - RiverCaveHydrology hydrology, - PlatformBlockState layer, - boolean ceiling - ) { - if (hydrology == null || hydrology.action() != RiverCaveAction.SEAL_GUARD) { - return true; - } - return layer != null - && B.isSolid(layer) - && !B.isFluid(layer) - && (!ceiling || !isGravityAffected(layer)); - } - - private double riverParentBiomeInheritance() { - if (getDimension().getRivers() == null || getDimension().getRivers().getCaves() == null) { - return 0D; - } - IrisRiverCaves caves = getDimension().getRivers().getCaves(); - return Math.max(0D, Math.min(1D, caves.getParentBiomeInheritance())); - } - - private IrisBiome resolveRiverParentBiome( - Long2ObjectOpenHashMap caveBiomeCache, - int worldX, - int y, - int worldZ, - IrisDimensionCarvingResolver.State resolverState - ) { - IrisBiome parent = resolveCaveBiome(caveBiomeCache, worldX, y, worldZ, resolverState); - if (parent != null && parent == getEngine().getSurfaceBiome(worldX, worldZ)) { - IrisBiome natural = getComplex().getNaturalTrueBiomeStream().get(worldX, worldZ); - return natural == null ? parent : natural; - } - return parent; - } - static boolean canReplaceCaveFloorLayer(Hunk output, int x, int y, int z, PlatformBlockState layer) { return !isGravityAffected(layer) || y > 0 && B.isSolid(output.getRaw(x, y - 1, z)); } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java index 8397feef3..678fbd38c 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveScratch.java @@ -140,7 +140,6 @@ final class CarveWallBuffer { private int[] keys; private MatterCavern[] values; - private boolean[] riverBoundaries; private int mask; private int resizeAt; private int size; @@ -155,12 +154,11 @@ final class CarveWallBuffer { keys = new int[capacity]; Arrays.fill(keys, EMPTY_KEY); values = new MatterCavern[capacity]; - riverBoundaries = new boolean[capacity]; mask = capacity - 1; resizeAt = Math.max(1, (int) (capacity * LOAD_FACTOR)); } - void put(int x, int y, int z, MatterCavern value, boolean riverBoundary) { + void put(int x, int y, int z, MatterCavern value) { int key = pack(x, y, z); int index = mix(key) & mask; @@ -169,7 +167,6 @@ final class CarveWallBuffer { if (existingKey == EMPTY_KEY) { keys[index] = key; values[index] = value; - riverBoundaries[index] = riverBoundary; size++; if (size >= resizeAt) { resize(); @@ -179,7 +176,6 @@ final class CarveWallBuffer { if (existingKey == key) { values[index] = value; - riverBoundaries[index] = riverBoundaries[index] || riverBoundary; return; } @@ -202,21 +198,6 @@ final class CarveWallBuffer { } } - boolean isRiverBoundary(int x, int y, int z) { - int key = pack(x, y, z); - int index = mix(key) & mask; - while (true) { - int existingKey = keys[index]; - if (existingKey == EMPTY_KEY) { - return false; - } - if (existingKey == key) { - return riverBoundaries[index]; - } - index = (index + 1) & mask; - } - } - void forEach(Consumer consumer) { for (int index = 0; index < keys.length; index++) { int key = keys[index]; @@ -226,7 +207,7 @@ final class CarveWallBuffer { MatterCavern cavern = values[index]; if (cavern != null) { - consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern, riverBoundaries[index]); + consumer.accept(unpackX(key), unpackY(key), unpackZ(key), cavern); } } } @@ -234,19 +215,16 @@ final class CarveWallBuffer { void clear() { Arrays.fill(keys, EMPTY_KEY); Arrays.fill(values, null); - Arrays.fill(riverBoundaries, false); size = 0; } private void resize() { int[] oldKeys = keys; MatterCavern[] oldValues = values; - boolean[] oldRiverBoundaries = riverBoundaries; int nextCapacity = oldKeys.length << 1; keys = new int[nextCapacity]; Arrays.fill(keys, EMPTY_KEY); values = new MatterCavern[nextCapacity]; - riverBoundaries = new boolean[nextCapacity]; mask = nextCapacity - 1; resizeAt = Math.max(1, (int) (nextCapacity * LOAD_FACTOR)); size = 0; @@ -255,12 +233,12 @@ final class CarveWallBuffer { int key = oldKeys[index]; MatterCavern value = oldValues[index]; if (key != EMPTY_KEY && value != null) { - reinsert(key, value, oldRiverBoundaries[index]); + reinsert(key, value); } } } - private void reinsert(int key, MatterCavern value, boolean riverBoundary) { + private void reinsert(int key, MatterCavern value) { int index = mix(key) & mask; while (keys[index] != EMPTY_KEY) { index = (index + 1) & mask; @@ -268,7 +246,6 @@ final class CarveWallBuffer { keys[index] = key; values[index] = value; - riverBoundaries[index] = riverBoundary; size++; } @@ -295,6 +272,6 @@ final class CarveWallBuffer { @FunctionalInterface interface Consumer { - void accept(int x, int y, int z, MatterCavern cavern, boolean riverBoundary); + void accept(int x, int y, int z, MatterCavern cavern); } } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java index 896da6096..4a8e82851 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java @@ -57,9 +57,10 @@ public class IrisPostModifier extends EngineAssignedModifier IrisDimension dimension = getDimension(); boolean walls = dimension.isPostProcessingWalls(); boolean slabs = dimension.isPostProcessingSlabs(); + int fluidHeight = dimension.getFluidHeight(); for (int i = 0; i < width; i++) { for (int j = 0; j < depth; j++) { - post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs); + post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs, fluidHeight); } } @@ -89,7 +90,7 @@ public class IrisPostModifier extends EngineAssignedModifier return heights; } - private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) { + private void post(int currentPostX, int currentPostZ, Hunk currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs, int fluidHeight) { // x/z are world coordinates, the hunk is indexed relative to this chunk origin. int originX = x - currentPostX; int originZ = z - currentPostZ; @@ -99,7 +100,6 @@ public class IrisPostModifier extends EngineAssignedModifier int hb = heights[center + planeWidth]; int hc = heights[center - 1]; int hd = heights[center - planeWidth]; - int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z)); // Floating Nibs int g = 0; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java b/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java index 44a93cab8..305f410d4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IObjectPlacer.java @@ -50,17 +50,6 @@ public interface IObjectPlacer { int getFluidHeight(); - default int getFluidHeight(int x, int z) { - Engine engine = getEngine(); - if (engine == null || engine.getComplex() == null) { - return getFluidHeight(); - } - int coordinateShift = getFluidHeight() - engine.getDimension().getFluidHeight(); - return coordinateShift + (int) Math.round( - engine.getComplex().getRiverWaterSurfaceStream().get(x, z) - ); - } - boolean isDebugSmartBore(); void setTile(int xx, int yy, int zz, TileData tile); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java index 4841cbf1f..ec7001c39 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiome.java @@ -130,8 +130,6 @@ public class IrisBiome extends IrisRegistrant implements IRare { private int lockLayersMax = 7; @Desc("Profile-driven 3D cave configuration") private IrisCaveProfile caveProfile = new IrisCaveProfile(); - @Desc("Biome-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit region and dimension settings.") - private IrisRiverOverride riverOverride = null; @MinNumber(1) @MaxNumber(512) @Desc("The rarity of this biome (integer)") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java index 8a09c6374..6ba1918dd 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeColorRenderer.java @@ -36,7 +36,7 @@ final class IrisBiomeColorRenderer { static Color getColor(IrisBiome biome, Engine engine, RenderType type) { switch (type) { - case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND, RIVER -> { + case BIOME, HEIGHT, CAVE_LAND, REGION, BIOME_SEA, BIOME_LAND -> { return biome.getCacheColor().aquire(() -> { if (biome.getColor() == null) { RandomColor randomColor = new RandomColor(biome.getName().hashCode()); diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java index cc9f49615..eef20e1a7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDepositGenerator.java @@ -49,6 +49,9 @@ import lombok.experimental.Accessors; @Desc("Creates ore & other block deposits underground") @Data public class IrisDepositGenerator { + private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L; + private static final long FNV_PRIME = 0x100000001b3L; + private final transient ConcurrentMap> objects = new ConcurrentHashMap<>(); private final transient AtomicCache> blockData = new AtomicCache<>(); private final transient AtomicCache ore = new AtomicCache<>(); @@ -135,7 +138,7 @@ public class IrisDepositGenerator { ClumpCacheKey cacheKey = new ClumpCacheKey(engine.getSeedManager().getDeposit(), minSize, maxSize); KList objects = this.objects.computeIfAbsent(cacheKey, key -> { - RNG rngv = new RNG(key.depositSeed() + hashCode()); + RNG rngv = new RNG(key.depositSeed() + stableClumpSalt(rdata)); KList objectsf = new KList<>(); for (int i = 0; i < varience; i++) { @@ -163,7 +166,7 @@ public class IrisDepositGenerator { engine.getSeedManager().getDeposit(), scaledMinSize, scaledMaxSize); KList objects = scaledObjects.computeIfAbsent(cacheKey, key -> { long sizeSeed = ((long) key.minSize() << 32) ^ (key.maxSize() & 0xffffffffL); - RNG rngv = new RNG(key.depositSeed() + hashCode() + sizeSeed); + RNG rngv = new RNG(key.depositSeed() + stableClumpSalt(rdata) + sizeSeed); KList generated = new KList<>(); for (int i = 0; i < varience; i++) { @@ -184,6 +187,64 @@ public class IrisDepositGenerator { return Math.max(0, Math.min(8192, (int) Math.round(size * multiplier))); } + long stableClumpSalt(IrisData rdata) { + long hash = FNV_OFFSET_BASIS; + hash = mix(hash, minHeight); + hash = mix(hash, maxHeight); + hash = mixEnum(hash, heightDistribution); + hash = mixEnum(hash, placementScope); + hash = mix(hash, surfaceClearance); + hash = mix(hash, minSize); + hash = mix(hash, maxSize); + hash = mixEnum(hash, shape); + hash = mix(hash, maxPerChunk); + hash = mix(hash, minPerChunk); + hash = mix(hash, Double.doubleToLongBits(spawnChance)); + hash = mix(hash, Double.doubleToLongBits(perClumpSpawnChance)); + hash = mix(hash, Double.doubleToLongBits(discardChanceOnAirExposure)); + KList resolvedPalette = getBlockData(rdata); + hash = mix(hash, resolvedPalette.size()); + for (PlatformBlockState block : resolvedPalette) { + hash = mixString(hash, block == null ? null : block.key()); + } + hash = mix(hash, varience); + hash = mixStrings(hash, replaceableBlocks); + hash = mixEnum(hash, biomeScope); + hash = mixStrings(hash, includedBiomes); + hash = mixStrings(hash, excludedBiomes); + return mix(hash, replaceBedrock ? 1L : 0L); + } + + private static long mix(long hash, long value) { + return (hash ^ value) * FNV_PRIME; + } + + private static long mixEnum(long hash, Enum value) { + return mixString(hash, value == null ? null : value.name()); + } + + private static long mixString(long hash, String value) { + if (value == null) { + return mix(hash, -1L); + } + long mixed = mix(hash, value.length()); + for (int i = 0; i < value.length(); i++) { + mixed = mix(mixed, value.charAt(i)); + } + return mixed; + } + + private static long mixStrings(long hash, KList values) { + if (values == null) { + return mix(hash, -1L); + } + long mixed = mix(hash, values.size()); + for (String value : values) { + mixed = mixString(mixed, value); + } + return mixed; + } + private IrisObject generateConfiguredClumpObject(RNG rng, IrisData rdata, int clumpMinSize, int clumpMaxSize) { int size = rng.i(clumpMinSize, clumpMaxSize + 1); return switch (shape) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index 6433ee185..d3679f7de 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -165,8 +165,6 @@ public class IrisDimension extends IrisRegistrant { private KList carving = new KList<>(); @Desc("Profile-driven 3D cave configuration") private IrisCaveProfile caveProfile = new IrisCaveProfile(); - @Desc("Connected surface rivers and contained river cave-water generation.") - private IrisRiverNetwork rivers = new IrisRiverNetwork(); @Desc("Refuse to place surface objects and trees over carved surface openings.") private boolean requireObjectSurfaceSupport = true; @MinNumber(0) @@ -515,17 +513,11 @@ public class IrisDimension extends IrisRegistrant { } Deque pending = new ArrayDeque<>(); - IrisRiverNetwork riverNetwork = getRivers(); - boolean riversEnabled = riverNetwork != null && riverNetwork.isEnabled(); - if (riversEnabled && riverNetwork.getBiomes() != null) { - addReachableBiomeKeys(pending, riverNetwork.getBiomes().getAllBiomeIds()); - } for (IrisRegion region : getAllRegions(g)) { if (region == null) { continue; } - addReachableBiomeKeys(pending, - riversEnabled ? region.getAllBiomeIds() : region.getNaturalBiomeIds()); + addReachableBiomeKeys(pending, region.getAllBiomeIds()); } for (IrisImageMapBinding binding : getImageMaps()) { if (binding == null || binding.getApplication() != IrisImageMapApplication.BIOME) { @@ -571,10 +563,6 @@ public class IrisDimension extends IrisRegistrant { biomes.put(loadKey, biome); addReachableBiomeKeys(pending, biome.getChildren()); addReachableBiomeKey(pending, biome.getCarvingBiome()); - if (riversEnabled && biome.getRiverOverride() != null) { - addReachableBiomeKeys(pending, biome.getRiverOverride().getAllBiomeIds()); - } - KList floatingChildren = biome.getFloatingChildBiomes(); if (floatingChildren == null) { continue; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java index c77a9c542..4bdb39890 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisEngineStreamType.java @@ -28,9 +28,6 @@ public enum IrisEngineStreamType { @Desc("Represents the given slope at the x, z coordinates") SLOPE((f) -> f.getComplex().getSlopeStream()), - @Desc("Represents terrain height before river incision and river biome replacement.") - NATURAL_HEIGHT((f) -> f.getComplex().getNaturalHeightStream()), - @Desc("Represents the base generator height at the given position. This includes only the biome generators / interpolation and noise features but does not include carving, caves.") HEIGHT((f) -> f.getComplex().getHeightStream()), @@ -44,19 +41,7 @@ public enum IrisEngineStreamType { REGION_STYLE((f) -> f.getComplex().getRegionStyleStream()), @Desc("Represents the identity of regions. Each region has a unique number (very large numbers)") - REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream()), - - @Desc("Represents block distance from the nearest active river centerline.") - RIVER_DISTANCE((f) -> f.getComplex().getRiverDistanceStream()), - - @Desc("Represents the merged upstream flow carried by the active river reach.") - RIVER_FLOW((f) -> f.getComplex().getRiverFlowStream()), - - @Desc("Represents the normalized river terrain-incision weight.") - RIVER_CARVE_WEIGHT((f) -> f.getComplex().getRiverCarveWeightStream()), - - @Desc("Represents the solved river water-surface height.") - RIVER_WATER_SURFACE((f) -> f.getComplex().getRiverWaterSurfaceStream()); + REGION_IDENTITY((f) -> f.getComplex().getRegionIdentityStream()); private final Function> getter; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java index e7f7f3f9f..d336d93c4 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java @@ -52,9 +52,7 @@ public class IrisInterpolator { @Override public int hashCode() { - // Bit-identical to Objects.hash(horizontalScale, function) without the Object[] + Double boxing. - // The exact value is load bearing: it decides HashMap bucket order for the generator maps in - // IrisComplex, and that order fixes the floating point summation order of interpolated heights. + // Bit-identical to Objects.hash(horizontalScale, function) without the Object[] or Double boxing. int result = 31 + Double.hashCode(horizontalScale); return (31 * result) + (function == null ? 0 : function.hashCode()); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java b/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java index d31215127..4d10aa6c7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisMaterialPalette.java @@ -20,21 +20,27 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.data.cache.LazyBoundedCache; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.iris.engine.object.annotations.Required; import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; -import art.arcane.iris.util.project.noise.CNG; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; -import art.arcane.iris.spi.PlatformBlockState; +import lombok.Setter; import lombok.experimental.Accessors; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; @Snippet("palette") @Accessors(chain = true) @@ -43,8 +49,17 @@ import java.util.Optional; @Desc("A palette of materials") @Data public class IrisMaterialPalette { + private static final int LAYER_GENERATOR_CACHE_SIZE = 32; + private static final int LAYER_GENERATOR_SALT = -23_498_896; + private final transient AtomicCache> blockData = new AtomicCache<>(); - private final transient AtomicCache layerGenerator = new AtomicCache<>(); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient LazyBoundedCache layerGenerators = + new LazyBoundedCache<>(LAYER_GENERATOR_CACHE_SIZE); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient AtomicReference recentLayerGenerator = new AtomicReference<>(); private final transient AtomicCache heightGenerator = new AtomicCache<>(); @Desc("The style of noise") private IrisGeneratorStyle style = NoiseStyle.STATIC.style(); @@ -82,11 +97,21 @@ public class IrisMaterialPalette { } public CNG getLayerGenerator(RNG rng, IrisData rdata) { - return layerGenerator.aquire(() -> - { - RNG rngx = rng.nextParallelRNG(-23498896 + getBlockData(rdata).size()); - return style.create(rngx, rdata); - }); + Engine engine = rdata == null ? null : rdata.getEngine(); + int generatorSignature = LAYER_GENERATOR_SALT + getBlockData(rdata).size(); + long generatorSeed = rng.getSeed() + generatorSignature; + CachedLayerGenerator recent = recentLayerGenerator.get(); + if (recent != null && recent.key.matches(rdata, engine, generatorSeed)) { + return recent.generator; + } + + LayerGeneratorKey key = new LayerGeneratorKey(rdata, engine, generatorSeed); + CNG generator = layerGenerators.computeIfAbsent(key, + ignored -> style.create(new RNG(generatorSeed), rdata, engine)); + if (generator != null) { + recentLayerGenerator.set(new CachedLayerGenerator(key, generator)); + } + return generator; } public IrisMaterialPalette qclear() { @@ -127,4 +152,49 @@ public class IrisMaterialPalette { palette.clear(); return this; } + + private static final class LayerGeneratorKey { + private final IrisData data; + private final Engine engine; + private final long seed; + + private LayerGeneratorKey(IrisData data, Engine engine, long seed) { + this.data = data; + this.engine = engine; + this.seed = seed; + } + + private boolean matches(IrisData data, Engine engine, long seed) { + return this.data == data && this.engine == engine && this.seed == seed; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof LayerGeneratorKey other)) { + return false; + } + return data == other.data && engine == other.engine && seed == other.seed; + } + + @Override + public int hashCode() { + int result = System.identityHashCode(data); + result = 31 * result + System.identityHashCode(engine); + result = 31 * result + Long.hashCode(seed); + return result; + } + } + + private static final class CachedLayerGenerator { + private final LayerGeneratorKey key; + private final CNG generator; + + private CachedLayerGenerator(LayerGeneratorKey key, CNG generator) { + this.key = key; + this.generator = generator; + } + } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java index 1c4d1b459..fdce91245 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java @@ -19,7 +19,8 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.data.cache.AtomicCache; +import art.arcane.iris.engine.data.cache.LazyBoundedCache; +import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.annotations.ArrayType; import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; @@ -30,11 +31,16 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.interpolation.IrisInterpolation; import art.arcane.iris.util.project.noise.CNG; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.experimental.Accessors; +import java.util.concurrent.atomic.AtomicReference; + @Snippet("generator") @Accessors(chain = true) @NoArgsConstructor @@ -42,7 +48,16 @@ import lombok.experimental.Accessors; @Desc("A noise generator") @Data public class IrisNoiseGenerator { - private final transient AtomicCache generator = new AtomicCache<>(); + private static final int GENERATOR_CACHE_SIZE = 32; + private static final long GENERATOR_SEED_SALT = 33_955_677L; + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient LazyBoundedCache generators = + new LazyBoundedCache<>(GENERATOR_CACHE_SIZE); + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private final transient AtomicReference recentGenerator = new AtomicReference<>(); @MinNumber(0.0001) @Desc("The coordinate input zoom") private double zoom = 1; @@ -87,7 +102,20 @@ public class IrisNoiseGenerator { } protected CNG getGenerator(long superSeed, IrisData data) { - return generator.aquire(() -> style.create(new RNG(superSeed + 33955677 - seed), data).oct(octaves)); + Engine engine = data == null ? null : data.getEngine(); + long generatorSeed = superSeed + GENERATOR_SEED_SALT - seed; + CachedGenerator recent = recentGenerator.get(); + if (recent != null && recent.key.matches(data, engine, generatorSeed)) { + return recent.generator; + } + + GeneratorKey key = new GeneratorKey(data, engine, generatorSeed); + CNG generator = generators.computeIfAbsent(key, + ignored -> style.createNoCache(new RNG(generatorSeed), data).oct(octaves)); + if (generator != null) { + recentGenerator.set(new CachedGenerator(key, generator)); + } + return generator; } public double getMax() { @@ -136,4 +164,49 @@ public class IrisNoiseGenerator { return g; } + + private static final class GeneratorKey { + private final IrisData data; + private final Engine engine; + private final long seed; + + private GeneratorKey(IrisData data, Engine engine, long seed) { + this.data = data; + this.engine = engine; + this.seed = seed; + } + + private boolean matches(IrisData data, Engine engine, long seed) { + return this.data == data && this.engine == engine && this.seed == seed; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (!(object instanceof GeneratorKey other)) { + return false; + } + return data == other.data && engine == other.engine && seed == other.seed; + } + + @Override + public int hashCode() { + int result = System.identityHashCode(data); + result = 31 * result + System.identityHashCode(engine); + result = 31 * result + Long.hashCode(seed); + return result; + } + } + + private static final class CachedGenerator { + private final GeneratorKey key; + private final CNG generator; + + private CachedGenerator(GeneratorKey key, CNG generator) { + this.key = key; + this.generator = generator; + } + } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java index c894795de..f8a73780e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacementRunner.java @@ -332,8 +332,7 @@ final class IrisObjectPlacementRunner { return -1; } - if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() - && y + rty + ty >= placer.getFluidHeight(x, z)) { + if (!config.isForcePlace() && !rawStructurePiece && config.isUnderwater() && y + rty + ty >= placer.getFluidHeight()) { return -1; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 0f2475884..7fd44ba09 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -118,8 +118,6 @@ public class IrisRegion extends IrisRegistrant implements IRare { private double caveBiomeZoom = 1; @Desc("Profile-driven 3D cave configuration") private IrisCaveProfile caveProfile = new IrisCaveProfile(); - @Desc("Region-level river routing, shape, cave-entry, and biome-pool overrides. Omit to inherit dimension settings.") - private IrisRiverOverride riverOverride = null; @RegistryListResource(IrisBiome.class) @Required @ArrayType(min = 1, type = String.class) @@ -279,33 +277,18 @@ public class IrisRegion extends IrisRegistrant implements IRare { } public KSet getAllBiomeIds() { - KSet names = getNaturalBiomeIds(); - if (riverOverride != null) { - names.addAll(riverOverride.getAllBiomeIds()); - } - return names; - } - - public KSet getNaturalBiomeIds() { KSet names = new KSet<>(); names.addAll(landBiomes); names.addAll(caveBiomes); names.addAll(seaBiomes); names.addAll(shoreBiomes); + return names; } public KList getAllBiomes(DataProvider g) { - return resolveBiomes(g, getAllBiomeIds()); - } - - public KList getNaturalBiomes(DataProvider g) { - return resolveBiomes(g, getNaturalBiomeIds()); - } - - private KList resolveBiomes(DataProvider g, KSet biomeIds) { KMap b = new KMap<>(); - KSet names = biomeIds.copy(); + KSet names = getAllBiomeIds(); while (!names.isEmpty()) { for (String i : new KList<>(names)) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverBiomes.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverBiomes.java deleted file mode 100644 index 95fd64bad..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverBiomes.java +++ /dev/null @@ -1,61 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KSet; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Biome pools used by dimension-level river sections and contained river caves.") -@Data -public class IrisRiverBiomes { - @Desc("Noise used to select a biome inside the active river-section pool.") - private IrisGeneratorStyle selectionStyle = new IrisGeneratorStyle(NoiseStyle.CELLULAR_IRIS_DOUBLE) - .zoomed(512D); - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Biome pool for wet river channels.") - private KList channel = new KList<>(); - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Biome pool for river banks outside the wet channel.") - private KList bank = new KList<>(); - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Biome pool for river reaches meeting natural sea.") - private KList mouth = new KList<>(); - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Biome pool for dry river channels and terminal tapers.") - private KList dry = new KList<>(); - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Cave biome pool for accepted contained river cave bodies.") - private KList floodedCave = new KList<>(); - - public KSet getAllBiomeIds() { - KSet biomeIds = new KSet<>(); - addAll(biomeIds, channel); - addAll(biomeIds, bank); - addAll(biomeIds, mouth); - addAll(biomeIds, dry); - addAll(biomeIds, floodedCave); - return biomeIds; - } - - private static void addAll(KSet destination, KList source) { - if (source != null) { - destination.addAll(source); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaveFallback.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaveFallback.java deleted file mode 100644 index d79e9b56b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaveFallback.java +++ /dev/null @@ -1,12 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Selects the fallback when a requested river cave connection cannot be proven safe.") -public enum IrisRiverCaveFallback { - @Desc("Keep the rejected connection sealed.") - SEALED, - - @Desc("Try a bounded generated grotto instead of the rejected existing cave.") - GENERATE_GROTTO -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaveMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaveMode.java deleted file mode 100644 index 40b6e3924..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaveMode.java +++ /dev/null @@ -1,21 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Selects the contained cave-water behavior available to river entry events.") -public enum IrisRiverCaveMode { - @Desc("Keep the surface reservoir sealed from caves.") - SEALED, - - @Desc("Flood only an existing cave component whose complete fluid-reachable boundary is proven closed.") - FLOOD_CLOSED_COMPONENT, - - @Desc("Generate a bounded grotto with a guaranteed solid shell.") - GENERATE_GROTTO, - - @Desc("Use a proven closed cave component when available, otherwise generate a bounded grotto.") - GROTTO_OR_CLOSED_COMPONENT, - - @Desc("Generate a controlled falling column into a proven contained pool.") - WATERFALL_POOL -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaves.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaves.java deleted file mode 100644 index 603a5dee7..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverCaves.java +++ /dev/null @@ -1,103 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Controls bounded and transactionally validated river-to-cave connections.") -@Data -public class IrisRiverCaves { - @Desc("The contained cave-water behavior available to river entry events.") - private IrisRiverCaveMode mode = IrisRiverCaveMode.SEALED; - - @Desc("Selects cave-entry stations at stable river-reach anchors.") - private IrisRiverNoiseChance entry = new IrisRiverNoiseChance() - .setChance(0.12D) - .setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(1024D)) - .setInfluence(0.4D); - - @MinNumber(16) - @MaxNumber(4096) - @Desc("The minimum distance in blocks between cave-entry candidates.") - private int minimumSpacing = 128; - - @MinNumber(0) - @MaxNumber(16) - @Desc("The maximum entry-noise-eligible cave anchors accepted on one ordinary reach. A forced sinkhole terminal uses its reach exclusively and requires this value above zero.") - private int maximumPerReach = 1; - - @MinNumber(1) - @MaxNumber(256) - @Desc("The maximum vertical distance searched while boring from river bed to cave.") - private int maxBoreDepth = 48; - - @MinNumber(1) - @MaxNumber(16) - @Desc("The radius in blocks of a generated river-to-cave throat.") - private int throatRadius = 2; - - @MinNumber(-64) - @MaxNumber(64) - @Desc("The offset applied to river water height when filling an accepted cave body.") - private int waterLevelOffset = 0; - - @MinNumber(0) - @MaxNumber(64) - @Desc("The minimum dry headroom retained above water in generated grottos.") - private int dryHeadroom = 4; - - @MinNumber(2) - @MaxNumber(128) - @Desc("The horizontal radius in blocks of a generated sealed grotto.") - private int grottoHorizontalRadius = 12; - - @MinNumber(2) - @MaxNumber(128) - @Desc("The vertical radius in blocks of a generated sealed grotto.") - private int grottoVerticalRadius = 7; - - @Desc("Noise shaping the boundary of generated sealed grottos.") - private IrisGeneratorStyle grottoShapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D); - - @Desc("Noise warping the coordinate field used for generated sealed grottos.") - private IrisGeneratorStyle grottoWarpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D); - - @MinNumber(0) - @MaxNumber(32) - @Desc("The maximum coordinate warp applied to generated sealed grottos in blocks.") - private double grottoWarpStrength = 2D; - - @MinNumber(0) - @MaxNumber(1) - @Desc("The fraction of river-cave boundary columns that inherit the naturally resolved parent cave or surface biome instead of a flooded-cave override.") - private double parentBiomeInheritance = 0.5D; - - @MinNumber(4) - @MaxNumber(256) - @Desc("The horizontal proof radius for an existing closed cave component.") - private int maxFloodRadius = 48; - - @MinNumber(4) - @MaxNumber(256) - @Desc("The vertical proof depth for an existing closed cave component.") - private int maxFloodDepth = 32; - - @MinNumber(64) - @MaxNumber(1048576) - @Desc("The greatest cave-component volume that may be fully proven and flooded.") - private int maxFloodVolume = 8192; - - @Desc("The behavior used when a requested cave connection cannot be proven safe.") - private IrisRiverCaveFallback fallback = IrisRiverCaveFallback.SEALED; - - @Desc("The policy for fluid already present in a candidate contained cave body.") - private IrisRiverExistingFluidPolicy existingFluidPolicy = IrisRiverExistingFluidPolicy.REJECT; - - @Desc("Sparse, independently filled cave-floor pools generated at deep river-network anchors.") - private IrisRiverDeepPools deepPools = new IrisRiverDeepPools(); -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverDeepPools.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverDeepPools.java deleted file mode 100644 index 283a98989..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverDeepPools.java +++ /dev/null @@ -1,92 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Controls sparse, river-anchored fluid pools attached to deep cave floors.") -@Data -public class IrisRiverDeepPools { - @Desc("Enables independently configured deep cave pools along eligible wet river reaches.") - private boolean enabled = false; - - @Desc("Selects complete wet river reaches that may host deep pools.") - private IrisRiverNoiseChance reach = new IrisRiverNoiseChance() - .setChance(1D / 3D) - .setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(4096D)) - .setInfluence(0.08D); - - @MinNumber(16) - @MaxNumber(4096) - @Desc("The minimum distance in blocks between deep-pool candidates.") - private int minimumSpacing = 768; - - @MinNumber(0) - @MaxNumber(16) - @Desc("The maximum accepted deep pools on one river reach.") - private int maximumPerReach = 1; - - @MinNumber(-2048) - @MaxNumber(2048) - @Desc("The lowest absolute world Y considered for the pool fluid surface.") - private int minimumFluidY = -224; - - @MinNumber(-2048) - @MaxNumber(2048) - @Desc("The highest absolute world Y considered for the pool fluid surface.") - private int maximumFluidY = -104; - - @MinNumber(0) - @MaxNumber(256) - @Desc("The horizontal distance searched from a river anchor for a contained cave floor.") - private int searchRadius = 16; - - @MinNumber(1) - @MaxNumber(64) - @Desc("The number of deterministic nearby columns tested for a contained cave floor.") - private int searchAttempts = 12; - - @MinNumber(2) - @MaxNumber(128) - @Desc("The horizontal radius of the generated deep-pool chamber.") - private int horizontalRadius = 18; - - @MinNumber(2) - @MaxNumber(64) - @Desc("The vertical radius of the generated deep-pool chamber.") - private int verticalRadius = 8; - - @MinNumber(1) - @MaxNumber(63) - @Desc("The dry chamber height retained above the deep-pool fluid surface.") - private int dryHeadroom = 4; - - @Desc("Noise shaping the deep-pool chamber boundary.") - private IrisGeneratorStyle shapeStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(12D); - - @MinNumber(0) - @MaxNumber(0.75) - @Desc("The proportional noise displacement applied to the deep-pool chamber boundary.") - private double shapeVariation = 0.5D; - - @Desc("Noise warping the deep-pool chamber coordinate field.") - private IrisGeneratorStyle warpStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(24D); - - @MinNumber(0) - @MaxNumber(64) - @Desc("The maximum coordinate warp applied to the deep-pool chamber in blocks.") - private double warpStrength = 6D; - - @MinNumber(64) - @MaxNumber(1048576) - @Desc("The greatest generated deep-pool chamber volume that may be transactionally published.") - private int maximumVolume = 32768; - - @Desc("The fluid palette used only by accepted deep pools.") - private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("lava"); -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverExistingFluidPolicy.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverExistingFluidPolicy.java deleted file mode 100644 index 356d8f71c..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverExistingFluidPolicy.java +++ /dev/null @@ -1,15 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Controls how river cave hydrology treats fluid already present in a candidate cave body.") -public enum IrisRiverExistingFluidPolicy { - @Desc("Reject a candidate containing any existing fluid.") - REJECT, - - @Desc("Accept only fluid compatible with the river fluid palette.") - ALLOW_SAME, - - @Desc("Replace contained existing fluid with the river fluid palette.") - REPLACE -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverNetwork.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverNetwork.java deleted file mode 100644 index 623043946..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverNetwork.java +++ /dev/null @@ -1,30 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Dimension-owned configuration for connected surface rivers and contained river cave water.") -@Data -public class IrisRiverNetwork { - @Desc("Enable the river network for this dimension.") - private boolean enabled = false; - - @Desc("Dimension-owned connected graph and source-selection settings.") - private IrisRiverTopology topology = new IrisRiverTopology(); - - @Desc("Channel geometry, terrain incision, meanders, and terminal behavior.") - private IrisRiverTerrain terrain = new IrisRiverTerrain(); - - @Desc("River water-surface settings.") - private IrisRiverWater water = new IrisRiverWater(); - - @Desc("Dimension-level biome pools for river sections and contained river caves.") - private IrisRiverBiomes biomes = new IrisRiverBiomes(); - - @Desc("Bounded river-to-cave connection settings.") - private IrisRiverCaves caves = new IrisRiverCaves(); -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverNoiseChance.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverNoiseChance.java deleted file mode 100644 index 2c66d92ff..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverNoiseChance.java +++ /dev/null @@ -1,27 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("A deterministic graph-event chance modulated by configurable noise.") -@Data -public class IrisRiverNoiseChance { - @MinNumber(0) - @MaxNumber(1) - @Desc("The base probability before noise modulation.") - private double chance = 1D; - - @Desc("The noise sampled once at the stable graph-event anchor.") - private IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT); - - @MinNumber(0) - @MaxNumber(1) - @Desc("The maximum centered noise contribution added to the base probability.") - private double influence = 0D; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverOverride.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverOverride.java deleted file mode 100644 index f5c7b9cf8..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverOverride.java +++ /dev/null @@ -1,103 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KSet; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Nullable river settings overridden by a region or natural biome without changing graph identity.") -@Data -public class IrisRiverOverride { - @Desc("Whether new river sources may begin in this area. Existing trunks are unaffected.") - private Boolean allowSources = null; - - @Desc("How downstream routing treats this area.") - private IrisRiverRoutingPolicy routingPolicy = null; - - @MinNumber(0) - @MaxNumber(64) - @Desc("Multiplier applied to downstream routing cost.") - private Double routingCostMultiplier = null; - - @MinNumber(0.0001) - @MaxNumber(16) - @Desc("Multiplier applied to wet channel width.") - private Double widthMultiplier = null; - - @MinNumber(0) - @MaxNumber(16) - @Desc("Multiplier applied to river bank width.") - private Double bankWidthMultiplier = null; - - @MinNumber(0.0001) - @MaxNumber(16) - @Desc("Multiplier applied to river-bed depth.") - private Double depthMultiplier = null; - - @MinNumber(0) - @MaxNumber(16) - @Desc("Multiplier applied to maximum terrain incision.") - private Double maxIncisionMultiplier = null; - - @MinNumber(0) - @MaxNumber(16) - @Desc("Multiplier applied to reach continuation probability.") - private Double continuationChanceMultiplier = null; - - @MinNumber(0) - @MaxNumber(16) - @Desc("Multiplier applied to cave-entry probability.") - private Double caveEntryMultiplier = null; - - @Desc("Optional terminal behavior override for failed routes in this area.") - private IrisRiverTerminalMode terminalMode = null; - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Optional replacement biome pool for wet river channels. Empty explicitly disables this pool.") - private KList channelBiomes = null; - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Optional replacement biome pool for river banks. Empty explicitly disables this pool.") - private KList bankBiomes = null; - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Optional replacement biome pool for river mouths. Empty explicitly disables this pool.") - private KList mouthBiomes = null; - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Optional replacement biome pool for dry river channels. Empty explicitly disables this pool.") - private KList dryBiomes = null; - - @RegistryListResource(IrisBiome.class) - @ArrayType(type = String.class) - @Desc("Optional replacement cave biome pool for accepted contained river cave bodies. Empty explicitly disables this pool.") - private KList floodedCaveBiomes = null; - - public KSet getAllBiomeIds() { - KSet biomeIds = new KSet<>(); - addAll(biomeIds, channelBiomes); - addAll(biomeIds, bankBiomes); - addAll(biomeIds, mouthBiomes); - addAll(biomeIds, dryBiomes); - addAll(biomeIds, floodedCaveBiomes); - return biomeIds; - } - - private static void addAll(KSet destination, KList source) { - if (source != null) { - destination.addAll(source); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingPolicy.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingPolicy.java deleted file mode 100644 index 5b75c5ba3..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverRoutingPolicy.java +++ /dev/null @@ -1,15 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Controls how river routing treats a region or biome.") -public enum IrisRiverRoutingPolicy { - @Desc("Allow normal river routing through this area.") - ALLOW, - - @Desc("Increase the routing cost while still permitting established river trunks.") - AVOID, - - @Desc("Forbid river reaches from crossing this area.") - BLOCK -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTerminalMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTerminalMode.java deleted file mode 100644 index 483b30469..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTerminalMode.java +++ /dev/null @@ -1,15 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Selects how a river route ends when it cannot continue to an outlet.") -public enum IrisRiverTerminalMode { - @Desc("Suppress the failed route instead of generating it.") - SUPPRESS, - - @Desc("Continue as a dry channel that tapers back into natural terrain.") - DRY_CHANNEL, - - @Desc("End in a contained underground grotto when cave hydrology accepts the connection.") - SINKHOLE_GROTTO -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTerrain.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTerrain.java deleted file mode 100644 index 4c94a056b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTerrain.java +++ /dev/null @@ -1,123 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.Required; -import art.arcane.volmlib.util.collection.KList; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Controls river channel geometry, banks, incision, meanders, and terminal tapering.") -@Data -public class IrisRiverTerrain { - @Desc("The wet channel width in blocks before stream-order scaling.") - private IrisStyledRange channelWidth = range(8D, 20D, NoiseStyle.IRIS, 1024D); - - @Desc("The bank width outside the wet channel in blocks.") - private IrisStyledRange bankWidth = range(5D, 18D, NoiseStyle.IRIS, 1024D); - - @Desc("The wet-bed depth below the local water surface, or dry-channel depth below natural terrain, in blocks.") - private IrisStyledRange depth = range(2D, 7D, NoiseStyle.IRIS, 768D); - - @MinNumber(0) - @MaxNumber(64) - @Desc("The radius added to every channel after worm, regional, local, and stream-order shaping.") - private double channelRadiusBonus = 0D; - - @MinNumber(1) - @MaxNumber(2048) - @Desc("The final wet-channel width cap after region, biome, and stream-order scaling.") - private double maxChannelWidth = 10D; - - @MinNumber(0) - @MaxNumber(2048) - @Desc("The final bank-width cap on each side after region and biome scaling.") - private double maxBankWidth = 4D; - - @MinNumber(1) - @MaxNumber(512) - @Desc("The final river-depth cap after region, biome, and stream-order scaling.") - private double maxDepth = 10D; - - @MinNumber(0) - @MaxNumber(8) - @Desc("Additional channel-width fraction applied for each merged upstream flow order.") - private double orderWidthFactor = 0.35D; - - @MinNumber(0) - @MaxNumber(8) - @Desc("Additional river-bed depth fraction applied for each merged upstream flow order.") - private double orderDepthFactor = 0.2D; - - @Desc("Selects whether a complete graph reach may incise terrain. A rejected reach follows terminal behavior.") - private IrisRiverNoiseChance incision = new IrisRiverNoiseChance(); - - @MinNumber(0) - @MaxNumber(512) - @Desc("The greatest permitted vertical incision below natural terrain.") - private int maxIncision = 48; - - @MinNumber(0.125) - @MaxNumber(16) - @Desc("The exponent shaping the channel-to-bank cross-section transition.") - private double bankExponent = 2D; - - @MinNumber(0) - @MaxNumber(16) - @Desc("The longitudinal transition length and maximum lateral and roof flare where a surface river enters or exits solid terrain.") - private double tunnelMouthBlend = 2D; - - @Desc("Noise modulating the submerged floor of river tunnels.") - private IrisGeneratorStyle tunnelFloorStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(48D); - - @Desc("The subterranean tunnel width multiplier relative to the surface river width.") - private IrisStyledRange tunnelWidthMultiplier = range(1D, 1D, NoiseStyle.FLAT, 1D); - - @MinNumber(0) - @MaxNumber(8) - @Desc("The maximum vertical floor variation in river tunnels.") - private double tunnelFloorVariation = 2D; - - @Desc("Noise modulating the dry roof of river tunnels.") - private IrisGeneratorStyle tunnelRoofStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(64D); - - @MinNumber(0) - @MaxNumber(16) - @Desc("The maximum vertical roof variation in river tunnels.") - private double tunnelRoofVariation = 3D; - - @Required - @ArrayType(min = 1, type = IrisRiverWorm.class) - @Desc("Weighted root Perlin-worm families with inherited child styles for trunks and tributaries.") - private KList worms = new KList(); - - @Desc("Modulates small river-bed height variation after the connected channel shape is solved.") - private IrisGeneratorStyle bedRoughnessStyle = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(96D); - - @MinNumber(0) - @MaxNumber(8) - @Desc("The maximum river-bed roughness in blocks.") - private double bedRoughness = 0.75D; - - @Desc("The behavior used when a graph route cannot continue as a wet channel.") - private IrisRiverTerminalMode terminalMode = IrisRiverTerminalMode.DRY_CHANNEL; - - @MinNumber(8) - @MaxNumber(1024) - @Desc("The distance in blocks over which a terminal channel returns to natural terrain.") - private int terminalTaper = 64; - - @MinNumber(0) - @MaxNumber(1) - @Desc("The probability that a failed wet route continues as a tapered dry channel.") - private double dryContinuationChance = 1D; - - private static IrisStyledRange range(double min, double max, NoiseStyle style, double zoom) { - return new IrisStyledRange(min, max, new IrisGeneratorStyle(style).zoomed(zoom)); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTopology.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTopology.java deleted file mode 100644 index 0a23e651c..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverTopology.java +++ /dev/null @@ -1,112 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Dimension-owned settings for the deterministic connected river graph.") -@Data -public class IrisRiverTopology { - @MinNumber(64) - @MaxNumber(4096) - @Desc("The routing-cell width in blocks. This controls graph identity and cannot be overridden by regions or biomes.") - private int cellSize = 512; - - @MinNumber(1) - @MaxNumber(64) - @Desc("The number of routing cells grouped into one immutable river cache tile.") - private int tileCells = 4; - - @MinNumber(0) - @MaxNumber(0.49) - @Desc("The fraction of a routing cell used to jitter its graph node away from the center.") - private double siteJitter = 0.35D; - - @MinNumber(1) - @MaxNumber(256) - @Desc("The maximum number of directed graph reaches followed by one source route.") - private int maxRouteReaches = 16; - - @MinNumber(0) - @MaxNumber(64) - @Desc("The minimum number of noise-weighted source nodes selected in each routing tile while source chance is above zero.") - private int minimumSourcesPerTile = 0; - - @MinNumber(0) - @MaxNumber(7) - @Desc("The number of alternate downstream reaches inspected before declaring a sink.") - private int sinkSearchReaches = 4; - - @MinNumber(8) - @MaxNumber(256) - @Desc("The spacing of deterministic drainage-basin sinks in routing cells. Larger values produce longer trunks and wider tributary trees.") - private int routingBasinCells = 64; - - @MinNumber(8) - @MaxNumber(256) - @Desc("The wavelength in routing cells of the smooth domain warp applied to drainage distance.") - private int routingDeviationScaleCells = 24; - - @MinNumber(0) - @MaxNumber(32) - @Desc("The maximum drainage-domain displacement in routing cells. Zero keeps straight radial basin gradients.") - private double routingDeviationStrengthCells = 0D; - - @MinNumber(1) - @MaxNumber(64) - @Desc("The horizontal basin-distance span in routing cells per one block of terraced water rise.") - private double routingPlateauHeight = 8D; - - @Desc("Selects complete river source routes at stable graph nodes.") - private IrisRiverNoiseChance source = new IrisRiverNoiseChance() - .setChance(0.05D) - .setStyle(new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(8192D)) - .setInfluence(0.035D); - - @Desc("Selects complete continuation reaches. A rejected reach terminates or suppresses its route rather than creating a gap.") - private IrisRiverNoiseChance continuation = new IrisRiverNoiseChance() - .setChance(0.99D) - .setStyle(new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(4096D)) - .setInfluence(0.01D); - - @Desc("Adds deterministic cost variation while choosing downstream graph neighbors.") - private IrisGeneratorStyle routingStyle = new IrisGeneratorStyle(NoiseStyle.VASCULAR).zoomed(8192D); - - @MinNumber(0) - @MaxNumber(1024) - @Desc("The maximum routing-cost contribution from routingStyle.") - private double routingNoiseWeight = 24D; - - @MinNumber(0) - @MaxNumber(1024) - @Desc("The penalty for choosing a downstream edge that does not follow the local routingStyle tangent.") - private double flowAlignmentWeight = 24D; - - @MinNumber(0) - @MaxNumber(1024) - @Desc("The deterministic attraction toward shared downstream nodes. Larger values form stronger tributary trees and confluences.") - private double confluenceWeight = 0D; - - @MinNumber(0) - @MaxNumber(16) - @Desc("The contribution of natural terrain height to downstream routing cost.") - private double terrainHeightWeight = 0.7D; - - @MinNumber(0) - @MaxNumber(16) - @Desc("The contribution of natural terrain slope to downstream routing cost.") - private double terrainSlopeWeight = 0.35D; - - @MinNumber(0) - @MaxNumber(16) - @Desc("The routing preference toward natural sea outlets.") - private double oceanAttraction = 1D; - - @Desc("Require every wet source route to reach natural sea or a proven sea-reaching trunk.") - private boolean requireOcean = false; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWater.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWater.java deleted file mode 100644 index c66d6d36a..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWater.java +++ /dev/null @@ -1,40 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("Controls the river water-surface solver.") -@Data -public class IrisRiverWater { - @Desc("The strategy used to determine river water-surface height.") - private IrisRiverWaterMode mode = IrisRiverWaterMode.FIXED; - - @MinNumber(-2048) - @MaxNumber(2048) - @Desc("The base river fluid surface in absolute world Y, independent of the dimension ocean height.") - private int fluidHeight = 63; - - @Desc("The river fluid palette used by surface channels, contained tunnels, grottos, and waterfall throats.") - private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water"); - - @MinNumber(8) - @MaxNumber(4096) - @Desc("The target length of each flat terraced pool in blocks.") - private int poolLength = 96; - - @MinNumber(0) - @MaxNumber(64) - @Desc("The greatest terraced river height permitted above fluidHeight.") - private int maximumPoolRise = 4; - - @MinNumber(1) - @MaxNumber(32) - @Desc("The vertical height of controlled drops between terraced pools.") - private int dropHeight = 1; -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWaterMode.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWaterMode.java deleted file mode 100644 index 5e61e92f8..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWaterMode.java +++ /dev/null @@ -1,12 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("Selects how a river determines its surface fluid height.") -public enum IrisRiverWaterMode { - @Desc("Use the river water configuration's fixed fluid height for every wet reach.") - FIXED, - - @Desc("Use flat pools connected by controlled vertical drops.") - TERRACED -} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWorm.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWorm.java deleted file mode 100644 index e00c0840c..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverWorm.java +++ /dev/null @@ -1,138 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.object.annotations.ArrayType; -import art.arcane.iris.engine.object.annotations.Desc; -import art.arcane.iris.engine.object.annotations.MaxNumber; -import art.arcane.iris.engine.object.annotations.MinNumber; -import art.arcane.iris.engine.object.annotations.Required; -import art.arcane.volmlib.util.collection.KList; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -@Accessors(chain = true) -@NoArgsConstructor -@Desc("One weighted Perlin-worm river shape and its channel proportions.") -@Data -public class IrisRiverWorm { - @Required - @Desc("Unique lowercase identifier for this root or child style.") - private String id = "river"; - - @Desc("Stable salt for this Perlin field pair.") - private long seed = 1L; - - @MinNumber(0.000001) - @MaxNumber(1000000) - @Desc("Relative probability when selecting this root family or one child transition.") - private double weight = 1D; - - @MinNumber(8) - @MaxNumber(16384) - @Desc("Primary gradient-Perlin wavelength in blocks.") - private double wavelength = 1024D; - - @MinNumber(8) - @MaxNumber(16384) - @Desc("Secondary gradient-Perlin wavelength in blocks.") - private double detailWavelength = 256D; - - @MinNumber(0) - @MaxNumber(1) - @Desc("Primary heading deviation as a fraction of 180 degrees.") - private double tortuosity = 0.5D; - - @MinNumber(0) - @MaxNumber(1) - @Desc("Secondary heading deviation as a fraction of 180 degrees.") - private double detailTortuosity = 0.15D; - - @MinNumber(0) - @MaxNumber(1024) - @Desc("Maximum endpoint-bridged displacement from the reach chord in blocks.") - private double maxOffset = 320D; - - @MinNumber(1) - @MaxNumber(64) - @Desc("Number of deterministic Perlin-worm steps used to resolve the reach.") - private int segments = 48; - - @MinNumber(0.125) - @MaxNumber(8) - @Desc("Channel-width multiplier for reaches selecting this worm.") - private double widthMultiplier = 1D; - - @MinNumber(0.125) - @MaxNumber(8) - @Desc("Bank-width multiplier for reaches selecting this worm.") - private double bankMultiplier = 1D; - - @MinNumber(0.125) - @MaxNumber(8) - @Desc("Depth multiplier for reaches selecting this worm.") - private double depthMultiplier = 1D; - - @MinNumber(8) - @MaxNumber(16384) - @Desc("Primary world-space wavelength controlling longitudinal body swelling and pinching.") - private double bodyWavelength = 512D; - - @MinNumber(8) - @MaxNumber(16384) - @Desc("Detail wavelength adding smaller changes to the longitudinal body profile.") - private double bodyDetailWavelength = 128D; - - @MinNumber(0) - @MaxNumber(1) - @Desc("Share of the longitudinal body field supplied by bodyDetailWavelength; the remainder uses bodyWavelength.") - private double bodyDetailInfluence = 0.3D; - - @MinNumber(0) - @MaxNumber(0.875) - @Desc("Maximum proportional channel-width variation along this style's body.") - private double widthVariation = 0D; - - @MinNumber(0) - @MaxNumber(0.875) - @Desc("Maximum proportional bank or basin-width variation along this style's body.") - private double bankVariation = 0D; - - @MinNumber(0) - @MaxNumber(0.875) - @Desc("Maximum proportional bed-depth variation along this style's body.") - private double depthVariation = 0D; - - @MinNumber(0) - @MaxNumber(0.875) - @Desc("Maximum downward variation of tunnel roof clearance without exceeding the authored cave headroom.") - private double roofVariation = 0D; - - @MinNumber(1) - @MaxNumber(8) - @Desc("Number of upstream children admitted before additional siblings decay probabilistically.") - private int branchCap = 4; - - @MinNumber(0) - @MaxNumber(1) - @Desc("Multiplicative survival probability for every sibling beyond branchCap.") - private double branchDecay = 0.35D; - - @MinNumber(0) - @MaxNumber(8) - @Desc("Multiplier applied to the dimension confluence attraction for this style.") - private double confluenceMultiplier = 1D; - - @MinNumber(0) - @MaxNumber(1) - @Desc("Chance that an upstream continuation mutates from this style into one weighted child.") - private double childChance = 0D; - - @MinNumber(0) - @MaxNumber(1) - @Desc("Additional child-mutation chance for each sibling slot beyond the primary branch.") - private double branchChildChance = 0D; - - @ArrayType(type = IrisRiverWorm.class) - @Desc("Weighted descendant styles inherited by the complete upstream lineage after mutation.") - private KList children = new KList(); -} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index d5640e4dd..533e930db 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -311,14 +311,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun } private int resolveInitialSpawnY(World world, Location initialSpawn, int minY, int maxY) { - Engine activeEngine = engine; - if (activeEngine != null && activeEngine.getComplex() != null && activeEngine.getComplex().getHeightStream() != null) { - int generatedY = activeEngine.getMinHeight() - + activeEngine.getComplex().getHeightStream().get(initialSpawn.getX(), initialSpawn.getZ()).intValue() - + 1; - return Math.max(minY, Math.min(maxY, generatedY)); - } - return Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn) + 1)); } diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverAnchor.java b/core/src/main/java/art/arcane/iris/engine/river/RiverAnchor.java deleted file mode 100644 index 80324dd24..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverAnchor.java +++ /dev/null @@ -1,27 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.Objects; - -public record RiverAnchor( - RiverEdgeId reachId, - int index, - long stableId, - double samplingSpacing, - long samplingSalt, - double x, - double z, - double alongReach, - RiverRouteState state, - int flow, - int order -) { - public RiverAnchor { - Objects.requireNonNull(reachId); - Objects.requireNonNull(state); - if (index < 0 || !Double.isFinite(samplingSpacing) || samplingSpacing <= 0D - || !Double.isFinite(x) || !Double.isFinite(z) - || !Double.isFinite(alongReach) || alongReach < 0.0 || alongReach > 1.0) { - throw new IllegalArgumentException("River anchor index and coordinates must be valid"); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverBodyProfile.java b/core/src/main/java/art/arcane/iris/engine/river/RiverBodyProfile.java deleted file mode 100644 index 071a162ce..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverBodyProfile.java +++ /dev/null @@ -1,191 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.Arrays; - -public final class RiverBodyProfile { - private final double[] positions; - private final double[] widths; - private final double[] bankWidths; - private final double[] depths; - private final double[] roofScales; - private final double maximumWidth; - private final double maximumBankWidth; - private final double maximumDepth; - - public RiverBodyProfile( - double[] positions, - double[] widths, - double[] bankWidths, - double[] depths, - double[] roofScales - ) { - if (positions == null || widths == null || bankWidths == null || depths == null || roofScales == null - || positions.length < 2 - || positions.length != widths.length - || positions.length != bankWidths.length - || positions.length != depths.length - || positions.length != roofScales.length) { - throw new IllegalArgumentException("River body profiles require matching dimension samples"); - } - this.positions = positions.clone(); - this.widths = widths.clone(); - this.bankWidths = bankWidths.clone(); - this.depths = depths.clone(); - this.roofScales = roofScales.clone(); - double resolvedMaximumWidth = 0D; - double resolvedMaximumBankWidth = 0D; - double resolvedMaximumDepth = 0D; - for (int index = 0; index < this.positions.length; index++) { - double position = this.positions[index]; - if (!Double.isFinite(position) || position < 0D || position > 1D - || index > 0 && position <= this.positions[index - 1]) { - throw new IllegalArgumentException("River body profile positions must increase from zero to one"); - } - requirePositive(this.widths[index], "width"); - requireNonNegative(this.bankWidths[index], "bank width"); - requirePositive(this.depths[index], "depth"); - requireUnitScale(this.roofScales[index], "roof scale"); - resolvedMaximumWidth = StrictMath.max(resolvedMaximumWidth, this.widths[index]); - resolvedMaximumBankWidth = StrictMath.max(resolvedMaximumBankWidth, this.bankWidths[index]); - resolvedMaximumDepth = StrictMath.max(resolvedMaximumDepth, this.depths[index]); - } - if (this.positions[0] != 0D || this.positions[this.positions.length - 1] != 1D) { - throw new IllegalArgumentException("River body profile positions must include zero and one"); - } - maximumWidth = resolvedMaximumWidth; - maximumBankWidth = resolvedMaximumBankWidth; - maximumDepth = resolvedMaximumDepth; - } - - public static RiverBodyProfile constant(double width, double bankWidth, double depth) { - return new RiverBodyProfile( - new double[]{0D, 1D}, - new double[]{width, width}, - new double[]{bankWidth, bankWidth}, - new double[]{depth, depth}, - new double[]{1D, 1D} - ); - } - - public double width(double alongReach) { - return sample(widths, alongReach); - } - - public double bankWidth(double alongReach) { - return sample(bankWidths, alongReach); - } - - public double depth(double alongReach) { - return sample(depths, alongReach); - } - - public double roofScale(double alongReach) { - return sample(roofScales, alongReach); - } - - public double maximumWidth() { - return maximumWidth; - } - - public double maximumBankWidth() { - return maximumBankWidth; - } - - public double maximumDepth() { - return maximumDepth; - } - - public int size() { - return positions.length; - } - - public double position(int index) { - return positions[index]; - } - - public double widthAtIndex(int index) { - return widths[index]; - } - - public double bankWidthAtIndex(int index) { - return bankWidths[index]; - } - - public double depthAtIndex(int index) { - return depths[index]; - } - - public double roofScaleAtIndex(int index) { - return roofScales[index]; - } - - public int intervalIndex(double alongReach) { - double position = StrictMath.max(0D, StrictMath.min(1D, alongReach)); - int index = Arrays.binarySearch(positions, position); - if (index >= 0) { - return StrictMath.min(index, positions.length - 2); - } - return StrictMath.max(0, StrictMath.min(-index - 2, positions.length - 2)); - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (!(object instanceof RiverBodyProfile profile)) { - return false; - } - return Arrays.equals(positions, profile.positions) - && Arrays.equals(widths, profile.widths) - && Arrays.equals(bankWidths, profile.bankWidths) - && Arrays.equals(depths, profile.depths) - && Arrays.equals(roofScales, profile.roofScales); - } - - @Override - public int hashCode() { - int hash = Arrays.hashCode(positions); - hash = 31 * hash + Arrays.hashCode(widths); - hash = 31 * hash + Arrays.hashCode(bankWidths); - hash = 31 * hash + Arrays.hashCode(depths); - return 31 * hash + Arrays.hashCode(roofScales); - } - - private double sample(double[] values, double alongReach) { - double position = StrictMath.max(0D, StrictMath.min(1D, alongReach)); - int index = Arrays.binarySearch(positions, position); - if (index >= 0) { - return values[index]; - } - int upper = -index - 1; - if (upper <= 0) { - return values[0]; - } - if (upper >= positions.length) { - return values[values.length - 1]; - } - int lower = upper - 1; - double range = positions[upper] - positions[lower]; - double interpolation = range <= 0D ? 0D : (position - positions[lower]) / range; - return values[lower] + (values[upper] - values[lower]) * interpolation; - } - - private static void requirePositive(double value, String name) { - if (!Double.isFinite(value) || value <= 0D) { - throw new IllegalArgumentException("River body profile " + name + " must be finite and positive"); - } - } - - private static void requireNonNegative(double value, String name) { - if (!Double.isFinite(value) || value < 0D) { - throw new IllegalArgumentException("River body profile " + name + " must be finite and non-negative"); - } - } - - private static void requireUnitScale(double value, String name) { - if (!Double.isFinite(value) || value <= 0D || value > 1D) { - throw new IllegalArgumentException("River body profile " + name + " must be greater than zero and at most one"); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverEdgeId.java b/core/src/main/java/art/arcane/iris/engine/river/RiverEdgeId.java deleted file mode 100644 index 9b25e5957..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverEdgeId.java +++ /dev/null @@ -1,34 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.Objects; - -public record RiverEdgeId(RiverNodeId first, RiverNodeId second) implements Comparable { - public RiverEdgeId { - Objects.requireNonNull(first); - Objects.requireNonNull(second); - if (first.compareTo(second) >= 0) { - throw new IllegalArgumentException("River edge endpoints must be distinct and canonical"); - } - } - - public static RiverEdgeId of(RiverNodeId first, RiverNodeId second) { - Objects.requireNonNull(first); - Objects.requireNonNull(second); - if (first.equals(second)) { - throw new IllegalArgumentException("River edge endpoints must be distinct"); - } - return first.compareTo(second) < 0 - ? new RiverEdgeId(first, second) - : new RiverEdgeId(second, first); - } - - public long stableId() { - return RiverNetwork.mix(first.stableId() ^ Long.rotateLeft(second.stableId(), 29)); - } - - @Override - public int compareTo(RiverEdgeId other) { - int firstComparison = first.compareTo(other.first); - return firstComparison != 0 ? firstComparison : second.compareTo(other.second); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverNetwork.java b/core/src/main/java/art/arcane/iris/engine/river/RiverNetwork.java deleted file mode 100644 index 9cfeb7723..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverNetwork.java +++ /dev/null @@ -1,1343 +0,0 @@ -package art.arcane.iris.engine.river; - -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; - -public final class RiverNetwork { - private static final long NODE_X_SALT = 0x6A09E667F3BCC909L; - private static final long NODE_Z_SALT = 0xBB67AE8584CAA73BL; - private static final long NODE_RANK_SALT = 0x3C6EF372FE94F82BL; - private static final long BASIN_X_SALT = 0xCBBB9D5DC1059ED8L; - private static final long BASIN_Z_SALT = 0x629A292A367CD507L; - private static final long BASIN_DEVIATION_X_SALT = 0xA4093822299F31D0L; - private static final long BASIN_DEVIATION_Z_SALT = 0x082EFA98EC4E6C89L; - private static final long DIAGONAL_SALT = 0xA54FF53A5F1D36F1L; - private static final long SOURCE_SALT = 0x510E527FADE682D1L; - private static final long SOURCE_FLOOR_SALT = 0xD6E8FEB86659FD93L; - private static final long REACH_SALT = 0x9B05688C2B3E6C1FL; - private static final long DRY_SALT = 0x1F83D9ABFB41BD6BL; - private static final long WORM_FAMILY_SALT = 0x5BE0CD19137E2179L; - private static final long WORM_CHILD_GATE_SALT = 0x452821E638D01377L; - private static final long WORM_CHILD_SELECTION_SALT = 0xBE5466CF34E90C6CL; - private static final long WORM_PRIMARY_SALT = 0x243F6A8885A308D3L; - private static final long WORM_DETAIL_SALT = 0x13198A2E03707344L; - private static final long BODY_WIDTH_PRIMARY_SALT = 0xA4093822299F31D0L; - private static final long BODY_WIDTH_DETAIL_SALT = 0x082EFA98EC4E6C89L; - private static final long BODY_BANK_PRIMARY_SALT = 0x452821E638D01377L; - private static final long BODY_BANK_DETAIL_SALT = 0xBE5466CF34E90C6CL; - private static final long BODY_DEPTH_PRIMARY_SALT = 0xC0AC29B7C97C50DDL; - private static final long BODY_DEPTH_DETAIL_SALT = 0x3F84D5B5B5470917L; - private static final long BODY_ROOF_PRIMARY_SALT = 0xD1310BA698DFB5ACL; - private static final long BODY_ROOF_DETAIL_SALT = 0x2FFD72DBD01ADFB7L; - private static final long CONFLUENCE_SALT = 0x9E3779B97F4A7C15L; - private static final long BRANCH_SLOT_SALT = 0x94D049BB133111EBL; - private static final long BRANCH_GATE_SALT = 0x2545F4914F6CDD1DL; - private static final int MINIMUM_BODY_PROFILE_SAMPLES = 12; - private static final int MAXIMUM_BODY_PROFILE_SAMPLES = 512; - private static final double PERLIN_NORMALIZATION = 1.4142135623730951D; - - private final RiverNetworkOptions options; - - public RiverNetwork(RiverNetworkOptions options) { - this.options = Objects.requireNonNull(options); - } - - public RiverNetworkOptions options() { - return options; - } - - public RiverNode nodeAtCell(long cellX, long cellZ, RiverTerrainSampler terrain) { - return createNode(new RiverNodeId(cellX, cellZ), Objects.requireNonNull(terrain)); - } - - public RiverNode nodeAtWorld(int blockX, int blockZ, RiverTerrainSampler terrain) { - long cellX = Math.floorDiv(blockX, options.cellSize()); - long cellZ = Math.floorDiv(blockZ, options.cellSize()); - return nodeAtCell(cellX, cellZ, terrain); - } - - public int tileXForBlock(int blockX) { - return Math.floorDiv(blockX, options.cellSize() * options.tileCells()); - } - - public int tileZForBlock(int blockZ) { - return Math.floorDiv(blockZ, options.cellSize() * options.tileCells()); - } - - public RiverTile buildTileForBlock(int blockX, int blockZ, RiverTerrainSampler terrain) { - return buildTile(tileXForBlock(blockX), tileZForBlock(blockZ), terrain); - } - - public RiverSample sample(int blockX, int blockZ, RiverTerrainSampler terrain) { - return buildTileForBlock(blockX, blockZ, terrain).sample(blockX, blockZ); - } - - public List neighbors(RiverNodeId id) { - Objects.requireNonNull(id); - ArrayList neighbors = new ArrayList<>(8); - addUnique(neighbors, new RiverNodeId(id.cellX() - 1L, id.cellZ())); - addUnique(neighbors, new RiverNodeId(id.cellX() + 1L, id.cellZ())); - addUnique(neighbors, new RiverNodeId(id.cellX(), id.cellZ() - 1L)); - addUnique(neighbors, new RiverNodeId(id.cellX(), id.cellZ() + 1L)); - for (long squareX = id.cellX() - 1L; squareX <= id.cellX(); squareX++) { - for (long squareZ = id.cellZ() - 1L; squareZ <= id.cellZ(); squareZ++) { - RiverNodeId diagonal = diagonalNeighbor(id, squareX, squareZ); - if (diagonal != null) { - addUnique(neighbors, diagonal); - } - } - } - neighbors.sort(Comparator.naturalOrder()); - return List.copyOf(neighbors); - } - - public RiverNode downstream(RiverNodeId id, RiverTerrainSampler terrain) { - Objects.requireNonNull(id); - Objects.requireNonNull(terrain); - NodeResolver resolver = new NodeResolver(terrain); - RiverNode node = resolver.resolve(id); - List candidates = resolver.downstreamCandidates(node); - for (RiverNode candidate : candidates) { - RiverRoutingContext context = resolver.routingContext(node, candidate); - if (!resolver.reachFeasible(context)) { - continue; - } - return resolver.continuationPermitted(context) ? candidate : null; - } - return null; - } - - public List downstreamCandidates(RiverNodeId id, RiverTerrainSampler terrain) { - Objects.requireNonNull(id); - Objects.requireNonNull(terrain); - NodeResolver resolver = new NodeResolver(terrain); - return resolver.downstreamCandidates(resolver.resolve(id)); - } - - public RiverRoute trace(RiverNodeId source, RiverTerrainSampler terrain) { - Objects.requireNonNull(source); - Objects.requireNonNull(terrain); - return trace(source, new NodeResolver(terrain)); - } - - public RiverTile buildTile(int tileX, int tileZ, RiverTerrainSampler terrain) { - Objects.requireNonNull(terrain); - long tileWorldSize = (long) options.cellSize() * options.tileCells(); - long minimumX = (long) tileX * tileWorldSize; - long minimumZ = (long) tileZ * tileWorldSize; - long maximumX = minimumX + tileWorldSize; - long maximumZ = minimumZ + tileWorldSize; - requireWorldBounds(minimumX, minimumZ, maximumX, maximumZ); - - int geometryPadding = geometryPaddingCells(); - long targetMinimumCellX = (long) tileX * options.tileCells() - geometryPadding; - long targetMinimumCellZ = (long) tileZ * options.tileCells() - geometryPadding; - long targetMaximumCellX = (long) (tileX + 1) * options.tileCells() - 1L + geometryPadding; - long targetMaximumCellZ = (long) (tileZ + 1) * options.tileCells() - 1L + geometryPadding; - long sourceMinimumCellX = targetMinimumCellX - options.maxRouteReaches(); - long sourceMinimumCellZ = targetMinimumCellZ - options.maxRouteReaches(); - long sourceMaximumCellX = targetMaximumCellX + options.maxRouteReaches(); - long sourceMaximumCellZ = targetMaximumCellZ + options.maxRouteReaches(); - - NodeResolver resolver = new NodeResolver(terrain); - int sourceWidth = Math.toIntExact(sourceMaximumCellX - sourceMinimumCellX + 1L); - int sourceDepth = Math.toIntExact(sourceMaximumCellZ - sourceMinimumCellZ + 1L); - int sourceCount = Math.multiplyExact(sourceWidth, sourceDepth); - ArrayList routes = new ArrayList<>(sourceCount); - for (long cellX = sourceMinimumCellX; cellX <= sourceMaximumCellX; cellX++) { - for (long cellZ = sourceMinimumCellZ; cellZ <= sourceMaximumCellZ; cellZ++) { - routes.add(trace(new RiverNodeId(cellX, cellZ), resolver)); - } - } - LinkedHashMap accumulators = new LinkedHashMap<>(); - for (RiverRoute route : routes) { - accumulate(route, resolver, accumulators); - } - - ArrayList reaches = new ArrayList<>(accumulators.size()); - for (ReachAccumulator accumulator : accumulators.values()) { - if (!potentiallyIntersects( - accumulator.from, - accumulator.to, - minimumX, - minimumZ, - maximumX, - maximumZ - )) { - continue; - } - RiverReach reach = accumulator.build(); - if (intersects(reach, minimumX, minimumZ, maximumX, maximumZ)) { - reaches.add(reach); - } - } - reaches.sort(Comparator.comparing(RiverReach::id)); - return new RiverTile( - tileX, - tileZ, - (int) minimumX, - (int) minimumZ, - (int) maximumX, - (int) maximumZ, - reaches - ); - } - - public static long mix(long value) { - long mixed = value; - mixed ^= mixed >>> 30; - mixed *= 0xBF58476D1CE4E5B9L; - mixed ^= mixed >>> 27; - mixed *= 0x94D049BB133111EBL; - mixed ^= mixed >>> 31; - return mixed; - } - - private RiverNode createNode(RiverNodeId id, RiverTerrainSampler terrain) { - NodePosition position = nodePosition(id); - double x = position.x(); - double z = position.z(); - int blockX = position.blockX(); - int blockZ = position.blockZ(); - RiverTerrainNodeSample terrainSample = terrain.sampleNode(blockX, blockZ); - double naturalHeight = finiteOrZero(terrainSample.naturalHeight()); - boolean ocean = terrainSample.ocean(); - boolean riverAllowed = terrainSample.riverAllowed(); - double routingNoise = centered(hash(id, NODE_RANK_SALT)); - double routingScore = naturalHeight * options.terrainHeightWeight() - + routingNoise * options.routingNoiseWeight() - + finiteOrZero(terrainSample.routingCost()); - double drainageDistance = drainageBasin(id).distance(); - double hydraulicHeight = ocean - ? options.hydraulicBaseHeight() - : options.hydraulicBaseHeight() - + StrictMath.floor(drainageDistance / options.routingPlateauHeight()); - double rank = ocean ? -Double.MAX_VALUE : drainageDistance; - return new RiverNode( - id, - x, - z, - naturalHeight, - hydraulicHeight, - rank, - finiteOrZero(routingScore), - ocean, - riverAllowed - ); - } - - private double drainageDistance(RiverNodeId id) { - return drainageBasin(id).distance(); - } - - private DrainageBasin drainageBasin(RiverNodeId id) { - int basinCells = options.routingBasinCells(); - double nodeX = id.cellX() + 0.5D; - double nodeZ = id.cellZ() + 0.5D; - double deviationStrength = options.routingDeviationStrengthCells(); - if (deviationStrength > 0D) { - int deviationScale = options.routingDeviationScaleCells(); - double originalX = nodeX; - double originalZ = nodeZ; - nodeX += smoothCellNoise(originalX, originalZ, deviationScale, BASIN_DEVIATION_X_SALT) - * deviationStrength; - nodeZ += smoothCellNoise(originalX, originalZ, deviationScale, BASIN_DEVIATION_Z_SALT) - * deviationStrength; - } - long basinX = (long) StrictMath.floor(nodeX / basinCells); - long basinZ = (long) StrictMath.floor(nodeZ / basinCells); - double jitterRadius = basinCells * 0.45D; - double nearestDistance = Double.MAX_VALUE; - DrainageBasinId nearestId = null; - for (long candidateX = basinX - 1L; candidateX <= basinX + 1L; candidateX++) { - for (long candidateZ = basinZ - 1L; candidateZ <= basinZ + 1L; candidateZ++) { - double siteX = (candidateX + 0.5D) * basinCells - + centered(hash(candidateX, candidateZ, BASIN_X_SALT)) * jitterRadius; - double siteZ = (candidateZ + 0.5D) * basinCells - + centered(hash(candidateX, candidateZ, BASIN_Z_SALT)) * jitterRadius; - double distance = StrictMath.hypot(nodeX - siteX, nodeZ - siteZ); - if (distance < nearestDistance) { - nearestDistance = distance; - nearestId = new DrainageBasinId(candidateX, candidateZ); - } - } - } - return new DrainageBasin(nearestId, nearestDistance); - } - - private double smoothCellNoise(double x, double z, int scale, long salt) { - double scaledX = x / scale; - double scaledZ = z / scale; - long minimumX = (long) StrictMath.floor(scaledX); - long minimumZ = (long) StrictMath.floor(scaledZ); - double fractionX = scaledX - minimumX; - double fractionZ = scaledZ - minimumZ; - double fadeX = fractionX * fractionX * (3D - 2D * fractionX); - double fadeZ = fractionZ * fractionZ * (3D - 2D * fractionZ); - double northwest = centered(hash(minimumX, minimumZ, salt)); - double northeast = centered(hash(minimumX + 1L, minimumZ, salt)); - double southwest = centered(hash(minimumX, minimumZ + 1L, salt)); - double southeast = centered(hash(minimumX + 1L, minimumZ + 1L, salt)); - double north = northwest + (northeast - northwest) * fadeX; - double south = southwest + (southeast - southwest) * fadeX; - return north + (south - north) * fadeZ; - } - - private NodePosition nodePosition(RiverNodeId id) { - double centerX = ((double) id.cellX() + 0.5D) * options.cellSize(); - double centerZ = ((double) id.cellZ() + 0.5D) * options.cellSize(); - double jitterRadius = options.siteJitter() * options.cellSize() * 0.5D; - double x = centerX + centered(hash(id, NODE_X_SALT)) * jitterRadius; - double z = centerZ + centered(hash(id, NODE_Z_SALT)) * jitterRadius; - return new NodePosition( - x, - z, - clampToInt(StrictMath.round(x)), - clampToInt(StrictMath.round(z)) - ); - } - - private List computeDownstreamCandidates(RiverNode node, NodeResolver resolver) { - if (node.ocean() || !node.riverAllowed()) { - return List.of(); - } - ArrayList ranked = new ArrayList<>(8); - for (RiverNodeId neighborId : neighbors(node.id())) { - RiverNode neighbor = resolver.resolve(neighborId); - if (!neighbor.riverAllowed()) { - continue; - } - if (compareRank(neighbor, node) >= 0) { - continue; - } - RiverWorm worm = resolver.worm(node, neighbor); - if (!branchPermitted(node, neighbor, resolver, worm)) { - continue; - } - RiverRoutingContext context = resolver.routingContext(node, neighbor); - double routingCost = finiteNonNegative(resolver.terrain.reachRoutingCost(context)); - double oceanAttraction = neighbor.ocean() ? options.oceanAttraction() : 0.0; - double flowAlignmentCost = flowAlignmentCost(node, neighbor, resolver); - double confluenceAttraction = unit(hash(neighbor.id(), CONFLUENCE_SALT)) - * options.confluenceWeight() - * worm.confluenceMultiplier(); - ranked.add(new RankedCandidate( - neighbor, - neighbor.routingScore() + routingCost + flowAlignmentCost - - oceanAttraction - confluenceAttraction - )); - } - ranked.sort((first, second) -> { - int costComparison = Double.compare(first.cost(), second.cost()); - return costComparison != 0 ? costComparison : compareRank(first.node(), second.node()); - }); - ArrayList candidates = new ArrayList<>(ranked.size()); - for (RankedCandidate candidate : ranked) { - candidates.add(candidate.node()); - } - return List.copyOf(candidates); - } - - private boolean branchPermitted( - RiverNode child, - RiverNode parent, - NodeResolver resolver, - RiverWorm worm - ) { - RiverEdgeId childEdge = RiverEdgeId.of(child.id(), parent.id()); - int childSlot = resolver.branchSlot(parent, child); - if (childSlot < worm.branchCap()) { - return true; - } - double survivalChance = 1D; - for (int overflow = worm.branchCap(); overflow <= childSlot; overflow++) { - survivalChance *= worm.branchDecay(); - } - return gate(hash(childEdge, BRANCH_GATE_SALT), survivalChance); - } - - private RiverRoute trace(RiverNodeId sourceId, NodeResolver resolver) { - if (!resolver.sourcePermitted(sourceId)) { - return new RiverRoute(sourceId, RiverRouteState.SUPPRESSED, List.of(), false, false); - } - RiverNode source = resolver.resolve(sourceId); - - ArrayList edges = new ArrayList<>(options.maxRouteReaches()); - RiverNode current = source; - boolean reachedOcean = false; - boolean exhaustedHorizon = true; - for (int reachIndex = 0; reachIndex < options.maxRouteReaches(); reachIndex++) { - RiverNode next = null; - int examined = 0; - for (RiverNode candidate : resolver.downstreamCandidates(current)) { - if (examined >= options.downstreamCandidateLimit()) { - break; - } - examined++; - RiverRoutingContext context = resolver.routingContext(current, candidate); - if (resolver.reachFeasible(context)) { - next = candidate; - break; - } - } - if (next == null) { - exhaustedHorizon = false; - break; - } - RiverEdgeId edgeId = RiverEdgeId.of(current.id(), next.id()); - if (!resolver.continuationPermitted(resolver.routingContext(current, next))) { - exhaustedHorizon = false; - break; - } - edges.add(edgeId); - current = next; - if (current.ocean()) { - reachedOcean = true; - break; - } - } - - if (reachedOcean) { - return new RiverRoute(sourceId, RiverRouteState.WET, edges, true, false); - } - if (exhaustedHorizon && !options.requireOcean()) { - return new RiverRoute(sourceId, RiverRouteState.WET, edges, false, false); - } - if (!edges.isEmpty()) { - RiverTerminalPolicy terminalPolicy = resolver.terminalPolicy(current); - if (terminalPolicy == RiverTerminalPolicy.WET - || (terminalPolicy == RiverTerminalPolicy.INHERIT && !options.requireOcean())) { - return new RiverRoute(sourceId, RiverRouteState.WET, edges, false, true); - } - if ((terminalPolicy == RiverTerminalPolicy.DRY - || terminalPolicy == RiverTerminalPolicy.INHERIT) - && resolver.dryPermitted(sourceId)) { - return new RiverRoute(sourceId, RiverRouteState.DRY, edges, false, true); - } - } - return new RiverRoute(sourceId, RiverRouteState.SUPPRESSED, List.of(), false, false); - } - - private void accumulate( - RiverRoute route, - NodeResolver resolver, - Map accumulators - ) { - if (route.state() == RiverRouteState.SUPPRESSED) { - return; - } - for (int edgeIndex = 0; edgeIndex < route.edges().size(); edgeIndex++) { - RiverEdgeId edgeId = route.edges().get(edgeIndex); - ReachAccumulator accumulator = accumulators.get(edgeId); - if (accumulator == null) { - RiverNode first = resolver.resolve(edgeId.first()); - RiverNode second = resolver.resolve(edgeId.second()); - RiverNode from = compareRank(first, second) > 0 ? first : second; - RiverNode to = from == first ? second : first; - accumulator = new ReachAccumulator( - edgeId, - from, - to, - resolver.routingContext(from, to), - resolver.worm(from, to), - resolver.terrain - ); - accumulators.put(edgeId, accumulator); - } - boolean terminal = route.terminal() && edgeIndex == route.edges().size() - 1; - accumulator.add(route.state(), terminal); - } - } - - private RiverNodeId diagonalNeighbor(RiverNodeId id, long squareX, long squareZ) { - boolean ascending = (hash(squareX, squareZ, DIAGONAL_SALT) & 1L) == 0L; - RiverNodeId first = ascending - ? new RiverNodeId(squareX, squareZ) - : new RiverNodeId(squareX, squareZ + 1L); - RiverNodeId second = ascending - ? new RiverNodeId(squareX + 1L, squareZ + 1L) - : new RiverNodeId(squareX + 1L, squareZ); - if (id.equals(first)) { - return second; - } - return id.equals(second) ? first : null; - } - - private RiverPolyline createPolyline( - RiverNode from, - RiverNode to, - RiverWorm worm - ) { - int pointCount = worm.segments() + 1; - double[] rawX = new double[pointCount]; - double[] rawZ = new double[pointCount]; - double deltaX = to.x() - from.x(); - double deltaZ = to.z() - from.z(); - double length = StrictMath.hypot(deltaX, deltaZ); - rawX[0] = from.x(); - rawZ[0] = from.z(); - if (length <= 0D) { - return new RiverPolyline(rawX, rawZ); - } - double baseHeading = StrictMath.atan2(deltaZ, deltaX); - double stepLength = length / worm.segments(); - for (int point = 1; point < pointCount; point++) { - double x = rawX[point - 1]; - double z = rawZ[point - 1]; - double primary = perlin(x, z, worm.wavelength(), worm.seed() ^ WORM_PRIMARY_SALT); - double detail = perlin(x, z, worm.detailWavelength(), worm.seed() ^ WORM_DETAIL_SALT); - double heading = baseHeading + StrictMath.PI * ( - primary * worm.tortuosity() - + detail * worm.detailTortuosity() - ); - rawX[point] = x + StrictMath.cos(heading) * stepLength; - rawZ[point] = z + StrictMath.sin(heading) * stepLength; - } - double[] x = new double[pointCount]; - double[] z = new double[pointCount]; - double rawDeltaX = rawX[pointCount - 1] - from.x(); - double rawDeltaZ = rawZ[pointCount - 1] - from.z(); - double maximumDisplacement = 0D; - for (int point = 0; point < pointCount; point++) { - double t = (double) point / (pointCount - 1); - double tSquared = t * t; - double envelope = 16D * tSquared * (1D - t) * (1D - t); - double straightX = from.x() + deltaX * t; - double straightZ = from.z() + deltaZ * t; - double rawBridgeX = rawX[point] - (from.x() + rawDeltaX * t); - double rawBridgeZ = rawZ[point] - (from.z() + rawDeltaZ * t); - x[point] = straightX + rawBridgeX * envelope; - z[point] = straightZ + rawBridgeZ * envelope; - maximumDisplacement = StrictMath.max( - maximumDisplacement, - StrictMath.hypot(x[point] - straightX, z[point] - straightZ) - ); - } - double maximumOffset = StrictMath.min(worm.maxOffset(), length * 0.35D); - if (maximumDisplacement > maximumOffset && maximumDisplacement > 0D) { - double scale = maximumOffset / maximumDisplacement; - for (int point = 1; point < pointCount - 1; point++) { - double t = (double) point / (pointCount - 1); - double straightX = from.x() + deltaX * t; - double straightZ = from.z() + deltaZ * t; - x[point] = straightX + (x[point] - straightX) * scale; - z[point] = straightZ + (z[point] - straightZ) * scale; - } - } - x[0] = from.x(); - z[0] = from.z(); - x[pointCount - 1] = to.x(); - z[pointCount - 1] = to.z(); - return new RiverPolyline(x, z); - } - - private RiverWorm rootWormFor(RiverNodeId rootId) { - return selectWeighted(options.worms(), hash(rootId, WORM_FAMILY_SALT)); - } - - private RiverWorm childWormFor( - RiverWorm parent, - RiverNodeId parentId, - RiverNodeId childId, - int branchSlot - ) { - if (parent.children().isEmpty()) { - return parent; - } - double chance = StrictMath.min( - 1D, - parent.childChance() - + StrictMath.min(7, branchSlot) * parent.branchChildChance() - ); - RiverEdgeId edgeId = RiverEdgeId.of(childId, parentId); - if (!gate(hash(edgeId, WORM_CHILD_GATE_SALT), chance)) { - return parent; - } - return selectWeighted(parent.children(), hash(edgeId, WORM_CHILD_SELECTION_SALT)); - } - - private RiverWorm selectWeighted(List worms, long selectionHash) { - double totalWeight = 0D; - for (RiverWorm worm : worms) { - totalWeight += worm.weight(); - } - double selection = unit(selectionHash) * totalWeight; - double cumulative = 0D; - for (RiverWorm worm : worms) { - cumulative += worm.weight(); - if (selection < cumulative) { - return worm; - } - } - return worms.get(worms.size() - 1); - } - - private double perlin(double x, double z, double wavelength, long salt) { - double scaledX = x / wavelength; - double scaledZ = z / wavelength; - long minimumX = (long) StrictMath.floor(scaledX); - long minimumZ = (long) StrictMath.floor(scaledZ); - double fractionX = scaledX - minimumX; - double fractionZ = scaledZ - minimumZ; - double fadeX = perlinFade(fractionX); - double fadeZ = perlinFade(fractionZ); - double northwest = perlinGradient(minimumX, minimumZ, fractionX, fractionZ, salt); - double northeast = perlinGradient(minimumX + 1L, minimumZ, fractionX - 1D, fractionZ, salt); - double southwest = perlinGradient(minimumX, minimumZ + 1L, fractionX, fractionZ - 1D, salt); - double southeast = perlinGradient( - minimumX + 1L, - minimumZ + 1L, - fractionX - 1D, - fractionZ - 1D, - salt - ); - double north = northwest + (northeast - northwest) * fadeX; - double south = southwest + (southeast - southwest) * fadeX; - return StrictMath.max( - -1D, - StrictMath.min(1D, (north + (south - north) * fadeZ) * PERLIN_NORMALIZATION) - ); - } - - private double perlinGradient(long latticeX, long latticeZ, double x, double z, long salt) { - return switch ((int) (hash(latticeX, latticeZ, salt) & 7L)) { - case 0 -> x; - case 1 -> -x; - case 2 -> z; - case 3 -> -z; - case 4 -> (x + z) / PERLIN_NORMALIZATION; - case 5 -> (-x + z) / PERLIN_NORMALIZATION; - case 6 -> (x - z) / PERLIN_NORMALIZATION; - default -> (-x - z) / PERLIN_NORMALIZATION; - }; - } - - private double perlinFade(double value) { - double squared = value * value; - double cubed = squared * value; - return cubed * (value * (value * 6D - 15D) + 10D); - } - - private double bodyMultiplier( - ReachPosition position, - RiverWorm worm, - long primarySalt, - long detailSalt, - double variation - ) { - if (variation <= 0D) { - return 1D; - } - return StrictMath.max( - 0.125D, - 1D + bodyField(position, worm, primarySalt, detailSalt) * variation - ); - } - - private double roofScale(ReachPosition position, RiverWorm worm) { - if (worm.roofVariation() <= 0D) { - return 1D; - } - double normalized = bodyField( - position, - worm, - BODY_ROOF_PRIMARY_SALT, - BODY_ROOF_DETAIL_SALT - ) * 0.5D + 0.5D; - return StrictMath.max(0.125D, 1D - normalized * worm.roofVariation()); - } - - private double bodyField( - ReachPosition position, - RiverWorm worm, - long primarySalt, - long detailSalt - ) { - double primary = perlin( - position.x(), - position.z(), - worm.bodyWavelength(), - worm.seed() ^ primarySalt - ); - double detail = perlin( - position.x(), - position.z(), - worm.bodyDetailWavelength(), - worm.seed() ^ detailSalt - ); - double detailInfluence = worm.bodyDetailInfluence(); - return primary * (1D - detailInfluence) + detail * detailInfluence; - } - - private FlowTangent resolveFlowTangent(RiverNode node, RiverTerrainSampler terrain) { - double spacing = StrictMath.max(1D, options.cellSize() * 0.5D); - double left = terrain.flowNoise(node.x() - spacing, node.z()); - double right = terrain.flowNoise(node.x() + spacing, node.z()); - double top = terrain.flowNoise(node.x(), node.z() - spacing); - double bottom = terrain.flowNoise(node.x(), node.z() + spacing); - if (!Double.isFinite(left) || !Double.isFinite(right) - || !Double.isFinite(top) || !Double.isFinite(bottom)) { - return new FlowTangent(0D, 0D); - } - double tangentX = -(bottom - top); - double tangentZ = right - left; - double tangentLength = StrictMath.hypot(tangentX, tangentZ); - if (tangentLength <= 0.0000001D) { - return new FlowTangent(0D, 0D); - } - return new FlowTangent(tangentX / tangentLength, tangentZ / tangentLength); - } - - private double flowAlignmentCost(RiverNode from, RiverNode to, NodeResolver resolver) { - if (options.flowAlignmentWeight() <= 0D) { - return 0D; - } - FlowTangent tangent = resolver.flowTangent(from); - if (tangent.x() == 0D && tangent.z() == 0D) { - return 0D; - } - double deltaX = to.x() - from.x(); - double deltaZ = to.z() - from.z(); - double length = StrictMath.hypot(deltaX, deltaZ); - if (length <= 0.0000001D) { - return options.flowAlignmentWeight(); - } - double alignment = StrictMath.abs( - tangent.x() * deltaX / length + tangent.z() * deltaZ / length - ); - return options.flowAlignmentWeight() * (1D - StrictMath.min(1D, alignment)); - } - - private boolean intersects( - RiverReach reach, - long minimumX, - long minimumZ, - long maximumX, - long maximumZ - ) { - double radius = reach.width() * 0.5 + reach.bankWidth(); - RiverPolyline polyline = reach.polyline(); - for (int point = 0; point < polyline.size() - 1; point++) { - double segmentMinimumX = StrictMath.min(polyline.x(point), polyline.x(point + 1)) - radius; - double segmentMaximumX = StrictMath.max(polyline.x(point), polyline.x(point + 1)) + radius; - double segmentMinimumZ = StrictMath.min(polyline.z(point), polyline.z(point + 1)) - radius; - double segmentMaximumZ = StrictMath.max(polyline.z(point), polyline.z(point + 1)) + radius; - if (segmentMaximumX >= minimumX && segmentMinimumX < maximumX - && segmentMaximumZ >= minimumZ && segmentMinimumZ < maximumZ) { - return true; - } - } - return false; - } - - private boolean potentiallyIntersects( - RiverNode from, - RiverNode to, - long minimumX, - long minimumZ, - long maximumX, - long maximumZ - ) { - double length = StrictMath.hypot(to.x() - from.x(), to.z() - from.z()); - double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), length * 0.35D); - double padding = options.maximumReachRadius() + maximumWormOffset; - double reachMinimumX = StrictMath.min(from.x(), to.x()) - padding; - double reachMaximumX = StrictMath.max(from.x(), to.x()) + padding; - double reachMinimumZ = StrictMath.min(from.z(), to.z()) - padding; - double reachMaximumZ = StrictMath.max(from.z(), to.z()) + padding; - return reachMaximumX >= minimumX && reachMinimumX < maximumX - && reachMaximumZ >= minimumZ && reachMinimumZ < maximumZ; - } - - private int geometryPaddingCells() { - double maximumEdgeAxisDelta = options.cellSize() * (1D + options.siteJitter()); - double maximumEdgeLength = StrictMath.sqrt(2D) * maximumEdgeAxisDelta; - double maximumWormOffset = StrictMath.min(options.maximumWormOffset(), maximumEdgeLength * 0.35D); - double displacement = options.maximumReachRadius() + maximumWormOffset; - return 1 + (int) StrictMath.ceil(displacement / options.cellSize()); - } - - private int compareRank(RiverNode first, RiverNode second) { - if (first.ocean() != second.ocean()) { - return first.ocean() ? -1 : 1; - } - int rankComparison = Double.compare(first.rank(), second.rank()); - if (rankComparison != 0) { - return rankComparison; - } - int hydraulicComparison = Double.compare(first.hydraulicHeight(), second.hydraulicHeight()); - return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id()); - } - - private long hash(RiverNodeId id, long salt) { - return mix(options.seed() ^ id.stableId() ^ salt); - } - - private long hash(RiverEdgeId id, long salt) { - return mix(options.seed() ^ id.stableId() ^ salt); - } - - private long hash(long x, long z, long salt) { - return mix(options.seed() ^ salt ^ mix(x * 0x9E3779B97F4A7C15L) ^ Long.rotateLeft(mix(z), 27)); - } - - private static void addUnique(List values, RiverNodeId candidate) { - if (!values.contains(candidate)) { - values.add(candidate); - } - } - - private static boolean gate(long hash, double chance) { - if (chance <= 0.0) { - return false; - } - if (chance >= 1.0) { - return true; - } - return unit(hash) < chance; - } - - private static double centered(long hash) { - return unit(hash) * 2.0 - 1.0; - } - - private static double unit(long hash) { - return (hash >>> 11) * 0x1.0p-53; - } - - private static int clampToInt(long value) { - return (int) StrictMath.max(Integer.MIN_VALUE, StrictMath.min(Integer.MAX_VALUE, value)); - } - - private static double finiteOrZero(double value) { - return Double.isFinite(value) ? value : 0.0; - } - - private static double finiteNonNegative(double value) { - return Double.isFinite(value) && value > 0.0 ? value : 0.0; - } - - private static double effectiveChance(double baseChance, double multiplier) { - if (!Double.isFinite(multiplier) || multiplier <= 0.0) { - return 0.0; - } - return StrictMath.min(1.0, baseChance * multiplier); - } - - private static void requireWorldBounds(long minimumX, long minimumZ, long maximumX, long maximumZ) { - if (minimumX < Integer.MIN_VALUE || minimumZ < Integer.MIN_VALUE - || maximumX > Integer.MAX_VALUE || maximumZ > Integer.MAX_VALUE) { - throw new IllegalArgumentException("River tile exceeds integer world coordinates"); - } - } - - private final class NodeResolver { - private final RiverTerrainSampler terrain; - private final Map nodes; - private final Map> downstreamCandidates; - private final Map sourceGates; - private final Map> minimumSources; - private final Map reachFeasibilities; - private final Map continuationGates; - private final Map dryGates; - private final Map terminalPolicies; - private final Map routingContexts; - private final Map flowTangents; - private final Map branchSlots; - private final Map styleDistances; - private final Map styleParents; - private final Map styleWorms; - private final Map styleBranchSlots; - private final Map resolvedBranchParents; - private final Map resolvedStyleBranchParents; - - private NodeResolver(RiverTerrainSampler terrain) { - this.terrain = terrain; - nodes = new HashMap<>(); - downstreamCandidates = new HashMap<>(); - sourceGates = new HashMap<>(); - minimumSources = new HashMap<>(); - reachFeasibilities = new HashMap<>(); - continuationGates = new HashMap<>(); - dryGates = new HashMap<>(); - terminalPolicies = new HashMap<>(); - routingContexts = new HashMap<>(); - flowTangents = new HashMap<>(); - branchSlots = new HashMap<>(); - styleDistances = new HashMap<>(); - styleParents = new HashMap<>(); - styleWorms = new HashMap<>(); - styleBranchSlots = new HashMap<>(); - resolvedBranchParents = new HashMap<>(); - resolvedStyleBranchParents = new HashMap<>(); - } - - private RiverNode resolve(RiverNodeId id) { - return nodes.computeIfAbsent(id, key -> createNode(key, terrain)); - } - - private List downstreamCandidates(RiverNode node) { - return downstreamCandidates.computeIfAbsent( - node.id(), - ignored -> computeDownstreamCandidates(node, this)); - } - - private int branchSlot(RiverNode parent, RiverNode child) { - if (!resolvedBranchParents.containsKey(parent.id())) { - ArrayList upstream = new ArrayList<>(8); - for (RiverNodeId siblingId : neighbors(parent.id())) { - RiverNode sibling = resolve(siblingId); - if (sibling.riverAllowed() && compareRank(sibling, parent) > 0) { - upstream.add(sibling); - } - } - upstream.sort((first, second) -> { - long firstPriority = hash(RiverEdgeId.of(first.id(), parent.id()), BRANCH_SLOT_SALT); - long secondPriority = hash(RiverEdgeId.of(second.id(), parent.id()), BRANCH_SLOT_SALT); - int priorityComparison = Long.compareUnsigned(firstPriority, secondPriority); - return priorityComparison != 0 - ? priorityComparison - : first.id().compareTo(second.id()); - }); - for (int slot = 0; slot < upstream.size(); slot++) { - RiverNode sibling = upstream.get(slot); - branchSlots.put(RiverEdgeId.of(sibling.id(), parent.id()), slot); - } - resolvedBranchParents.put(parent.id(), true); - } - return branchSlots.getOrDefault(RiverEdgeId.of(child.id(), parent.id()), Integer.MAX_VALUE); - } - - private boolean sourcePermitted(RiverNodeId sourceId) { - return sourceGates.computeIfAbsent(sourceId, this::computeSourcePermitted); - } - - private boolean computeSourcePermitted(RiverNodeId sourceId) { - if (options.sourceChance() <= 0D) { - return false; - } - boolean minimumSelected = minimumSources(sourceId).contains(sourceId); - if (minimumSelected) { - return true; - } - long sourceHash = hash(sourceId, SOURCE_SALT); - double maximumMultiplier = terrain.maximumSourceChanceMultiplier(); - if (!minimumSelected - && Double.isFinite(maximumMultiplier) - && !gate(sourceHash, effectiveChance(options.sourceChance(), maximumMultiplier))) { - return false; - } - NodePosition position = nodePosition(sourceId); - RiverTerrainSourceSample sourceSample = terrain.sampleSource(position.blockX(), position.blockZ()); - double chance = effectiveChance( - options.sourceChance(), - sourceSample.chanceMultiplier() - ); - boolean selected = gate(sourceHash, chance); - boolean permitted = selected - && sourceSample.riverAllowed() - && !sourceSample.ocean(); - return permitted; - } - - private List minimumSources(RiverNodeId sourceId) { - if (options.minimumSourcesPerTile() <= 0 || options.sourceChance() <= 0D) { - return List.of(); - } - SourceTileId tileId = new SourceTileId( - Math.floorDiv(sourceId.cellX(), options.tileCells()), - Math.floorDiv(sourceId.cellZ(), options.tileCells()) - ); - return minimumSources.computeIfAbsent(tileId, this::computeMinimumSources); - } - - private List computeMinimumSources(SourceTileId tileId) { - long minimumCellX = tileId.tileX() * options.tileCells(); - long minimumCellZ = tileId.tileZ() * options.tileCells(); - int candidateCount = options.tileCells() * options.tileCells(); - int targetCount = Math.min(options.minimumSourcesPerTile(), candidateCount); - if (targetCount == candidateCount) { - ArrayList selected = new ArrayList<>(candidateCount); - for (long cellX = minimumCellX; cellX < minimumCellX + options.tileCells(); cellX++) { - for (long cellZ = minimumCellZ; cellZ < minimumCellZ + options.tileCells(); cellZ++) { - RiverNodeId candidateId = new RiverNodeId(cellX, cellZ); - if (sourceFloorEligible(candidateId)) { - selected.add(candidateId); - } - } - } - return List.copyOf(selected); - } - ArrayList candidates = new ArrayList<>(candidateCount); - for (long cellX = minimumCellX; cellX < minimumCellX + options.tileCells(); cellX++) { - for (long cellZ = minimumCellZ; cellZ < minimumCellZ + options.tileCells(); cellZ++) { - RiverNodeId candidateId = new RiverNodeId(cellX, cellZ); - candidates.add(new WeightedSource( - candidateId, - -drainageDistance(candidateId) - + unit(hash(candidateId, SOURCE_FLOOR_SALT)) * 0.25D - )); - } - } - candidates.sort(Comparator.comparingDouble(WeightedSource::priority) - .thenComparing(WeightedSource::id)); - ArrayList selected = new ArrayList<>(targetCount); - for (WeightedSource candidate : candidates) { - if (!sourceFloorEligible(candidate.id())) { - continue; - } - selected.add(candidate.id()); - if (selected.size() >= targetCount) { - break; - } - } - return List.copyOf(selected); - } - - private boolean sourceFloorEligible(RiverNodeId candidateId) { - NodePosition position = nodePosition(candidateId); - RiverTerrainSourceSample sourceSample = terrain.sampleSource(position.blockX(), position.blockZ()); - return sourceSample.riverAllowed() - && !sourceSample.ocean() - && Double.isFinite(sourceSample.chanceMultiplier()) - && sourceSample.chanceMultiplier() > 0D; - } - - private boolean reachFeasible(RiverRoutingContext context) { - return reachFeasibilities.computeIfAbsent( - context.edgeId(), - ignored -> terrain.allowsReach(context)); - } - - private boolean continuationPermitted(RiverRoutingContext context) { - return continuationGates.computeIfAbsent(context.edgeId(), ignored -> { - double chance = effectiveChance( - options.reachChance(), - terrain.reachChanceMultiplier(context.midpointX(), context.midpointZ()) - ); - return gate(hash(context.edgeId(), REACH_SALT), chance); - }); - } - - private boolean dryPermitted(RiverNodeId sourceId) { - return dryGates.computeIfAbsent( - sourceId, - ignored -> gate(hash(sourceId, DRY_SALT), options.dryChannelChance())); - } - - private RiverTerminalPolicy terminalPolicy(RiverNode terminal) { - return terminalPolicies.computeIfAbsent(terminal.id(), ignored -> { - int terminalX = clampToInt(StrictMath.round(terminal.x())); - int terminalZ = clampToInt(StrictMath.round(terminal.z())); - RiverTerminalPolicy sampled = terrain.terminalPolicy(terminalX, terminalZ); - return sampled == null ? RiverTerminalPolicy.INHERIT : sampled; - }); - } - - private RiverRoutingContext routingContext(RiverNode from, RiverNode to) { - RiverEdgeId edgeId = RiverEdgeId.of(from.id(), to.id()); - RiverWorm worm = worm(from, to); - return routingContexts.computeIfAbsent(edgeId, ignored -> RiverRoutingContext.lazy( - edgeId, - from, - to, - () -> createPolyline(from, to, worm))); - } - - private RiverWorm worm(RiverNode first, RiverNode second) { - RiverNode child = compareRank(first, second) > 0 ? first : second; - return styleWorm(child.id()); - } - - private RiverWorm styleWorm(RiverNodeId id) { - RiverWorm cached = styleWorms.get(id); - if (cached != null) { - return cached; - } - StyleParent parent = styleParent(id); - RiverWorm selected; - if (parent.id() == null) { - selected = rootWormFor(id); - } else { - RiverWorm parentWorm = styleWorm(parent.id()); - selected = childWormFor( - parentWorm, - parent.id(), - id, - styleBranchSlot(parent.id(), id) - ); - } - styleWorms.put(id, selected); - return selected; - } - - private StyleParent styleParent(RiverNodeId id) { - return styleParents.computeIfAbsent(id, nodeId -> { - double nodeDistance = styleDistance(nodeId); - RiverNodeId selected = null; - double selectedDistance = Double.POSITIVE_INFINITY; - for (RiverNodeId candidate : neighbors(nodeId)) { - double candidateDistance = styleDistance(candidate); - if (candidateDistance >= nodeDistance - 0.000000001D) { - continue; - } - if (candidateDistance < selectedDistance - || candidateDistance == selectedDistance - && (selected == null || candidate.compareTo(selected) < 0)) { - selected = candidate; - selectedDistance = candidateDistance; - } - } - return new StyleParent(selected); - }); - } - - private double styleDistance(RiverNodeId id) { - return styleDistances.computeIfAbsent(id, RiverNetwork.this::drainageDistance); - } - - private int styleBranchSlot(RiverNodeId parentId, RiverNodeId childId) { - if (!resolvedStyleBranchParents.containsKey(parentId)) { - ArrayList children = new ArrayList<>(8); - for (RiverNodeId candidate : neighbors(parentId)) { - StyleParent candidateParent = styleParent(candidate); - if (parentId.equals(candidateParent.id())) { - children.add(candidate); - } - } - children.sort((first, second) -> { - long firstPriority = hash(RiverEdgeId.of(first, parentId), BRANCH_SLOT_SALT); - long secondPriority = hash(RiverEdgeId.of(second, parentId), BRANCH_SLOT_SALT); - int priorityComparison = Long.compareUnsigned(firstPriority, secondPriority); - return priorityComparison != 0 ? priorityComparison : first.compareTo(second); - }); - for (int slot = 0; slot < children.size(); slot++) { - RiverNodeId child = children.get(slot); - styleBranchSlots.put(RiverEdgeId.of(child, parentId), slot); - } - resolvedStyleBranchParents.put(parentId, true); - } - return styleBranchSlots.getOrDefault(RiverEdgeId.of(childId, parentId), Integer.MAX_VALUE); - } - - private FlowTangent flowTangent(RiverNode node) { - return flowTangents.computeIfAbsent( - node.id(), - ignored -> resolveFlowTangent(node, terrain)); - } - } - - private record RankedCandidate(RiverNode node, double cost) { - } - - private record NodePosition(double x, double z, int blockX, int blockZ) { - } - - private record FlowTangent(double x, double z) { - } - - private record DrainageBasinId(long x, long z) { - } - - private record DrainageBasin(DrainageBasinId id, double distance) { - } - - private record StyleParent(RiverNodeId id) { - } - - private record SourceTileId(long tileX, long tileZ) { - } - - private record WeightedSource(RiverNodeId id, double priority) { - } - - private final class ReachAccumulator { - private final RiverEdgeId id; - private final RiverNode from; - private final RiverNode to; - private final RiverRoutingContext context; - private final RiverWorm worm; - private final RiverTerrainSampler terrain; - private int wetFlow; - private int dryFlow; - private int terminalWetFlow; - private int terminalDryFlow; - - private ReachAccumulator( - RiverEdgeId id, - RiverNode from, - RiverNode to, - RiverRoutingContext context, - RiverWorm worm, - RiverTerrainSampler terrain - ) { - this.id = id; - this.from = from; - this.to = to; - this.context = context; - this.worm = worm; - this.terrain = terrain; - } - - private void add(RiverRouteState state, boolean terminal) { - if (state == RiverRouteState.WET) { - wetFlow++; - if (terminal) { - terminalWetFlow++; - } - } else if (state == RiverRouteState.DRY) { - dryFlow++; - if (terminal) { - terminalDryFlow++; - } - } - } - - private RiverReach build() { - int flow = wetFlow + dryFlow; - int order = 1 + (31 - Integer.numberOfLeadingZeros(flow)); - RiverBodyProfile bodyProfile = bodyProfile(order, worm); - RiverRouteState state = wetFlow > 0 ? RiverRouteState.WET : RiverRouteState.DRY; - return new RiverReach( - id, - from, - to, - state, - flow, - order, - bodyProfile.maximumWidth(), - bodyProfile.maximumBankWidth(), - bodyProfile.maximumDepth(), - bodyProfile, - state == RiverRouteState.WET && to.ocean(), - state == RiverRouteState.WET - ? terminalWetFlow == wetFlow - : terminalDryFlow == dryFlow, - context.polyline() - ); - } - - private RiverBodyProfile bodyProfile(int order, RiverWorm worm) { - RiverPolyline polyline = context.polyline(); - double minimumWavelength = StrictMath.min(worm.bodyWavelength(), worm.bodyDetailWavelength()); - int resolvedSamples = 1 + (int) StrictMath.ceil(polyline.length() * 2D / minimumWavelength); - int sampleCount = StrictMath.max( - MINIMUM_BODY_PROFILE_SAMPLES, - StrictMath.min(MAXIMUM_BODY_PROFILE_SAMPLES, resolvedSamples) - ); - double[] positions = new double[sampleCount]; - double[] widths = new double[sampleCount]; - double[] bankWidths = new double[sampleCount]; - double[] depths = new double[sampleCount]; - double[] roofScales = new double[sampleCount]; - double widthOrderScale = 1D + options.orderWidthFactor() * (order - 1); - double depthOrderScale = 1D + options.orderDepthFactor() * (order - 1); - for (int index = 0; index < sampleCount; index++) { - double alongReach = (double) index / (sampleCount - 1); - ReachPosition position = positionAt(polyline, alongReach); - double baseWidth = positiveOrFallback( - terrain.channelWidth( - context, - position.x(), - position.z(), - options.channelWidth() - ), - options.channelWidth() - ); - positions[index] = alongReach; - widths[index] = StrictMath.min( - options.maxChannelWidth(), - StrictMath.max( - 1D, - baseWidth - * widthOrderScale - * worm.widthMultiplier() - * bodyMultiplier( - position, - worm, - BODY_WIDTH_PRIMARY_SALT, - BODY_WIDTH_DETAIL_SALT, - worm.widthVariation() - ) - + options.channelRadiusBonus() * 2D - ) - ); - double baseBankWidth = nonNegativeOrFallback( - terrain.bankWidth(context, position.x(), position.z(), options.bankWidth()), - options.bankWidth() - ); - bankWidths[index] = StrictMath.min( - options.maxBankWidth(), - baseBankWidth - * worm.bankMultiplier() - * bodyMultiplier( - position, - worm, - BODY_BANK_PRIMARY_SALT, - BODY_BANK_DETAIL_SALT, - worm.bankVariation() - ) - ); - double baseDepth = positiveOrFallback( - terrain.depth(context, position.x(), position.z(), options.depth()), - options.depth() - ); - depths[index] = StrictMath.min( - options.maxDepth(), - StrictMath.max( - 1D, - baseDepth - * depthOrderScale - * worm.depthMultiplier() - * bodyMultiplier( - position, - worm, - BODY_DEPTH_PRIMARY_SALT, - BODY_DEPTH_DETAIL_SALT, - worm.depthVariation() - ) - ) - ); - roofScales[index] = roofScale(position, worm); - } - return new RiverBodyProfile(positions, widths, bankWidths, depths, roofScales); - } - } - - private static ReachPosition positionAt(RiverPolyline polyline, double alongReach) { - double targetDistance = Math.max(0D, Math.min(1D, alongReach)) * polyline.length(); - for (int point = 0; point < polyline.size() - 1; point++) { - double segmentStart = polyline.cumulativeLength(point); - double segmentEnd = polyline.cumulativeLength(point + 1); - if (targetDistance > segmentEnd && point < polyline.size() - 2) { - continue; - } - double segmentLength = segmentEnd - segmentStart; - double interpolation = segmentLength <= 0D - ? 0D - : (targetDistance - segmentStart) / segmentLength; - return new ReachPosition( - polyline.x(point) + (polyline.x(point + 1) - polyline.x(point)) * interpolation, - polyline.z(point) + (polyline.z(point + 1) - polyline.z(point)) * interpolation - ); - } - int last = polyline.size() - 1; - return new ReachPosition(polyline.x(last), polyline.z(last)); - } - - private static double positiveOrFallback(double value, double fallback) { - return Double.isFinite(value) && value > 0.0 ? value : fallback; - } - - private static double nonNegativeOrFallback(double value, double fallback) { - return Double.isFinite(value) && value >= 0.0 ? value : fallback; - } - - private record ReachPosition(double x, double z) { - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverNetworkOptions.java b/core/src/main/java/art/arcane/iris/engine/river/RiverNetworkOptions.java deleted file mode 100644 index f4b7520b3..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverNetworkOptions.java +++ /dev/null @@ -1,488 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -public record RiverNetworkOptions( - long seed, - int cellSize, - int tileCells, - double siteJitter, - int maxRouteReaches, - int minimumSourcesPerTile, - int downstreamCandidateLimit, - int routingBasinCells, - int routingDeviationScaleCells, - double routingDeviationStrengthCells, - double routingPlateauHeight, - double hydraulicBaseHeight, - boolean requireOcean, - double sourceChance, - double reachChance, - double dryChannelChance, - double terrainHeightWeight, - double routingNoiseWeight, - double flowAlignmentWeight, - double confluenceWeight, - double oceanAttraction, - double channelWidth, - double bankWidth, - double depth, - double channelRadiusBonus, - double maxChannelWidth, - double maxBankWidth, - double maxDepth, - double orderWidthFactor, - double orderDepthFactor, - double maximumReachRadius, - List worms -) { - public RiverNetworkOptions { - requireRange(cellSize, 8, 4096, "cellSize"); - requireRange(tileCells, 1, 64, "tileCells"); - requireRange(maxRouteReaches, 1, 256, "maxRouteReaches"); - requireRange(minimumSourcesPerTile, 0, tileCells * tileCells, "minimumSourcesPerTile"); - requireRange(downstreamCandidateLimit, 1, 8, "downstreamCandidateLimit"); - requireRange(routingBasinCells, 8, 256, "routingBasinCells"); - requireRange(routingDeviationScaleCells, 8, 256, "routingDeviationScaleCells"); - requireRange(routingDeviationStrengthCells, 0D, 32D, "routingDeviationStrengthCells"); - requirePositive(routingPlateauHeight, "routingPlateauHeight"); - requireFinite(hydraulicBaseHeight, "hydraulicBaseHeight"); - requireProbability(siteJitter, "siteJitter"); - requireProbability(sourceChance, "sourceChance"); - requireProbability(reachChance, "reachChance"); - requireProbability(dryChannelChance, "dryChannelChance"); - requireFiniteNonNegative(terrainHeightWeight, "terrainHeightWeight"); - requireFiniteNonNegative(routingNoiseWeight, "routingNoiseWeight"); - requireFiniteNonNegative(flowAlignmentWeight, "flowAlignmentWeight"); - requireFiniteNonNegative(confluenceWeight, "confluenceWeight"); - requireFiniteNonNegative(oceanAttraction, "oceanAttraction"); - requirePositive(channelWidth, "channelWidth"); - requireFiniteNonNegative(bankWidth, "bankWidth"); - requirePositive(depth, "depth"); - requireFiniteNonNegative(channelRadiusBonus, "channelRadiusBonus"); - requirePositive(maxChannelWidth, "maxChannelWidth"); - requireFiniteNonNegative(maxBankWidth, "maxBankWidth"); - requirePositive(maxDepth, "maxDepth"); - requireFiniteNonNegative(orderWidthFactor, "orderWidthFactor"); - requireFiniteNonNegative(orderDepthFactor, "orderDepthFactor"); - requireFiniteNonNegative(maximumReachRadius, "maximumReachRadius"); - if (worms == null || worms.isEmpty()) { - throw new IllegalArgumentException("worms must contain at least one profile"); - } - if (worms.size() > 16) { - throw new IllegalArgumentException("worms must contain at most 16 root profiles"); - } - Set ids = new HashSet(); - Set seeds = new HashSet(); - int wormCount = validateWormTree(worms, 1, ids, seeds); - if (wormCount > 128) { - throw new IllegalArgumentException("worm hierarchy must contain at most 128 profiles"); - } - worms = List.copyOf(worms); - RiverTopologyComplexity.requireSafe( - cellSize, - tileCells, - siteJitter, - maxRouteReaches, - maximumReachRadius, - maximumWormOffset(worms), - maximumWormSegments(worms) - ); - } - - public static Builder builder(long seed) { - return new Builder(seed); - } - - public double maximumWormOffset() { - return maximumWormOffset(worms); - } - - public int maximumWormSegments() { - return maximumWormSegments(worms); - } - - private static double maximumWormOffset(List worms) { - double maximum = 0D; - for (RiverWorm worm : worms) { - if (worm == null) { - throw new IllegalArgumentException("worms must not contain null profiles"); - } - maximum = StrictMath.max(maximum, worm.maxOffset()); - maximum = StrictMath.max(maximum, maximumWormOffset(worm.children())); - } - return maximum; - } - - private static int maximumWormSegments(List worms) { - int maximum = 1; - for (RiverWorm worm : worms) { - if (worm == null) { - throw new IllegalArgumentException("worms must not contain null profiles"); - } - maximum = StrictMath.max(maximum, worm.segments()); - maximum = StrictMath.max(maximum, maximumWormSegments(worm.children())); - } - return maximum; - } - - private static int validateWormTree( - List worms, - int depth, - Set ids, - Set seeds - ) { - if (depth > 4) { - throw new IllegalArgumentException("worm hierarchy must be at most 4 profiles deep"); - } - int count = 0; - for (RiverWorm worm : worms) { - if (worm == null) { - throw new IllegalArgumentException("worm hierarchy must not contain null profiles"); - } - if (!ids.add(worm.id())) { - throw new IllegalArgumentException("worm ids must be unique: " + worm.id()); - } - if (!seeds.add(worm.seed())) { - throw new IllegalArgumentException("worm seeds must be unique: " + worm.seed()); - } - count++; - if (!worm.children().isEmpty()) { - count += validateWormTree(worm.children(), depth + 1, ids, seeds); - } - } - return count; - } - - private static void requireRange(int value, int minimum, int maximum, String name) { - if (value < minimum || value > maximum) { - throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum); - } - } - - private static void requireRange(double value, double minimum, double maximum, String name) { - if (!Double.isFinite(value) || value < minimum || value > maximum) { - throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum); - } - } - - private static void requireProbability(double value, String name) { - if (!Double.isFinite(value) || value < 0.0 || value > 1.0) { - throw new IllegalArgumentException(name + " must be finite and between 0 and 1"); - } - } - - private static void requireFiniteNonNegative(double value, String name) { - if (!Double.isFinite(value) || value < 0.0) { - throw new IllegalArgumentException(name + " must be finite and non-negative"); - } - } - - private static void requirePositive(double value, String name) { - if (!Double.isFinite(value) || value <= 0.0) { - throw new IllegalArgumentException(name + " must be finite and positive"); - } - } - - private static void requireFinite(double value, String name) { - if (!Double.isFinite(value)) { - throw new IllegalArgumentException(name + " must be finite"); - } - } - - public static final class Builder { - private final long seed; - private int cellSize; - private int tileCells; - private double siteJitter; - private int maxRouteReaches; - private int minimumSourcesPerTile; - private int downstreamCandidateLimit; - private int routingBasinCells; - private int routingDeviationScaleCells; - private double routingDeviationStrengthCells; - private double routingPlateauHeight; - private double hydraulicBaseHeight; - private boolean requireOcean; - private double sourceChance; - private double reachChance; - private double dryChannelChance; - private double terrainHeightWeight; - private double routingNoiseWeight; - private double flowAlignmentWeight; - private double confluenceWeight; - private double oceanAttraction; - private double channelWidth; - private double bankWidth; - private double depth; - private double channelRadiusBonus; - private double maxChannelWidth; - private double maxBankWidth; - private double maxDepth; - private double orderWidthFactor; - private double orderDepthFactor; - private double maximumReachRadius; - private List worms; - - private Builder(long seed) { - this.seed = seed; - cellSize = 512; - tileCells = 4; - siteJitter = 0.35; - maxRouteReaches = 16; - minimumSourcesPerTile = 0; - downstreamCandidateLimit = 4; - routingBasinCells = 64; - routingDeviationScaleCells = 24; - routingDeviationStrengthCells = 0D; - routingPlateauHeight = 8.0; - hydraulicBaseHeight = 64D; - requireOcean = false; - sourceChance = 0.12; - reachChance = 0.98; - dryChannelChance = 0.35; - terrainHeightWeight = 1.0; - routingNoiseWeight = 24.0; - flowAlignmentWeight = 0D; - confluenceWeight = 0D; - oceanAttraction = 64.0; - channelWidth = 10.0; - bankWidth = 8.0; - depth = 4.0; - maxChannelWidth = 10D; - maxBankWidth = 8D; - maxDepth = 10D; - orderWidthFactor = 0.35; - orderDepthFactor = 0.2; - maximumReachRadius = Double.NaN; - worms = List.of(new RiverWorm( - "default", - 1L, - 1D, - 1024D, - 256D, - 0.5D, - 0.15D, - 40D, - 8, - 1D, - 1D, - 1D, - 512D, - 128D, - 0.3D, - 0D, - 0D, - 0D, - 0D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - )); - } - - public Builder cellSize(int value) { - cellSize = value; - return this; - } - - public Builder tileCells(int value) { - tileCells = value; - return this; - } - - public Builder siteJitter(double value) { - siteJitter = value; - return this; - } - - public Builder maxRouteReaches(int value) { - maxRouteReaches = value; - return this; - } - - public Builder minimumSourcesPerTile(int value) { - minimumSourcesPerTile = value; - return this; - } - - public Builder downstreamCandidateLimit(int value) { - downstreamCandidateLimit = value; - return this; - } - - public Builder routingBasinCells(int value) { - routingBasinCells = value; - return this; - } - - public Builder routingDeviationScaleCells(int value) { - routingDeviationScaleCells = value; - return this; - } - - public Builder routingDeviationStrengthCells(double value) { - routingDeviationStrengthCells = value; - return this; - } - - public Builder routingPlateauHeight(double value) { - routingPlateauHeight = value; - return this; - } - - public Builder hydraulicBaseHeight(double value) { - hydraulicBaseHeight = value; - return this; - } - - public Builder requireOcean(boolean value) { - requireOcean = value; - return this; - } - - public Builder sourceChance(double value) { - sourceChance = value; - return this; - } - - public Builder reachChance(double value) { - reachChance = value; - return this; - } - - public Builder dryChannelChance(double value) { - dryChannelChance = value; - return this; - } - - public Builder terrainHeightWeight(double value) { - terrainHeightWeight = value; - return this; - } - - public Builder routingNoiseWeight(double value) { - routingNoiseWeight = value; - return this; - } - - public Builder flowAlignmentWeight(double value) { - flowAlignmentWeight = value; - return this; - } - - public Builder confluenceWeight(double value) { - confluenceWeight = value; - return this; - } - - public Builder oceanAttraction(double value) { - oceanAttraction = value; - return this; - } - - public Builder channelWidth(double value) { - channelWidth = value; - return this; - } - - public Builder bankWidth(double value) { - bankWidth = value; - return this; - } - - public Builder depth(double value) { - depth = value; - return this; - } - - public Builder channelRadiusBonus(double value) { - channelRadiusBonus = value; - return this; - } - - public Builder maxChannelWidth(double value) { - maxChannelWidth = value; - return this; - } - - public Builder maxBankWidth(double value) { - maxBankWidth = value; - return this; - } - - public Builder maxDepth(double value) { - maxDepth = value; - return this; - } - - public Builder orderWidthFactor(double value) { - orderWidthFactor = value; - return this; - } - - public Builder orderDepthFactor(double value) { - orderDepthFactor = value; - return this; - } - - public Builder maximumReachRadius(double value) { - maximumReachRadius = value; - return this; - } - - public Builder worms(List value) { - worms = value; - return this; - } - - public RiverNetworkOptions build() { - double resolvedMaximumReachRadius = Double.isFinite(maximumReachRadius) - ? maximumReachRadius - : defaultMaximumReachRadius(); - return new RiverNetworkOptions( - seed, - cellSize, - tileCells, - siteJitter, - maxRouteReaches, - minimumSourcesPerTile, - downstreamCandidateLimit, - routingBasinCells, - routingDeviationScaleCells, - routingDeviationStrengthCells, - routingPlateauHeight, - hydraulicBaseHeight, - requireOcean, - sourceChance, - reachChance, - dryChannelChance, - terrainHeightWeight, - routingNoiseWeight, - flowAlignmentWeight, - confluenceWeight, - oceanAttraction, - channelWidth, - bankWidth, - depth, - channelRadiusBonus, - maxChannelWidth, - maxBankWidth, - maxDepth, - orderWidthFactor, - orderDepthFactor, - resolvedMaximumReachRadius, - worms - ); - } - - private double defaultMaximumReachRadius() { - return maxChannelWidth * 0.5D + maxBankWidth; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverNode.java b/core/src/main/java/art/arcane/iris/engine/river/RiverNode.java deleted file mode 100644 index 73b895b77..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverNode.java +++ /dev/null @@ -1,24 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.Objects; - -public record RiverNode( - RiverNodeId id, - double x, - double z, - double naturalHeight, - double hydraulicHeight, - double rank, - double routingScore, - boolean ocean, - boolean riverAllowed -) { - public RiverNode { - Objects.requireNonNull(id); - if (!Double.isFinite(x) || !Double.isFinite(z) || !Double.isFinite(naturalHeight) - || !Double.isFinite(hydraulicHeight) - || !Double.isFinite(rank) || !Double.isFinite(routingScore)) { - throw new IllegalArgumentException("River node coordinates, height, and rank must be finite"); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverNodeId.java b/core/src/main/java/art/arcane/iris/engine/river/RiverNodeId.java deleted file mode 100644 index 352502f58..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverNodeId.java +++ /dev/null @@ -1,13 +0,0 @@ -package art.arcane.iris.engine.river; - -public record RiverNodeId(long cellX, long cellZ) implements Comparable { - public long stableId() { - return RiverNetwork.mix(cellX * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(cellZ * 0xC2B2AE3D27D4EB4FL, 31)); - } - - @Override - public int compareTo(RiverNodeId other) { - int xComparison = Long.compare(cellX, other.cellX); - return xComparison != 0 ? xComparison : Long.compare(cellZ, other.cellZ); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverPolyline.java b/core/src/main/java/art/arcane/iris/engine/river/RiverPolyline.java deleted file mode 100644 index 9970374ca..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverPolyline.java +++ /dev/null @@ -1,48 +0,0 @@ -package art.arcane.iris.engine.river; - -public final class RiverPolyline { - private final double[] x; - private final double[] z; - private final double[] cumulativeLength; - private final double length; - - public RiverPolyline(double[] x, double[] z) { - if (x.length != z.length || x.length < 2) { - throw new IllegalArgumentException("River polyline requires matching coordinate arrays and at least two points"); - } - this.x = x.clone(); - this.z = z.clone(); - cumulativeLength = new double[x.length]; - double measuredLength = 0.0; - for (int i = 0; i < x.length; i++) { - if (!Double.isFinite(x[i]) || !Double.isFinite(z[i])) { - throw new IllegalArgumentException("River polyline coordinates must be finite"); - } - if (i > 0) { - measuredLength += StrictMath.hypot(x[i] - x[i - 1], z[i] - z[i - 1]); - cumulativeLength[i] = measuredLength; - } - } - length = measuredLength; - } - - public int size() { - return x.length; - } - - public double x(int index) { - return x[index]; - } - - public double z(int index) { - return z[index]; - } - - public double cumulativeLength(int index) { - return cumulativeLength[index]; - } - - public double length() { - return length; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverReach.java b/core/src/main/java/art/arcane/iris/engine/river/RiverReach.java deleted file mode 100644 index 383b224d9..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverReach.java +++ /dev/null @@ -1,59 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.Objects; - -public record RiverReach( - RiverEdgeId id, - RiverNode from, - RiverNode to, - RiverRouteState state, - int flow, - int order, - double width, - double bankWidth, - double depth, - RiverBodyProfile bodyProfile, - boolean mouth, - boolean terminal, - RiverPolyline polyline -) { - public RiverReach { - Objects.requireNonNull(id); - Objects.requireNonNull(from); - Objects.requireNonNull(to); - Objects.requireNonNull(state); - Objects.requireNonNull(bodyProfile); - Objects.requireNonNull(polyline); - if (state == RiverRouteState.SUPPRESSED) { - throw new IllegalArgumentException("Suppressed routes cannot produce reaches"); - } - if (flow < 1 || order < 1) { - throw new IllegalArgumentException("River reach flow and order must be positive"); - } - if (!Double.isFinite(width) || width <= 0.0 || !Double.isFinite(bankWidth) || bankWidth < 0.0 - || !Double.isFinite(depth) || depth <= 0.0) { - throw new IllegalArgumentException("River reach dimensions must be finite and valid"); - } - if (Double.compare(width, bodyProfile.maximumWidth()) != 0 - || Double.compare(bankWidth, bodyProfile.maximumBankWidth()) != 0 - || Double.compare(depth, bodyProfile.maximumDepth()) != 0) { - throw new IllegalArgumentException("River reach dimensions must equal their body-profile maxima"); - } - } - - public double widthAt(double alongReach) { - return bodyProfile.width(alongReach); - } - - public double bankWidthAt(double alongReach) { - return bodyProfile.bankWidth(alongReach); - } - - public double depthAt(double alongReach) { - return bodyProfile.depth(alongReach); - } - - public double roofScaleAt(double alongReach) { - return bodyProfile.roofScale(alongReach); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverRoute.java b/core/src/main/java/art/arcane/iris/engine/river/RiverRoute.java deleted file mode 100644 index 1d619c05e..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverRoute.java +++ /dev/null @@ -1,18 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.List; -import java.util.Objects; - -public record RiverRoute( - RiverNodeId source, - RiverRouteState state, - List edges, - boolean oceanConnected, - boolean terminal -) { - public RiverRoute { - Objects.requireNonNull(source); - Objects.requireNonNull(state); - edges = List.copyOf(edges); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverRouteState.java b/core/src/main/java/art/arcane/iris/engine/river/RiverRouteState.java deleted file mode 100644 index 00beebf4e..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverRouteState.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.engine.river; - -public enum RiverRouteState { - WET, - DRY, - SUPPRESSED -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverRoutingContext.java b/core/src/main/java/art/arcane/iris/engine/river/RiverRoutingContext.java deleted file mode 100644 index ef9e856c1..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverRoutingContext.java +++ /dev/null @@ -1,105 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.Objects; -import java.util.function.Supplier; - -public final class RiverRoutingContext { - private final RiverEdgeId edgeId; - private final RiverNode from; - private final RiverNode to; - private final Supplier polylineSupplier; - private volatile RiverPolyline polyline; - - public RiverRoutingContext(RiverEdgeId edgeId, RiverNode from, RiverNode to, RiverPolyline polyline) { - this(edgeId, from, to, () -> polyline, Objects.requireNonNull(polyline)); - } - - static RiverRoutingContext lazy( - RiverEdgeId edgeId, - RiverNode from, - RiverNode to, - Supplier polylineSupplier - ) { - return new RiverRoutingContext(edgeId, from, to, polylineSupplier, null); - } - - private RiverRoutingContext( - RiverEdgeId edgeId, - RiverNode from, - RiverNode to, - Supplier polylineSupplier, - RiverPolyline polyline - ) { - this.edgeId = Objects.requireNonNull(edgeId); - this.from = Objects.requireNonNull(from); - this.to = Objects.requireNonNull(to); - this.polylineSupplier = Objects.requireNonNull(polylineSupplier); - this.polyline = polyline; - } - - public RiverEdgeId edgeId() { - return edgeId; - } - - public RiverNode from() { - return from; - } - - public RiverNode to() { - return to; - } - - public RiverPolyline polyline() { - RiverPolyline resolved = polyline; - if (resolved != null) { - return resolved; - } - synchronized (this) { - if (polyline == null) { - polyline = Objects.requireNonNull(polylineSupplier.get()); - } - return polyline; - } - } - - public int midpointX() { - return (int) StrictMath.max( - Integer.MIN_VALUE, - StrictMath.min(Integer.MAX_VALUE, StrictMath.round((from.x() + to.x()) * 0.5)) - ); - } - - public int midpointZ() { - return (int) StrictMath.max( - Integer.MIN_VALUE, - StrictMath.min(Integer.MAX_VALUE, StrictMath.round((from.z() + to.z()) * 0.5)) - ); - } - - @Override - public boolean equals(Object candidate) { - if (this == candidate) { - return true; - } - if (!(candidate instanceof RiverRoutingContext context)) { - return false; - } - return edgeId.equals(context.edgeId) - && from.equals(context.from) - && to.equals(context.to) - && polyline().equals(context.polyline()); - } - - @Override - public int hashCode() { - return Objects.hash(edgeId, from, to, polyline()); - } - - @Override - public String toString() { - return "RiverRoutingContext[edgeId=" + edgeId - + ", from=" + from - + ", to=" + to - + ", polyline=" + polyline() + "]"; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverSample.java b/core/src/main/java/art/arcane/iris/engine/river/RiverSample.java deleted file mode 100644 index c527910da..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverSample.java +++ /dev/null @@ -1,37 +0,0 @@ -package art.arcane.iris.engine.river; - -public record RiverSample( - boolean present, - RiverRouteState state, - RiverSection section, - double distance, - double alongReach, - double carveWeight, - int flow, - int order, - double width, - double bankWidth, - double depth, - boolean terminal, - RiverEdgeId reachId -) { - private static final RiverSample NONE = new RiverSample( - false, - RiverRouteState.SUPPRESSED, - RiverSection.NONE, - Double.POSITIVE_INFINITY, - 0.0, - 0.0, - 0, - 0, - 0.0, - 0.0, - 0.0, - false, - null - ); - - public static RiverSample none() { - return NONE; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverSection.java b/core/src/main/java/art/arcane/iris/engine/river/RiverSection.java deleted file mode 100644 index 452774c60..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverSection.java +++ /dev/null @@ -1,10 +0,0 @@ -package art.arcane.iris.engine.river; - -public enum RiverSection { - NONE, - CHANNEL, - MOUTH, - BANK, - DRY_CHANNEL, - DRY_BANK -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTerminalPolicy.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTerminalPolicy.java deleted file mode 100644 index 5f0563d85..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTerminalPolicy.java +++ /dev/null @@ -1,8 +0,0 @@ -package art.arcane.iris.engine.river; - -public enum RiverTerminalPolicy { - INHERIT, - WET, - DRY, - SUPPRESS -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainNodeSample.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainNodeSample.java deleted file mode 100644 index 8fe2fbee6..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainNodeSample.java +++ /dev/null @@ -1,9 +0,0 @@ -package art.arcane.iris.engine.river; - -public record RiverTerrainNodeSample( - double naturalHeight, - boolean ocean, - boolean riverAllowed, - double routingCost -) { -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainSampler.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainSampler.java deleted file mode 100644 index 1fc250bec..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainSampler.java +++ /dev/null @@ -1,99 +0,0 @@ -package art.arcane.iris.engine.river; - -public interface RiverTerrainSampler { - double naturalHeight(int blockX, int blockZ); - - boolean isOcean(int blockX, int blockZ); - - default RiverTerrainNodeSample sampleNode(int blockX, int blockZ) { - return new RiverTerrainNodeSample( - naturalHeight(blockX, blockZ), - isOcean(blockX, blockZ), - allowsRiver(blockX, blockZ), - routingCost(blockX, blockZ) - ); - } - - default RiverTerrainSourceSample sampleSource(int blockX, int blockZ) { - return new RiverTerrainSourceSample( - sourceChanceMultiplier(blockX, blockZ), - allowsRiver(blockX, blockZ), - isOcean(blockX, blockZ) - ); - } - - default double routingCost(int blockX, int blockZ) { - return 0.0; - } - - default double sourceChanceMultiplier(int blockX, int blockZ) { - return 1.0; - } - - default double maximumSourceChanceMultiplier() { - return Double.POSITIVE_INFINITY; - } - - default double reachChanceMultiplier(int blockX, int blockZ) { - return 1.0; - } - - default boolean allowsRiver(int blockX, int blockZ) { - return true; - } - - default boolean allowsReach(RiverRoutingContext context) { - return true; - } - - default double reachRoutingCost(RiverRoutingContext context) { - return 0.0; - } - - default double flowNoise(double x, double z) { - return Double.NaN; - } - - default double channelWidth(RiverRoutingContext context, double fallback) { - return fallback; - } - - default double channelWidth( - RiverRoutingContext context, - double x, - double z, - double fallback - ) { - return channelWidth(context, fallback); - } - - default double bankWidth(RiverRoutingContext context, double fallback) { - return fallback; - } - - default double bankWidth( - RiverRoutingContext context, - double x, - double z, - double fallback - ) { - return bankWidth(context, fallback); - } - - default double depth(RiverRoutingContext context, double fallback) { - return fallback; - } - - default double depth( - RiverRoutingContext context, - double x, - double z, - double fallback - ) { - return depth(context, fallback); - } - - default RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) { - return RiverTerminalPolicy.INHERIT; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainSourceSample.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainSourceSample.java deleted file mode 100644 index 279feb58b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTerrainSourceSample.java +++ /dev/null @@ -1,8 +0,0 @@ -package art.arcane.iris.engine.river; - -public record RiverTerrainSourceSample( - double chanceMultiplier, - boolean riverAllowed, - boolean ocean -) { -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTile.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTile.java deleted file mode 100644 index 8c9bbdd85..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTile.java +++ /dev/null @@ -1,717 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -public final class RiverTile { - private static final int BUCKET_SIZE = 64; - - private final int tileX; - private final int tileZ; - private final int minimumX; - private final int minimumZ; - private final int maximumX; - private final int maximumZ; - private final List reaches; - private final Map reachesById; - private final Map> spatialIndex; - - public RiverTile( - int tileX, - int tileZ, - int minimumX, - int minimumZ, - int maximumX, - int maximumZ, - List reaches - ) { - if (minimumX >= maximumX || minimumZ >= maximumZ) { - throw new IllegalArgumentException("River tile bounds must have positive area"); - } - this.tileX = tileX; - this.tileZ = tileZ; - this.minimumX = minimumX; - this.minimumZ = minimumZ; - this.maximumX = maximumX; - this.maximumZ = maximumZ; - this.reaches = List.copyOf(reaches); - reachesById = indexById(this.reaches); - spatialIndex = createSpatialIndex(this.reaches); - } - - public int tileX() { - return tileX; - } - - public int tileZ() { - return tileZ; - } - - public int minimumX() { - return minimumX; - } - - public int minimumZ() { - return minimumZ; - } - - public int maximumX() { - return maximumX; - } - - public int maximumZ() { - return maximumZ; - } - - public List reaches() { - return reaches; - } - - public RiverReach reach(RiverEdgeId id) { - return reachesById.get(Objects.requireNonNull(id)); - } - - public List candidateAnchors(double spacing, long salt) { - return candidateAnchors(minimumX, minimumZ, maximumX, maximumZ, spacing, salt); - } - - public List candidateAnchors( - double queryMinimumX, - double queryMinimumZ, - double queryMaximumX, - double queryMaximumZ, - double spacing, - long salt - ) { - if (!Double.isFinite(spacing) || spacing <= 0.0) { - throw new IllegalArgumentException("River anchor spacing must be finite and positive"); - } - if (!Double.isFinite(queryMinimumX) || !Double.isFinite(queryMinimumZ) - || !Double.isFinite(queryMaximumX) || !Double.isFinite(queryMaximumZ) - || queryMinimumX >= queryMaximumX || queryMinimumZ >= queryMaximumZ) { - throw new IllegalArgumentException("River anchor query bounds must be finite and have positive area"); - } - ArrayList anchors = new ArrayList<>(); - for (RiverReach reach : indexedReaches(queryMinimumX, queryMinimumZ, queryMaximumX, queryMaximumZ)) { - addAnchors( - reach, - spacing, - salt, - queryMinimumX, - queryMinimumZ, - queryMaximumX, - queryMaximumZ, - anchors - ); - } - return List.copyOf(anchors); - } - - public int sampleCandidateCount(double x, double z) { - return indexedReaches(x, z).size(); - } - - public RiverSample sample(double x, double z) { - return sampleExpanded(x, z, 0D); - } - - public RiverSample sampleExpanded(double x, double z, double additionalRadius) { - if (!Double.isFinite(additionalRadius) || additionalRadius < 0D) { - throw new IllegalArgumentException("Additional river sample radius must be finite and non-negative"); - } - RiverReach nearestReach = null; - double nearestDistanceSquared = Double.POSITIVE_INFINITY; - double nearestAlongReach = 0.0; - List candidates = additionalRadius == 0D - ? indexedReaches(x, z) - : indexedReaches( - x - additionalRadius, - z - additionalRadius, - x + additionalRadius, - z + additionalRadius - ); - for (RiverReach reach : candidates) { - ClosestPoint closest = closestCoveringPoint(reach, x, z, additionalRadius); - if (closest == null) { - continue; - } - if (closest.distanceSquared() < nearestDistanceSquared - || (closest.distanceSquared() == nearestDistanceSquared - && nearestReach != null - && reach.id().compareTo(nearestReach.id()) < 0)) { - nearestReach = reach; - nearestDistanceSquared = closest.distanceSquared(); - nearestAlongReach = closest.alongReach(); - } - } - if (nearestReach == null) { - return RiverSample.none(); - } - - return createSample(nearestReach, nearestDistanceSquared, nearestAlongReach); - } - - public RiverSample sampleFootprint( - double queryMinimumX, - double queryMinimumZ, - double queryMaximumX, - double queryMaximumZ - ) { - if (!Double.isFinite(queryMinimumX) || !Double.isFinite(queryMinimumZ) - || !Double.isFinite(queryMaximumX) || !Double.isFinite(queryMaximumZ) - || queryMinimumX > queryMaximumX || queryMinimumZ > queryMaximumZ) { - throw new IllegalArgumentException("River footprint bounds must be finite and ordered"); - } - RiverReach nearestReach = null; - double nearestDistanceSquared = Double.POSITIVE_INFINITY; - double nearestAlongReach = 0.0; - for (RiverReach reach : indexedReachesInclusive( - queryMinimumX, - queryMinimumZ, - queryMaximumX, - queryMaximumZ - )) { - ClosestPoint closest = closestCoveringPoint( - reach, - queryMinimumX, - queryMinimumZ, - queryMaximumX, - queryMaximumZ - ); - if (closest == null) { - continue; - } - if (closest.distanceSquared() < nearestDistanceSquared - || (closest.distanceSquared() == nearestDistanceSquared - && nearestReach != null - && reach.id().compareTo(nearestReach.id()) < 0)) { - nearestReach = reach; - nearestDistanceSquared = closest.distanceSquared(); - nearestAlongReach = closest.alongReach(); - } - } - if (nearestReach == null) { - return RiverSample.none(); - } - - return createSample(nearestReach, nearestDistanceSquared, nearestAlongReach); - } - - private static RiverSample createSample( - RiverReach nearestReach, - double nearestDistanceSquared, - double nearestAlongReach - ) { - - double distance = StrictMath.sqrt(nearestDistanceSquared); - double localWidth = nearestReach.widthAt(nearestAlongReach); - double localBankWidth = nearestReach.bankWidthAt(nearestAlongReach); - double localDepth = nearestReach.depthAt(nearestAlongReach); - double channelRadius = localWidth * 0.5; - RiverSection section = section(nearestReach, distance, channelRadius); - double carveWeight = carveWeight(distance, channelRadius, localBankWidth); - return new RiverSample( - true, - nearestReach.state(), - section, - distance, - nearestAlongReach, - carveWeight, - nearestReach.flow(), - nearestReach.order(), - localWidth, - localBankWidth, - localDepth, - nearestReach.terminal(), - nearestReach.id() - ); - } - - private static RiverSection section(RiverReach reach, double distance, double channelRadius) { - if (distance <= channelRadius) { - if (reach.state() == RiverRouteState.DRY) { - return RiverSection.DRY_CHANNEL; - } - return reach.mouth() ? RiverSection.MOUTH : RiverSection.CHANNEL; - } - return reach.state() == RiverRouteState.DRY ? RiverSection.DRY_BANK : RiverSection.BANK; - } - - private void addAnchors( - RiverReach reach, - double spacing, - long salt, - double queryMinimumX, - double queryMinimumZ, - double queryMaximumX, - double queryMaximumZ, - List anchors - ) { - double length = reach.polyline().length(); - double firstDistance = unit(RiverNetwork.mix(reach.id().stableId() ^ salt)) * spacing; - int index = 0; - for (double distance = firstDistance; distance < length; distance += spacing) { - Position position = positionAt(reach.polyline(), distance); - if (position.x() >= minimumX && position.x() < maximumX - && position.z() >= minimumZ && position.z() < maximumZ - && position.x() >= queryMinimumX && position.x() < queryMaximumX - && position.z() >= queryMinimumZ && position.z() < queryMaximumZ) { - long stableId = RiverNetwork.mix( - reach.id().stableId() ^ salt ^ (long) index * 0x9E3779B97F4A7C15L - ); - anchors.add(new RiverAnchor( - reach.id(), - index, - stableId, - spacing, - salt, - position.x(), - position.z(), - position.alongReach(), - reach.state(), - reach.flow(), - reach.order() - )); - } - index++; - } - } - - private static Position positionAt(RiverPolyline polyline, double targetDistance) { - double traversed = 0.0; - for (int point = 0; point < polyline.size() - 1; point++) { - double startX = polyline.x(point); - double startZ = polyline.z(point); - double deltaX = polyline.x(point + 1) - startX; - double deltaZ = polyline.z(point + 1) - startZ; - double segmentLength = StrictMath.hypot(deltaX, deltaZ); - if (targetDistance <= traversed + segmentLength || point == polyline.size() - 2) { - double t = segmentLength == 0.0 ? 0.0 : (targetDistance - traversed) / segmentLength; - t = StrictMath.max(0.0, StrictMath.min(1.0, t)); - double alongReach = polyline.length() == 0.0 ? 0.0 : targetDistance / polyline.length(); - return new Position(startX + deltaX * t, startZ + deltaZ * t, alongReach); - } - traversed += segmentLength; - } - return new Position( - polyline.x(polyline.size() - 1), - polyline.z(polyline.size() - 1), - 1.0 - ); - } - - private static double unit(long hash) { - return (hash >>> 11) * 0x1.0p-53; - } - - private static double carveWeight(double distance, double channelRadius, double bankWidth) { - if (distance <= channelRadius || bankWidth == 0.0) { - return 1.0; - } - double t = StrictMath.min(1.0, (distance - channelRadius) / bankWidth); - double smooth = t * t * (3.0 - 2.0 * t); - return 1.0 - smooth; - } - - private static ClosestPoint closestCoveringPoint( - RiverReach reach, - double x, - double z, - double additionalRadius - ) { - RiverPolyline polyline = reach.polyline(); - RiverBodyProfile bodyProfile = reach.bodyProfile(); - double polylineLength = polyline.length(); - if (polylineLength == 0D) { - double distanceSquared = squared(x - polyline.x(0)) + squared(z - polyline.z(0)); - double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D) + additionalRadius; - return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null; - } - double nearest = Double.POSITIVE_INFINITY; - double nearestAlong = 0.0; - int pointLimit = polyline.size() - 1; - int profileLimit = bodyProfile.size() - 1; - for (int point = 0; point < pointLimit; point++) { - double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength; - double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength; - double segmentAlongSpan = segmentEndAlong - segmentStartAlong; - if (segmentAlongSpan == 0D) { - continue; - } - double startX = polyline.x(point); - double startZ = polyline.z(point); - double deltaX = polyline.x(point + 1) - startX; - double deltaZ = polyline.z(point + 1) - startZ; - int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong); - for (int profileIndex = firstProfileIndex; - profileIndex < profileLimit - && bodyProfile.position(profileIndex) <= segmentEndAlong; - profileIndex++) { - double profileStart = bodyProfile.position(profileIndex); - double profileEnd = bodyProfile.position(profileIndex + 1); - double overlapStart = StrictMath.max(segmentStartAlong, profileStart); - double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd); - if (overlapStart > overlapEnd) { - continue; - } - double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan; - double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan; - double profileSpan = profileEnd - profileStart; - double profileWidth = bodyProfile.widthAtIndex(profileIndex); - double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex); - double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1) - - profileWidth) / profileSpan; - double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1) - - profileBankWidth) / profileSpan; - double radiusBase = (profileWidth - + widthSlope * (segmentStartAlong - profileStart)) * 0.5D - + profileBankWidth - + bankSlope * (segmentStartAlong - profileStart) - + additionalRadius; - double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan; - ClosestPoint candidate = coveringPoint( - intervalStart, - intervalEnd, - deltaX, - startX - x, - deltaZ, - startZ - z, - radiusSlope, - radiusBase, - segmentStartAlong, - segmentAlongSpan - ); - if (candidate != null && candidate.distanceSquared() < nearest) { - nearest = candidate.distanceSquared(); - nearestAlong = candidate.alongReach(); - } - } - } - return Double.isFinite(nearest) ? new ClosestPoint(nearest, nearestAlong) : null; - } - - private static ClosestPoint closestCoveringPoint( - RiverReach reach, - double minimumX, - double minimumZ, - double maximumX, - double maximumZ - ) { - RiverPolyline polyline = reach.polyline(); - RiverBodyProfile bodyProfile = reach.bodyProfile(); - double polylineLength = polyline.length(); - if (polylineLength == 0D) { - double distanceSquared = pointRectangleDistanceSquared( - polyline.x(0), - polyline.z(0), - minimumX, - minimumZ, - maximumX, - maximumZ - ); - double radius = reach.widthAt(0D) * 0.5D + reach.bankWidthAt(0D); - return distanceSquared <= radius * radius ? new ClosestPoint(distanceSquared, 0D) : null; - } - double nearest = Double.POSITIVE_INFINITY; - double nearestAlong = 0.0; - int pointLimit = polyline.size() - 1; - int profileLimit = bodyProfile.size() - 1; - for (int point = 0; point < pointLimit; point++) { - double segmentStartAlong = polyline.cumulativeLength(point) / polylineLength; - double segmentEndAlong = polyline.cumulativeLength(point + 1) / polylineLength; - double segmentAlongSpan = segmentEndAlong - segmentStartAlong; - if (segmentAlongSpan == 0D) { - continue; - } - double startX = polyline.x(point); - double startZ = polyline.z(point); - double deltaX = polyline.x(point + 1) - startX; - double deltaZ = polyline.z(point + 1) - startZ; - int firstProfileIndex = bodyProfile.intervalIndex(segmentStartAlong); - for (int profileIndex = firstProfileIndex; - profileIndex < profileLimit - && bodyProfile.position(profileIndex) <= segmentEndAlong; - profileIndex++) { - double profileStart = bodyProfile.position(profileIndex); - double profileEnd = bodyProfile.position(profileIndex + 1); - double overlapStart = StrictMath.max(segmentStartAlong, profileStart); - double overlapEnd = StrictMath.min(segmentEndAlong, profileEnd); - if (overlapStart > overlapEnd) { - continue; - } - double intervalStart = (overlapStart - segmentStartAlong) / segmentAlongSpan; - double intervalEnd = (overlapEnd - segmentStartAlong) / segmentAlongSpan; - double profileSpan = profileEnd - profileStart; - double profileWidth = bodyProfile.widthAtIndex(profileIndex); - double profileBankWidth = bodyProfile.bankWidthAtIndex(profileIndex); - double widthSlope = (bodyProfile.widthAtIndex(profileIndex + 1) - - profileWidth) / profileSpan; - double bankSlope = (bodyProfile.bankWidthAtIndex(profileIndex + 1) - - profileBankWidth) / profileSpan; - double radiusBase = (profileWidth - + widthSlope * (segmentStartAlong - profileStart)) * 0.5D - + profileBankWidth - + bankSlope * (segmentStartAlong - profileStart); - double radiusSlope = (widthSlope * 0.5D + bankSlope) * segmentAlongSpan; - double cursor = intervalStart; - do { - double next = intervalEnd; - next = nextCrossing(startX, deltaX, minimumX, cursor, next); - next = nextCrossing(startX, deltaX, maximumX, cursor, next); - next = nextCrossing(startZ, deltaZ, minimumZ, cursor, next); - next = nextCrossing(startZ, deltaZ, maximumZ, cursor, next); - double middle = (cursor + next) * 0.5D; - double middleX = startX + deltaX * middle; - double middleZ = startZ + deltaZ * middle; - double distanceSlopeX = middleX < minimumX || middleX > maximumX ? deltaX : 0D; - double distanceBaseX = middleX < minimumX - ? startX - minimumX - : middleX > maximumX ? startX - maximumX : 0D; - double distanceSlopeZ = middleZ < minimumZ || middleZ > maximumZ ? deltaZ : 0D; - double distanceBaseZ = middleZ < minimumZ - ? startZ - minimumZ - : middleZ > maximumZ ? startZ - maximumZ : 0D; - ClosestPoint candidate = coveringPoint( - cursor, - next, - distanceSlopeX, - distanceBaseX, - distanceSlopeZ, - distanceBaseZ, - radiusSlope, - radiusBase, - segmentStartAlong, - segmentAlongSpan - ); - if (candidate != null && candidate.distanceSquared() < nearest) { - nearest = candidate.distanceSquared(); - nearestAlong = candidate.alongReach(); - } - cursor = next; - } while (cursor < intervalEnd); - } - } - return Double.isFinite(nearest) ? new ClosestPoint(nearest, nearestAlong) : null; - } - - private static ClosestPoint coveringPoint( - double intervalStart, - double intervalEnd, - double distanceSlopeX, - double distanceBaseX, - double distanceSlopeZ, - double distanceBaseZ, - double radiusSlope, - double radiusBase, - double segmentStartAlong, - double segmentAlongSpan - ) { - double distanceQuadratic = squared(distanceSlopeX) + squared(distanceSlopeZ); - double distanceLinear = 2D * (distanceSlopeX * distanceBaseX + distanceSlopeZ * distanceBaseZ); - double distanceConstant = squared(distanceBaseX) + squared(distanceBaseZ); - double coverageQuadratic = distanceQuadratic - squared(radiusSlope); - double coverageLinear = distanceLinear - 2D * radiusSlope * radiusBase; - double coverageConstant = distanceConstant - squared(radiusBase); - double distancePosition = distanceQuadratic == 0D - ? intervalStart - : clamp(-distanceLinear / (2D * distanceQuadratic), intervalStart, intervalEnd); - if (quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, distancePosition) <= 0D) { - return new ClosestPoint( - StrictMath.max(0D, quadraticValue( - distanceQuadratic, - distanceLinear, - distanceConstant, - distancePosition - )), - segmentStartAlong + segmentAlongSpan * distancePosition - ); - } - double coveragePosition = intervalStart; - double minimumCoverage = quadraticValue( - coverageQuadratic, - coverageLinear, - coverageConstant, - coveragePosition - ); - double endCoverage = quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, intervalEnd); - if (endCoverage < minimumCoverage) { - minimumCoverage = endCoverage; - coveragePosition = intervalEnd; - } - if (coverageQuadratic > 0D) { - double vertex = clamp(-coverageLinear / (2D * coverageQuadratic), intervalStart, intervalEnd); - double vertexCoverage = quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, vertex); - if (vertexCoverage < minimumCoverage) { - minimumCoverage = vertexCoverage; - coveragePosition = vertex; - } - } - if (minimumCoverage > 0D) { - return null; - } - double uncovered = distancePosition; - double covered = coveragePosition; - for (int iteration = 0; iteration < 40; iteration++) { - double middle = (uncovered + covered) * 0.5D; - if (quadraticValue(coverageQuadratic, coverageLinear, coverageConstant, middle) <= 0D) { - covered = middle; - } else { - uncovered = middle; - } - } - return new ClosestPoint( - StrictMath.max(0D, quadraticValue( - distanceQuadratic, - distanceLinear, - distanceConstant, - covered - )), - segmentStartAlong + segmentAlongSpan * covered - ); - } - - private static double nextCrossing( - double start, - double delta, - double boundary, - double cursor, - double currentNext - ) { - if (delta == 0D) { - return currentNext; - } - double crossing = (boundary - start) / delta; - return crossing > cursor && crossing < currentNext ? crossing : currentNext; - } - - private static double quadraticValue(double quadratic, double linear, double constant, double value) { - return (quadratic * value + linear) * value + constant; - } - - private static double clamp(double value, double minimum, double maximum) { - return StrictMath.max(minimum, StrictMath.min(maximum, value)); - } - - private static double pointRectangleDistanceSquared( - double x, - double z, - double minimumX, - double minimumZ, - double maximumX, - double maximumZ - ) { - double deltaX = x < minimumX ? minimumX - x : StrictMath.max(0.0, x - maximumX); - double deltaZ = z < minimumZ ? minimumZ - z : StrictMath.max(0.0, z - maximumZ); - return squared(deltaX) + squared(deltaZ); - } - - private static double squared(double value) { - return value * value; - } - - private static Map> createSpatialIndex(List reaches) { - HashMap> mutable = new HashMap<>(); - for (RiverReach reach : reaches) { - double radius = reach.width() * 0.5 + reach.bankWidth(); - RiverPolyline polyline = reach.polyline(); - for (int point = 0; point < polyline.size() - 1; point++) { - int minimumBucketX = bucket(StrictMath.min(polyline.x(point), polyline.x(point + 1)) - radius); - int maximumBucketX = bucket(StrictMath.max(polyline.x(point), polyline.x(point + 1)) + radius); - int minimumBucketZ = bucket(StrictMath.min(polyline.z(point), polyline.z(point + 1)) - radius); - int maximumBucketZ = bucket(StrictMath.max(polyline.z(point), polyline.z(point + 1)) + radius); - for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) { - for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) { - mutable.computeIfAbsent(bucketKey(bucketX, bucketZ), ignored -> new LinkedHashSet<>()).add(reach); - } - } - } - } - HashMap> immutable = new HashMap<>(mutable.size()); - for (Map.Entry> entry : mutable.entrySet()) { - immutable.put(entry.getKey(), List.copyOf(entry.getValue())); - } - return Map.copyOf(immutable); - } - - private static Map indexById(List reaches) { - HashMap indexed = new HashMap<>(reaches.size()); - for (RiverReach reach : reaches) { - RiverReach previous = indexed.put(reach.id(), reach); - if (previous != null) { - throw new IllegalArgumentException("River tile cannot contain duplicate reach IDs"); - } - } - return Map.copyOf(indexed); - } - - private List indexedReaches(double x, double z) { - return spatialIndex.getOrDefault(bucketKey(bucket(x), bucket(z)), List.of()); - } - - private List indexedReaches( - double queryMinimumX, - double queryMinimumZ, - double queryMaximumX, - double queryMaximumZ - ) { - LinkedHashSet indexed = new LinkedHashSet<>(); - int minimumBucketX = bucket(queryMinimumX); - int maximumBucketX = bucket(StrictMath.nextDown(queryMaximumX)); - int minimumBucketZ = bucket(queryMinimumZ); - int maximumBucketZ = bucket(StrictMath.nextDown(queryMaximumZ)); - if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) { - return spatialIndex.getOrDefault( - bucketKey(minimumBucketX, minimumBucketZ), - List.of() - ); - } - for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) { - for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) { - indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of())); - } - } - return List.copyOf(indexed); - } - - private List indexedReachesInclusive( - double queryMinimumX, - double queryMinimumZ, - double queryMaximumX, - double queryMaximumZ - ) { - int minimumBucketX = bucket(queryMinimumX); - int maximumBucketX = bucket(queryMaximumX); - int minimumBucketZ = bucket(queryMinimumZ); - int maximumBucketZ = bucket(queryMaximumZ); - if (minimumBucketX == maximumBucketX && minimumBucketZ == maximumBucketZ) { - return indexedReaches(queryMinimumX, queryMinimumZ); - } - LinkedHashSet indexed = new LinkedHashSet<>(); - for (int bucketX = minimumBucketX; bucketX <= maximumBucketX; bucketX++) { - for (int bucketZ = minimumBucketZ; bucketZ <= maximumBucketZ; bucketZ++) { - indexed.addAll(spatialIndex.getOrDefault(bucketKey(bucketX, bucketZ), List.of())); - } - } - return List.copyOf(indexed); - } - - private static int bucket(double coordinate) { - return (int) StrictMath.floor(coordinate / BUCKET_SIZE); - } - - private static long bucketKey(int bucketX, int bucketZ) { - return ((long) bucketX << 32) ^ (bucketZ & 0xFFFFFFFFL); - } - - private record Position(double x, double z, double alongReach) { - } - - private record ClosestPoint(double distanceSquared, double alongReach) { - } - -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTileCache.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTileCache.java deleted file mode 100644 index 2f89335ee..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTileCache.java +++ /dev/null @@ -1,200 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; - -public final class RiverTileCache implements AutoCloseable { - private final Object lock; - private final int maxCompletedEntries; - private final Map entries; - private final LinkedHashMap completedEntries; - private TileBuilder builder; - private boolean closed; - - public RiverTileCache(int maxCompletedEntries, TileBuilder builder) { - if (maxCompletedEntries < 1) { - throw new IllegalArgumentException("River tile cache capacity must be positive"); - } - this.maxCompletedEntries = maxCompletedEntries; - this.builder = Objects.requireNonNull(builder); - lock = new Object(); - entries = new HashMap<>(maxCompletedEntries); - completedEntries = new LinkedHashMap<>(maxCompletedEntries, 0.75f, true); - } - - public RiverTile get(int tileX, int tileZ) { - TileKey key = new TileKey(tileX, tileZ); - Entry entry; - TileBuilder activeBuilder; - boolean build; - synchronized (lock) { - requireOpen(); - entry = entries.get(key); - if (entry == null) { - entry = new Entry(); - entries.put(key, entry); - activeBuilder = builder; - build = true; - } else { - if (entry.completed) { - completedEntries.get(key); - } - activeBuilder = null; - build = false; - } - } - - if (build) { - build(key, entry, activeBuilder); - } - return await(entry.future, key); - } - - public int completedSize() { - synchronized (lock) { - return completedEntries.size(); - } - } - - public boolean isClosed() { - synchronized (lock) { - return closed; - } - } - - public void clear() { - List> invalidated; - synchronized (lock) { - requireOpen(); - invalidated = clearLocked(); - } - invalidate(invalidated, "River tile cache was cleared"); - } - - @Override - public void close() { - List> invalidated; - synchronized (lock) { - if (closed) { - return; - } - closed = true; - builder = null; - invalidated = clearLocked(); - } - invalidate(invalidated, "River tile cache was closed"); - } - - private void build(TileKey key, Entry entry, TileBuilder activeBuilder) { - try { - RiverTile tile = Objects.requireNonNull( - activeBuilder.build(key.tileX(), key.tileZ()), - "River tile builder returned null" - ); - if (tile.tileX() != key.tileX() || tile.tileZ() != key.tileZ()) { - throw new IllegalStateException( - "River tile builder returned " + tile.tileX() + "," + tile.tileZ() - + " for " + key.tileX() + "," + key.tileZ() - ); - } - publishCompleted(key, entry, tile); - } catch (Throwable failure) { - removeFailed(key, entry); - entry.future.completeExceptionally(failure); - } - } - - private void publishCompleted(TileKey key, Entry entry, RiverTile tile) { - synchronized (lock) { - if (closed || entries.get(key) != entry) { - entry.future.completeExceptionally(new IllegalStateException( - closed ? "River tile cache was closed" : "River tile cache entry was cleared" - )); - return; - } - entry.completed = true; - completedEntries.put(key, entry); - while (completedEntries.size() > maxCompletedEntries) { - Map.Entry eldest = completedEntries.entrySet().iterator().next(); - completedEntries.remove(eldest.getKey()); - entries.remove(eldest.getKey(), eldest.getValue()); - } - entry.future.complete(tile); - } - } - - private void removeFailed(TileKey key, Entry entry) { - synchronized (lock) { - entries.remove(key, entry); - completedEntries.remove(key, entry); - } - } - - private List> clearLocked() { - ArrayList> invalidated = new ArrayList<>(entries.size()); - for (Entry entry : entries.values()) { - if (!entry.future.isDone()) { - invalidated.add(entry.future); - } - } - entries.clear(); - completedEntries.clear(); - return invalidated; - } - - private void requireOpen() { - if (closed) { - throw new IllegalStateException("River tile cache is closed"); - } - } - - private static RiverTile await(CompletableFuture future, TileKey key) { - try { - return future.join(); - } catch (CompletionException failure) { - Throwable cause = failure.getCause(); - if (cause instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - if (cause instanceof RuntimeException runtimeException) { - throw runtimeException; - } - if (cause instanceof Error error) { - throw error; - } - throw new IllegalStateException( - "Failed to build river tile " + key.tileX() + "," + key.tileZ(), - cause - ); - } - } - - private static void invalidate(List> futures, String message) { - for (CompletableFuture future : futures) { - future.completeExceptionally(new IllegalStateException(message)); - } - } - - @FunctionalInterface - public interface TileBuilder { - RiverTile build(int tileX, int tileZ) throws Exception; - } - - private record TileKey(int tileX, int tileZ) { - } - - private static final class Entry { - private final CompletableFuture future; - private boolean completed; - - private Entry() { - future = new CompletableFuture<>(); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverTopologyComplexity.java b/core/src/main/java/art/arcane/iris/engine/river/RiverTopologyComplexity.java deleted file mode 100644 index a6261ba6e..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverTopologyComplexity.java +++ /dev/null @@ -1,211 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.ArrayList; -import java.util.List; - -public final class RiverTopologyComplexity { - public static final long MAXIMUM_SOURCE_WINDOW_CELLS = 65_536L; - public static final long MAXIMUM_ROUTE_SCAN_STEPS = 65_536L; - public static final long MAXIMUM_BUCKET_WRITES_PER_REACH = 1_048_576L; - public static final long MAXIMUM_TUNNEL_SAMPLE_COLUMNS = 65_536L; - private static final int SPATIAL_BUCKET_SIZE = 64; - - private RiverTopologyComplexity() { - } - - public static Estimate estimate( - int cellSize, - int tileCells, - double siteJitter, - int maxRouteReaches, - double maximumReachRadius, - double maximumWormOffset, - int maximumWormSegments - ) { - double maximumEdgeAxisDelta = cellSize * (1D + siteJitter); - long geometryPaddingCells = 1L + ceilToLong( - (maximumReachRadius + maximumWormOffset) / cellSize - ); - long targetWindowAxis = saturatedAdd(tileCells, saturatedMultiply(2L, geometryPaddingCells)); - long sourceWindowAxis = saturatedAdd( - targetWindowAxis, - saturatedMultiply(2L, maxRouteReaches) - ); - long sourceWindowCells = saturatedMultiply(sourceWindowAxis, sourceWindowAxis); - long maximumRouteScanSteps = saturatedMultiply(sourceWindowCells, maxRouteReaches); - double maximumSegmentSpan = maximumEdgeAxisDelta - + maximumWormOffset * 2D - + maximumReachRadius * 2D; - long maximumSegmentBucketAxis = saturatedAdd( - ceilToLong(maximumSegmentSpan / SPATIAL_BUCKET_SIZE), - 1L - ); - long maximumSegmentBucketCount = saturatedMultiply( - maximumSegmentBucketAxis, - maximumSegmentBucketAxis - ); - long maximumBucketWritesPerReach = saturatedMultiply( - maximumSegmentBucketCount, - maximumWormSegments - ); - return new Estimate( - geometryPaddingCells, - sourceWindowAxis, - sourceWindowCells, - maximumRouteScanSteps, - maximumSegmentBucketAxis, - maximumBucketWritesPerReach - ); - } - - public static void requireSafe( - int cellSize, - int tileCells, - double siteJitter, - int maxRouteReaches, - double maximumReachRadius, - double maximumWormOffset, - int maximumWormSegments - ) { - Estimate estimate = estimate( - cellSize, - tileCells, - siteJitter, - maxRouteReaches, - maximumReachRadius, - maximumWormOffset, - maximumWormSegments - ); - List violations = estimate.violations(); - if (!violations.isEmpty()) { - throw new IllegalArgumentException(String.join(" ", violations)); - } - } - - public static int tunnelHalo( - double maximumChannelWidth, - double maximumTunnelWidthMultiplier, - double tunnelMouthBlend - ) { - if (!Double.isFinite(maximumChannelWidth) || maximumChannelWidth <= 0D - || !Double.isFinite(maximumTunnelWidthMultiplier) || maximumTunnelWidthMultiplier < 1D - || !Double.isFinite(tunnelMouthBlend) || tunnelMouthBlend < 0D) { - throw new IllegalArgumentException("River tunnel dimensions must be finite and valid"); - } - return Math.max( - 1, - (int) StrictMath.ceil( - maximumChannelWidth * 0.5D * maximumTunnelWidthMultiplier + tunnelMouthBlend - ) + 1 - ); - } - - public static long tunnelSampleColumns( - double maximumChannelWidth, - double maximumTunnelWidthMultiplier, - double tunnelMouthBlend - ) { - long axis = 16L + 2L * tunnelHalo( - maximumChannelWidth, - maximumTunnelWidthMultiplier, - tunnelMouthBlend - ); - return saturatedMultiply(axis, axis); - } - - public static String tunnelPlanViolation( - double maximumChannelWidth, - double maximumTunnelWidthMultiplier, - double tunnelMouthBlend - ) { - long columns = tunnelSampleColumns( - maximumChannelWidth, - maximumTunnelWidthMultiplier, - tunnelMouthBlend - ); - if (columns <= MAXIMUM_TUNNEL_SAMPLE_COLUMNS) { - return null; - } - return "River tunnel planning may sample " + columns - + " columns per generated chunk, above the safe limit of " + MAXIMUM_TUNNEL_SAMPLE_COLUMNS - + "; reduce maxChannelWidth, tunnelWidthMultiplier.max, or tunnelMouthBlend."; - } - - public static void requireSafeTunnelPlan( - double maximumChannelWidth, - double maximumTunnelWidthMultiplier, - double tunnelMouthBlend - ) { - String violation = tunnelPlanViolation( - maximumChannelWidth, - maximumTunnelWidthMultiplier, - tunnelMouthBlend - ); - if (violation != null) { - throw new IllegalArgumentException(violation); - } - } - - private static long ceilToLong(double value) { - if (!Double.isFinite(value) || value >= Long.MAX_VALUE) { - return Long.MAX_VALUE; - } - if (value <= 0D) { - return 0L; - } - return (long) StrictMath.ceil(value); - } - - private static long saturatedAdd(long first, long second) { - if (first > Long.MAX_VALUE - second) { - return Long.MAX_VALUE; - } - return first + second; - } - - private static long saturatedMultiply(long first, long second) { - if (first == 0L || second == 0L) { - return 0L; - } - if (first > Long.MAX_VALUE / second) { - return Long.MAX_VALUE; - } - return first * second; - } - - public record Estimate( - long geometryPaddingCells, - long sourceWindowAxis, - long sourceWindowCells, - long maximumRouteScanSteps, - long maximumSegmentBucketAxis, - long maximumBucketWritesPerReach - ) { - public boolean safe() { - return violations().isEmpty(); - } - - public List violations() { - ArrayList violations = new ArrayList<>(3); - if (sourceWindowCells > MAXIMUM_SOURCE_WINDOW_CELLS) { - violations.add("River topology source window requires " + sourceWindowCells - + " cells (" + sourceWindowAxis + " per axis), above the safe limit of " - + MAXIMUM_SOURCE_WINDOW_CELLS - + "; increase cellSize or reduce tileCells, maxRouteReaches, channel width, or bank width."); - } - if (maximumRouteScanSteps > MAXIMUM_ROUTE_SCAN_STEPS) { - violations.add("River topology route scan permits " + maximumRouteScanSteps - + " source-to-reach steps, above the safe limit of " + MAXIMUM_ROUTE_SCAN_STEPS - + "; reduce maxRouteReaches, tileCells, channel width, or bank width."); - } - if (maximumBucketWritesPerReach > MAXIMUM_BUCKET_WRITES_PER_REACH) { - violations.add("River topology spatial index may require " + maximumBucketWritesPerReach - + " bucket writes for one reach (" + maximumSegmentBucketAxis - + " buckets per segment axis), above the safe limit of " - + MAXIMUM_BUCKET_WRITES_PER_REACH - + "; reduce channel width, bank width, orderWidthFactor, worm maxOffset, or worm segments."); - } - return List.copyOf(violations); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/RiverWorm.java b/core/src/main/java/art/arcane/iris/engine/river/RiverWorm.java deleted file mode 100644 index 539d3b54b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/RiverWorm.java +++ /dev/null @@ -1,81 +0,0 @@ -package art.arcane.iris.engine.river; - -import java.util.List; - -public record RiverWorm( - String id, - long seed, - double weight, - double wavelength, - double detailWavelength, - double tortuosity, - double detailTortuosity, - double maxOffset, - int segments, - double widthMultiplier, - double bankMultiplier, - double depthMultiplier, - double bodyWavelength, - double bodyDetailWavelength, - double bodyDetailInfluence, - double widthVariation, - double bankVariation, - double depthVariation, - double roofVariation, - int branchCap, - double branchDecay, - double confluenceMultiplier, - double childChance, - double branchChildChance, - List children -) { - public RiverWorm { - if (id == null || !id.matches("[a-z0-9][a-z0-9_-]{0,63}")) { - throw new IllegalArgumentException("id must use 1 to 64 lowercase letters, digits, underscores, or hyphens"); - } - requireRange(weight, 0.000001D, 1000000D, "weight"); - requireRange(wavelength, 8D, 16384D, "wavelength"); - requireRange(detailWavelength, 8D, 16384D, "detailWavelength"); - requireRange(tortuosity, 0D, 1D, "tortuosity"); - requireRange(detailTortuosity, 0D, 1D, "detailTortuosity"); - requireRange(maxOffset, 0D, 1024D, "maxOffset"); - if (segments < 1 || segments > 64) { - throw new IllegalArgumentException("segments must be between 1 and 64"); - } - requireRange(widthMultiplier, 0.125D, 8D, "widthMultiplier"); - requireRange(bankMultiplier, 0.125D, 8D, "bankMultiplier"); - requireRange(depthMultiplier, 0.125D, 8D, "depthMultiplier"); - requireRange(bodyWavelength, 8D, 16384D, "bodyWavelength"); - requireRange(bodyDetailWavelength, 8D, 16384D, "bodyDetailWavelength"); - requireRange(bodyDetailInfluence, 0D, 1D, "bodyDetailInfluence"); - requireRange(widthVariation, 0D, 0.875D, "widthVariation"); - requireRange(bankVariation, 0D, 0.875D, "bankVariation"); - requireRange(depthVariation, 0D, 0.875D, "depthVariation"); - requireRange(roofVariation, 0D, 0.875D, "roofVariation"); - if (branchCap < 1 || branchCap > 8) { - throw new IllegalArgumentException("branchCap must be between 1 and 8"); - } - requireRange(branchDecay, 0D, 1D, "branchDecay"); - requireRange(confluenceMultiplier, 0D, 8D, "confluenceMultiplier"); - requireRange(childChance, 0D, 1D, "childChance"); - requireRange(branchChildChance, 0D, 1D, "branchChildChance"); - if (children == null) { - throw new IllegalArgumentException("children must not be null"); - } - if (children.size() > 16) { - throw new IllegalArgumentException("children must contain at most 16 profiles"); - } - for (RiverWorm child : children) { - if (child == null) { - throw new IllegalArgumentException("children must not contain null profiles"); - } - } - children = List.copyOf(children); - } - - private static void requireRange(double value, double minimum, double maximum, String name) { - if (!Double.isFinite(value) || value < minimum || value > maximum) { - throw new IllegalArgumentException(name + " must be between " + minimum + " and " + maximum); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/CavePosition.java b/core/src/main/java/art/arcane/iris/engine/river/cave/CavePosition.java deleted file mode 100644 index 38693c462..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/CavePosition.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public record CavePosition(int x, int y, int z) { - public CavePosition offset(int dx, int dy, int dz) { - return new CavePosition(x + dx, y + dy, z + dz); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxel.java b/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxel.java deleted file mode 100644 index 6ea8abc31..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxel.java +++ /dev/null @@ -1,9 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public enum CaveVoxel { - SOLID, - CAVE_AIR, - COMPATIBLE_FLUID, - LAVA, - INCOMPATIBLE_FLUID -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxelPrecondition.java b/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxelPrecondition.java deleted file mode 100644 index 2bd2ce6a1..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxelPrecondition.java +++ /dev/null @@ -1,9 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import java.util.Objects; - -public record CaveVoxelPrecondition(CaveVoxel voxel, boolean openToSurface) { - public CaveVoxelPrecondition { - Objects.requireNonNull(voxel); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxelView.java b/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxelView.java deleted file mode 100644 index 990a05682..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/CaveVoxelView.java +++ /dev/null @@ -1,9 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public interface CaveVoxelView { - boolean isInWorld(CavePosition position); - - CaveVoxel voxelAt(CavePosition position); - - boolean isOpenToSurface(CavePosition position); -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveAction.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveAction.java deleted file mode 100644 index 430fdd352..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveAction.java +++ /dev/null @@ -1,8 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public enum RiverCaveAction { - WET_SOURCE, - FALLING_FLUID, - DRY_AIR, - SEAL_GUARD -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveContainmentPlanner.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveContainmentPlanner.java deleted file mode 100644 index 2db8771e3..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveContainmentPlanner.java +++ /dev/null @@ -1,1086 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -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.OptionalLong; -import java.util.Queue; -import java.util.Set; - -public final class RiverCaveContainmentPlanner { - private static final List DIRECTIONS = List.of( - new CavePosition(1, 0, 0), - new CavePosition(-1, 0, 0), - new CavePosition(0, 1, 0), - new CavePosition(0, -1, 0), - new CavePosition(0, 0, 1), - new CavePosition(0, 0, -1) - ); - private static final Comparator SOURCE_PRIORITY = Comparator - .comparingInt(RiverCaveSource::waterHeadY) - .reversed() - .thenComparingLong(RiverCaveSource::sourceId) - .thenComparingInt(source -> source.entry().x()) - .thenComparingInt(source -> source.entry().y()) - .thenComparingInt(source -> source.entry().z()) - .thenComparingInt(source -> source.target().x()) - .thenComparingInt(source -> source.target().y()) - .thenComparingInt(source -> source.target().z()) - .thenComparing(RiverCaveSource::mode); - - public RiverCavePlan plan( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings - ) { - Objects.requireNonNull(view); - Objects.requireNonNull(source); - Objects.requireNonNull(settings); - - RiverCaveRejection sourceRejection = validateSource(source); - if (sourceRejection != RiverCaveRejection.NONE) { - return rejected(source, sourceRejection); - } - - PathResult throat = buildThroat(view, source, settings); - if (throat.rejection() != RiverCaveRejection.NONE) { - return rejected(source, throat.rejection()); - } - - return switch (source.mode()) { - case CLOSED_COMPONENT -> planClosedComponent(view, source, settings, throat.positions()); - case GENERATED_GROTTO -> planGeneratedGrotto(view, source, settings, throat.positions()); - case GROTTO_OR_CLOSED_COMPONENT -> planGrottoOrClosedComponent( - view, - source, - settings, - throat.positions() - ); - case WATERFALL_POOL -> planWaterfallPool( - view, - source, - settings, - throat.positions() - ); - case DEEP_POOL -> planDeepPool(view, source, settings, throat.positions()); - }; - } - - public RiverCavePlanningResult planAll( - CaveVoxelView view, - Collection sources, - RiverCavePlannerSettings settings - ) { - Objects.requireNonNull(view); - Objects.requireNonNull(sources); - Objects.requireNonNull(settings); - - List orderedSources = new ArrayList<>(sources); - orderedSources.sort(SOURCE_PRIORITY); - List plans = new ArrayList<>(orderedSources.size()); - Map combinedActions = new LinkedHashMap<>(); - Map claimedBy = new HashMap<>(); - Map combinedPreconditions = new LinkedHashMap<>(); - - for (RiverCaveSource source : orderedSources) { - RiverCavePlan candidate = plan(view, source, settings); - if (!candidate.accepted()) { - plans.add(candidate); - continue; - } - OptionalLong winnerSourceId = findWinningSourceId( - candidate.actions().keySet(), - claimedBy - ); - if (winnerSourceId.isPresent()) { - plans.add(rejectedOverlap(source, winnerSourceId.getAsLong())); - } else { - plans.add(candidate); - combinedActions.putAll(candidate.actions()); - combinedPreconditions.putAll(candidate.baselinePreconditions()); - } - for (CavePosition position : candidate.actions().keySet()) { - claimedBy.putIfAbsent(position, source); - } - } - - return new RiverCavePlanningResult(plans, combinedActions, combinedPreconditions); - } - - private RiverCavePlan planGrottoOrClosedComponent( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - CaveVoxel targetVoxel = voxelAt(view, source.target()); - if (isFluidReachable(targetVoxel, settings)) { - return planClosedComponent(view, source, settings, throat); - } - if (targetVoxel == CaveVoxel.LAVA) { - return rejected(source, RiverCaveRejection.LAVA_CONTACT); - } - if (targetVoxel == CaveVoxel.INCOMPATIBLE_FLUID) { - return rejected(source, RiverCaveRejection.INCOMPATIBLE_FLUID); - } - return planGeneratedGrotto(view, source, settings, throat); - } - - private RiverCavePlan planWaterfallPool( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - if (!view.isOpenToSurface(source.target())) { - return planGrottoOrClosedComponent(view, source, settings, throat); - } - RiverCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat); - if (dryThroatRejection != RiverCaveRejection.NONE) { - return rejected(source, dryThroatRejection); - } - RiverCaveRejection shaftRejection = validateWaterfallShaft(view, source, settings, throat); - if (shaftRejection != RiverCaveRejection.NONE) { - return rejected(source, shaftRejection); - } - CaveVoxel targetVoxel = voxelAt(view, source.target()); - if (!isFluidReachable(targetVoxel, settings)) { - return rejected(source, rejectionForTarget(targetVoxel, settings)); - } - - Map actions = new HashMap<>(); - addThroatActions(actions, throat, source); - addSealGuards(view, source, actions); - return accepted(view, source, actions); - } - - private RiverCavePlan planClosedComponent( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - RiverCaveRejection dryThroatRejection = validateDryThroatContacts(view, source, throat); - if (dryThroatRejection != RiverCaveRejection.NONE) { - return rejected(source, dryThroatRejection); - } - RiverCaveRejection waterfallRejection = validateWaterfallShaft(view, source, settings, throat); - if (waterfallRejection != RiverCaveRejection.NONE) { - return rejected(source, waterfallRejection); - } - - CaveVoxel targetVoxel = voxelAt(view, source.target()); - if (!isFluidReachable(targetVoxel, settings)) { - return rejected(source, rejectionForTarget(targetVoxel, settings)); - } - - ComponentResult component = resolveClosedComponent(view, source, settings, throat); - if (component.rejection() != RiverCaveRejection.NONE) { - return rejected(source, component.rejection()); - } - - Map actions = new HashMap<>(); - addThroatActions(actions, throat, source); - for (CavePosition position : component.positions()) { - actions.put(position, RiverCaveAction.WET_SOURCE); - } - addSealGuards(view, source, actions); - return accepted(view, source, actions); - } - - private RiverCaveRejection validateDryThroatContacts( - CaveVoxelView view, - RiverCaveSource source, - List throat - ) { - Set throatPositions = Set.copyOf(throat); - for (CavePosition position : throat) { - if (position.y() <= source.waterHeadY()) { - continue; - } - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - CaveVoxel voxel = voxelAt(view, neighbor); - if (voxel == CaveVoxel.LAVA) { - return RiverCaveRejection.LAVA_CONTACT; - } - if (voxel == CaveVoxel.COMPATIBLE_FLUID) { - return RiverCaveRejection.EXISTING_FLUID; - } - if (voxel == CaveVoxel.INCOMPATIBLE_FLUID) { - return RiverCaveRejection.INCOMPATIBLE_FLUID; - } - } - } - return RiverCaveRejection.NONE; - } - - private RiverCaveRejection validateWaterfallShaft( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - if (source.mode() != RiverCaveMode.WATERFALL_POOL) { - return RiverCaveRejection.NONE; - } - - Set throatPositions = Set.copyOf(throat); - for (CavePosition position : throat) { - if (position.y() <= source.waterHeadY()) { - continue; - } - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (throatPositions.contains(neighbor) || isInletOpening(source, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor); - if (boundsRejection != RiverCaveRejection.NONE) { - return boundsRejection; - } - CaveVoxel voxel = voxelAt(view, neighbor); - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return hazard; - } - if (voxel != CaveVoxel.SOLID) { - return RiverCaveRejection.WATERFALL_SHAFT_OPEN; - } - } - } - return RiverCaveRejection.NONE; - } - - private RiverCavePlan planGeneratedGrotto( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - GrottoResult grotto = buildGrotto(source, settings); - if (grotto.rejection() != RiverCaveRejection.NONE) { - return rejected(source, grotto.rejection()); - } - Set chamber = grotto.positions(); - - Set carve = new HashSet<>(chamber.size() + throat.size()); - carve.addAll(chamber); - carve.addAll(throat); - RiverCaveRejection carveRejection = validateGeneratedCarve(view, source, settings, carve); - if (carveRejection != RiverCaveRejection.NONE) { - return rejected(source, carveRejection); - } - - BoundaryResult boundary = validateGeneratedBoundary(view, source, settings, carve); - if (boundary.rejection() != RiverCaveRejection.NONE) { - return rejected(source, boundary.rejection()); - } - - Map actions = new HashMap<>(); - addChamberActions(actions, chamber, source.waterHeadY()); - addThroatActions(actions, throat, source); - for (CavePosition position : boundary.sealGuards()) { - actions.put(position, RiverCaveAction.SEAL_GUARD); - } - return accepted(view, source, actions); - } - - private RiverCavePlan planDeepPool( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - GrottoResult grotto = buildGrotto(source, settings); - if (grotto.rejection() != RiverCaveRejection.NONE) { - return rejected(source, grotto.rejection()); - } - Set chamber = grotto.positions(); - Set carve = new HashSet<>(chamber.size() + throat.size()); - carve.addAll(chamber); - carve.addAll(throat); - - RiverCaveRejection carveRejection = validateDeepPoolCarve(view, source, settings, carve); - if (carveRejection != RiverCaveRejection.NONE) { - return rejected(source, carveRejection); - } - BoundaryResult boundary = validateDeepPoolBoundary(view, source, settings, carve); - if (boundary.rejection() != RiverCaveRejection.NONE) { - return rejected(source, boundary.rejection()); - } - - Map actions = new HashMap<>(); - addChamberActions(actions, chamber, source.waterHeadY()); - addThroatActions(actions, throat, source); - for (CavePosition position : boundary.sealGuards()) { - actions.put(position, RiverCaveAction.SEAL_GUARD); - } - return accepted(view, source, actions); - } - - private RiverCaveRejection validateDeepPoolCarve( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - Set carve - ) { - for (CavePosition position : carve) { - if (!view.isInWorld(position)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != RiverCaveRejection.NONE) { - return boundsRejection; - } - CaveVoxel voxel = voxelAt(view, position); - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return hazard; - } - if (voxel == CaveVoxel.SOLID) { - continue; - } - if (position.y() > source.waterHeadY() - && voxel == CaveVoxel.CAVE_AIR - && !view.isOpenToSurface(position)) { - continue; - } - return RiverCaveRejection.GROTTO_INTERSECTION; - } - return RiverCaveRejection.NONE; - } - - private BoundaryResult validateDeepPoolBoundary( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - Set carve - ) { - Set guards = new HashSet<>(); - for (CavePosition position : carve) { - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (carve.contains(neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return BoundaryResult.rejected(RiverCaveRejection.WORLD_BOUNDARY); - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor); - if (boundsRejection != RiverCaveRejection.NONE) { - return BoundaryResult.rejected(boundsRejection); - } - CaveVoxel voxel = voxelAt(view, neighbor); - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return BoundaryResult.rejected(hazard); - } - if (voxel == CaveVoxel.SOLID) { - guards.add(neighbor); - continue; - } - if (neighbor.y() > source.waterHeadY() - && voxel == CaveVoxel.CAVE_AIR - && !view.isOpenToSurface(neighbor)) { - continue; - } - return BoundaryResult.rejected(RiverCaveRejection.GROTTO_SHELL_OPEN); - } - } - return BoundaryResult.accepted(guards); - } - - private ComponentResult resolveClosedComponent( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat - ) { - Queue queue = new ArrayDeque<>(); - Set queued = new HashSet<>(); - Set resolved = new HashSet<>(); - - queue.add(source.target()); - queued.add(source.target()); - RiverCaveRejection seedRejection = addThroatContacts(view, source, settings, throat, queue, queued); - if (seedRejection != RiverCaveRejection.NONE) { - return ComponentResult.rejected(seedRejection); - } - - while (!queue.isEmpty()) { - CavePosition position = queue.remove(); - RiverCaveRejection positionRejection = validateReachablePosition(view, source, settings, position); - if (positionRejection != RiverCaveRejection.NONE) { - return ComponentResult.rejected(positionRejection); - } - if (!resolved.add(position)) { - continue; - } - if (resolved.size() > settings.maxFloodVolume()) { - return ComponentResult.rejected(RiverCaveRejection.VOLUME_LIMIT); - } - - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - RiverCaveRejection neighborRejection = inspectReachableNeighbor( - view, - source, - settings, - neighbor, - queue, - queued - ); - if (neighborRejection != RiverCaveRejection.NONE) { - return ComponentResult.rejected(neighborRejection); - } - } - } - - return ComponentResult.accepted(resolved); - } - - private RiverCaveRejection addThroatContacts( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List throat, - Queue queue, - Set queued - ) { - for (CavePosition position : throat) { - if (position.y() > source.waterHeadY()) { - continue; - } - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - RiverCaveRejection rejection = inspectReachableNeighbor( - view, - source, - settings, - neighbor, - queue, - queued - ); - if (rejection != RiverCaveRejection.NONE) { - return rejection; - } - } - } - return RiverCaveRejection.NONE; - } - - private RiverCaveRejection inspectReachableNeighbor( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - CavePosition position, - Queue queue, - Set queued - ) { - if (position.y() > source.waterHeadY()) { - return inspectAboveHeadNeighbor(view, source, position); - } - if (!view.isInWorld(position)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - - CaveVoxel voxel = voxelAt(view, position); - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return hazard; - } - if (!isFluidReachable(voxel, settings)) { - return RiverCaveRejection.NONE; - } - - RiverCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != RiverCaveRejection.NONE) { - return boundsRejection; - } - if (queued.add(position)) { - queue.add(position); - } - return RiverCaveRejection.NONE; - } - - private RiverCaveRejection inspectAboveHeadNeighbor( - CaveVoxelView view, - RiverCaveSource source, - CavePosition position - ) { - if (isInletOpening(source, position) || !view.isInWorld(position)) { - return RiverCaveRejection.NONE; - } - CaveVoxel voxel = voxelAt(view, position); - return switch (voxel) { - case LAVA -> RiverCaveRejection.LAVA_CONTACT; - case COMPATIBLE_FLUID -> RiverCaveRejection.EXISTING_FLUID; - case INCOMPATIBLE_FLUID -> RiverCaveRejection.INCOMPATIBLE_FLUID; - default -> RiverCaveRejection.NONE; - }; - } - - private RiverCaveRejection validateReachablePosition( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - CavePosition position - ) { - if (!view.isInWorld(position)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != RiverCaveRejection.NONE) { - return boundsRejection; - } - if (view.isOpenToSurface(position)) { - return RiverCaveRejection.OPEN_SURFACE; - } - CaveVoxel voxel = voxelAt(view, position); - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return hazard; - } - return isFluidReachable(voxel, settings) - ? RiverCaveRejection.NONE - : RiverCaveRejection.NO_CAVE_TARGET; - } - - private PathResult buildThroat( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings - ) { - CavePosition entry = source.entry(); - CavePosition target = source.target(); - int deltaX = target.x() - entry.x(); - int deltaY = target.y() - entry.y(); - int deltaZ = target.z() - entry.z(); - int movesX = Math.abs(deltaX); - int movesY = Math.abs(deltaY); - int movesZ = Math.abs(deltaZ); - int length = movesX + movesY + movesZ; - if (length > settings.maxThroatLength()) { - return PathResult.rejected(RiverCaveRejection.THROAT_LIMIT); - } - - int stepX = Integer.signum(deltaX); - int stepY = Integer.signum(deltaY); - int stepZ = Integer.signum(deltaZ); - int usedX = 0; - int usedY = 0; - int usedZ = 0; - CavePosition current = entry; - List positions = new ArrayList<>(length + 1); - - while (true) { - RiverCaveRejection positionRejection = validateThroatPosition(view, source, settings, current); - if (positionRejection != RiverCaveRejection.NONE) { - return PathResult.rejected(positionRejection); - } - positions.add(current); - if (current.equals(target)) { - return expandThroat(view, source, settings, positions); - } - - int axis = selectNextAxis(source.sourceId(), movesX, movesY, movesZ, usedX, usedY, usedZ); - if (axis == 0) { - current = current.offset(stepX, 0, 0); - usedX++; - } else if (axis == 1) { - current = current.offset(0, stepY, 0); - usedY++; - } else { - current = current.offset(0, 0, stepZ); - usedZ++; - } - } - } - - private PathResult expandThroat( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - List centerline - ) { - int radius = settings.throatRadius(); - int extent = radius - 1; - int radiusSquared = radius * radius; - Set expanded = new LinkedHashSet<>(); - for (CavePosition center : centerline) { - for (int dx = -extent; dx <= extent; dx++) { - for (int dy = -extent; dy <= extent; dy++) { - for (int dz = -extent; dz <= extent; dz++) { - if ((dx * dx) + (dy * dy) + (dz * dz) >= radiusSquared) { - continue; - } - CavePosition position = center.offset(dx, dy, dz); - if (position.y() > source.entry().y()) { - continue; - } - RiverCaveRejection rejection = validateThroatPosition(view, source, settings, position); - if (rejection != RiverCaveRejection.NONE) { - return PathResult.rejected(rejection); - } - expanded.add(position); - if (expanded.size() > settings.maxFloodVolume()) { - return PathResult.rejected(RiverCaveRejection.VOLUME_LIMIT); - } - } - } - } - } - return PathResult.accepted(List.copyOf(expanded)); - } - - private int selectNextAxis( - long sourceId, - int movesX, - int movesY, - int movesZ, - int usedX, - int usedY, - int usedZ - ) { - double scoreX = nextAxisScore(movesX, usedX); - double scoreY = nextAxisScore(movesY, usedY); - double scoreZ = nextAxisScore(movesZ, usedZ); - double minimum = Math.min(scoreX, Math.min(scoreY, scoreZ)); - int tieOffset = Math.floorMod(sourceId, 3); - for (int offset = 0; offset < 3; offset++) { - int axis = (tieOffset + offset) % 3; - double score = axis == 0 ? scoreX : axis == 1 ? scoreY : scoreZ; - if (score == minimum) { - return axis; - } - } - throw new IllegalStateException("No remaining throat axis"); - } - - private double nextAxisScore(int moves, int used) { - if (used >= moves) { - return Double.POSITIVE_INFINITY; - } - return ((2D * used) + 1D) / moves; - } - - private RiverCaveRejection validateThroatPosition( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - CavePosition position - ) { - if (!view.isInWorld(position)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != RiverCaveRejection.NONE) { - return boundsRejection; - } - return rejectionForHazard(voxelAt(view, position), settings); - } - - private GrottoResult buildGrotto(RiverCaveSource source, RiverCavePlannerSettings settings) { - int horizontalRadius = settings.grottoHorizontalRadius(); - int verticalRadius = settings.grottoVerticalRadius(); - Set candidates = new HashSet<>(); - - for (int dx = -horizontalRadius; dx <= horizontalRadius; dx++) { - for (int dy = -verticalRadius; dy <= verticalRadius; dy++) { - for (int dz = -horizontalRadius; dz <= horizontalRadius; dz++) { - if (settings.grottoShape().contains(source, settings, dx, dy, dz)) { - candidates.add(source.target().offset(dx, dy, dz)); - if (candidates.size() > settings.maxFloodVolume()) { - return GrottoResult.rejected(RiverCaveRejection.VOLUME_LIMIT); - } - } - } - } - } - candidates.add(source.target()); - for (int offset = 1; offset <= settings.dryHeadroom(); offset++) { - CavePosition headroom = new CavePosition( - source.target().x(), source.waterHeadY() + offset, source.target().z()); - if (Math.abs(headroom.y() - source.target().y()) > verticalRadius) { - return GrottoResult.rejected(RiverCaveRejection.DRY_HEADROOM_LIMIT); - } - candidates.add(headroom); - if (candidates.size() > settings.maxFloodVolume()) { - return GrottoResult.rejected(RiverCaveRejection.VOLUME_LIMIT); - } - } - return GrottoResult.accepted(connectedGrotto(source.target(), candidates)); - } - - private Set connectedGrotto(CavePosition target, Set candidates) { - Queue queue = new ArrayDeque<>(); - Set connected = new HashSet<>(); - queue.add(target); - connected.add(target); - while (!queue.isEmpty()) { - CavePosition position = queue.remove(); - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (candidates.contains(neighbor) && connected.add(neighbor)) { - queue.add(neighbor); - } - } - } - return connected; - } - - private RiverCaveRejection validateGeneratedCarve( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - Set carve - ) { - for (CavePosition position : carve) { - if (!view.isInWorld(position)) { - return RiverCaveRejection.WORLD_BOUNDARY; - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, position); - if (boundsRejection != RiverCaveRejection.NONE) { - return boundsRejection; - } - CaveVoxel voxel = voxelAt(view, position); - if (isGeneratedInletCarve(view, source, settings, position, voxel)) { - continue; - } - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return hazard; - } - if (voxel != CaveVoxel.SOLID) { - return RiverCaveRejection.GROTTO_INTERSECTION; - } - } - return RiverCaveRejection.NONE; - } - - private BoundaryResult validateGeneratedBoundary( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - Set carve - ) { - Set guards = new HashSet<>(); - for (CavePosition position : carve) { - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (carve.contains(neighbor)) { - continue; - } - if (isInletOpening(source, neighbor) - || isGeneratedInletOpening(view, source, settings, neighbor)) { - continue; - } - if (!view.isInWorld(neighbor)) { - return BoundaryResult.rejected(RiverCaveRejection.WORLD_BOUNDARY); - } - RiverCaveRejection boundsRejection = validateBounds(source, settings, neighbor); - if (boundsRejection != RiverCaveRejection.NONE) { - return BoundaryResult.rejected(boundsRejection); - } - CaveVoxel voxel = voxelAt(view, neighbor); - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - if (hazard != RiverCaveRejection.NONE) { - return BoundaryResult.rejected(hazard); - } - if (voxel != CaveVoxel.SOLID) { - return BoundaryResult.rejected(RiverCaveRejection.GROTTO_SHELL_OPEN); - } - guards.add(neighbor); - } - } - return BoundaryResult.accepted(guards); - } - - private void addChamberActions( - Map actions, - Collection positions, - int waterHeadY - ) { - for (CavePosition position : positions) { - RiverCaveAction action = position.y() <= waterHeadY - ? RiverCaveAction.WET_SOURCE - : RiverCaveAction.DRY_AIR; - actions.put(position, action); - } - } - - private void addThroatActions( - Map actions, - Collection throat, - RiverCaveSource source - ) { - for (CavePosition position : throat) { - RiverCaveAction action; - if (position.y() <= source.waterHeadY()) { - action = RiverCaveAction.WET_SOURCE; - } else if (source.mode() == RiverCaveMode.WATERFALL_POOL - || source.mode() == RiverCaveMode.GENERATED_GROTTO) { - action = RiverCaveAction.FALLING_FLUID; - } else { - action = RiverCaveAction.DRY_AIR; - } - actions.put(position, action); - } - } - - private void addSealGuards( - CaveVoxelView view, - RiverCaveSource source, - Map actions - ) { - Set guards = new HashSet<>(); - for (CavePosition position : List.copyOf(actions.keySet())) { - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (actions.containsKey(neighbor) - || isInletOpening(source, neighbor) - || !view.isInWorld(neighbor)) { - continue; - } - if (voxelAt(view, neighbor) == CaveVoxel.SOLID) { - guards.add(neighbor); - } - } - } - for (CavePosition guard : guards) { - actions.put(guard, RiverCaveAction.SEAL_GUARD); - } - } - - private boolean isInletOpening(RiverCaveSource source, CavePosition position) { - return position.equals(source.entry().offset(0, 1, 0)); - } - - private boolean isGeneratedInletCarve( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - CavePosition position, - CaveVoxel voxel - ) { - if (voxel != CaveVoxel.CAVE_AIR && voxel != CaveVoxel.COMPATIBLE_FLUID) { - return false; - } - int extent = Math.max(0, settings.throatRadius() - 1); - long deltaX = (long) position.x() - source.entry().x(); - long deltaZ = (long) position.z() - source.entry().z(); - return position.y() >= source.entry().y() - extent - && position.y() <= source.entry().y() - && deltaX * deltaX + deltaZ * deltaZ <= (long) extent * extent - && view.isOpenToSurface(position); - } - - private boolean isGeneratedInletOpening( - CaveVoxelView view, - RiverCaveSource source, - RiverCavePlannerSettings settings, - CavePosition position - ) { - int radius = Math.max(1, settings.throatRadius()); - long deltaX = (long) position.x() - source.entry().x(); - long deltaZ = (long) position.z() - source.entry().z(); - return position.y() >= source.entry().y() - && position.y() <= source.entry().y() + 1 - && deltaX * deltaX + deltaZ * deltaZ < (long) radius * radius - && view.isOpenToSurface(position); - } - - private RiverCaveRejection validateSource(RiverCaveSource source) { - if (source.entry().y() < source.waterHeadY()) { - return RiverCaveRejection.INVALID_SOURCE; - } - if (source.target().y() > source.waterHeadY()) { - return RiverCaveRejection.INVALID_SOURCE; - } - if (source.target().y() > source.entry().y()) { - return RiverCaveRejection.INVALID_SOURCE; - } - return RiverCaveRejection.NONE; - } - - private RiverCaveRejection validateBounds( - RiverCaveSource source, - RiverCavePlannerSettings settings, - CavePosition position - ) { - boolean closedComponent = source.mode() == RiverCaveMode.CLOSED_COMPONENT - || source.mode() == RiverCaveMode.WATERFALL_POOL; - int horizontalRadius = closedComponent - ? settings.maxClosedComponentHorizontalRadius() - : settings.maxHorizontalRadius(); - int maximumDepth = closedComponent - ? settings.maxClosedComponentDepth() - : settings.maxDepth(); - long deltaX = (long) position.x() - source.entry().x(); - long deltaZ = (long) position.z() - source.entry().z(); - long radiusSquared = (long) horizontalRadius * horizontalRadius; - if ((deltaX * deltaX) + (deltaZ * deltaZ) > radiusSquared) { - return RiverCaveRejection.RADIUS_LIMIT; - } - long depth = (long) source.entry().y() - position.y(); - int maximumGeneratedY = source.waterHeadY() + settings.dryHeadroom() + 1; - boolean allowedGeneratedHeadroom = !closedComponent - && position.y() <= maximumGeneratedY; - if ((depth < 0L && !allowedGeneratedHeadroom) || depth > maximumDepth) { - return RiverCaveRejection.DEPTH_LIMIT; - } - return RiverCaveRejection.NONE; - } - - private RiverCaveRejection rejectionForTarget( - CaveVoxel voxel, - RiverCavePlannerSettings settings - ) { - RiverCaveRejection hazard = rejectionForHazard(voxel, settings); - return hazard == RiverCaveRejection.NONE ? RiverCaveRejection.NO_CAVE_TARGET : hazard; - } - - private RiverCaveRejection rejectionForHazard( - CaveVoxel voxel, - RiverCavePlannerSettings settings - ) { - return switch (voxel) { - case LAVA -> RiverCaveRejection.LAVA_CONTACT; - case INCOMPATIBLE_FLUID -> settings.existingFluidPolicy() == RiverCaveFluidPolicy.REPLACE_CONTAINED - ? RiverCaveRejection.NONE - : RiverCaveRejection.INCOMPATIBLE_FLUID; - case COMPATIBLE_FLUID -> settings.existingFluidPolicy() == RiverCaveFluidPolicy.REJECT_EXISTING - ? RiverCaveRejection.EXISTING_FLUID - : RiverCaveRejection.NONE; - default -> RiverCaveRejection.NONE; - }; - } - - private boolean isFluidReachable(CaveVoxel voxel, RiverCavePlannerSettings settings) { - return voxel == CaveVoxel.CAVE_AIR - || (voxel == CaveVoxel.COMPATIBLE_FLUID - && settings.existingFluidPolicy() != RiverCaveFluidPolicy.REJECT_EXISTING) - || (voxel == CaveVoxel.INCOMPATIBLE_FLUID - && settings.existingFluidPolicy() == RiverCaveFluidPolicy.REPLACE_CONTAINED); - } - - private CaveVoxel voxelAt(CaveVoxelView view, CavePosition position) { - return Objects.requireNonNull(view.voxelAt(position)); - } - - private OptionalLong findWinningSourceId( - Set positions, - Map claimedBy - ) { - RiverCaveSource winner = null; - for (CavePosition position : positions) { - RiverCaveSource contender = claimedBy.get(position); - if (contender == null) { - continue; - } - if (winner == null || SOURCE_PRIORITY.compare(contender, winner) < 0) { - winner = contender; - } - } - return winner == null ? OptionalLong.empty() : OptionalLong.of(winner.sourceId()); - } - - private RiverCavePlan accepted( - CaveVoxelView view, - RiverCaveSource source, - Map actions - ) { - Map preconditions = new HashMap<>(actions.size()); - for (CavePosition position : actions.keySet()) { - preconditions.put( - position, - new CaveVoxelPrecondition(voxelAt(view, position), view.isOpenToSurface(position)) - ); - } - return new RiverCavePlan( - source, - RiverCaveRejection.NONE, - actions, - preconditions, - OptionalLong.empty() - ); - } - - private RiverCavePlan rejected(RiverCaveSource source, RiverCaveRejection rejection) { - return new RiverCavePlan( - source, - rejection, - Map.of(), - Map.of(), - OptionalLong.empty() - ); - } - - private RiverCavePlan rejectedOverlap(RiverCaveSource source, long winnerSourceId) { - return new RiverCavePlan( - source, - RiverCaveRejection.OVERLAPPING_SOURCE, - Map.of(), - Map.of(), - OptionalLong.of(winnerSourceId) - ); - } - - private record PathResult(List positions, RiverCaveRejection rejection) { - private static PathResult accepted(List positions) { - return new PathResult(List.copyOf(positions), RiverCaveRejection.NONE); - } - - private static PathResult rejected(RiverCaveRejection rejection) { - return new PathResult(List.of(), rejection); - } - } - - private record ComponentResult(Set positions, RiverCaveRejection rejection) { - private static ComponentResult accepted(Set positions) { - return new ComponentResult(Set.copyOf(positions), RiverCaveRejection.NONE); - } - - private static ComponentResult rejected(RiverCaveRejection rejection) { - return new ComponentResult(Set.of(), rejection); - } - } - - private record BoundaryResult(Set sealGuards, RiverCaveRejection rejection) { - private static BoundaryResult accepted(Set sealGuards) { - return new BoundaryResult(Set.copyOf(sealGuards), RiverCaveRejection.NONE); - } - - private static BoundaryResult rejected(RiverCaveRejection rejection) { - return new BoundaryResult(Set.of(), rejection); - } - } - - private record GrottoResult(Set positions, RiverCaveRejection rejection) { - private static GrottoResult accepted(Set positions) { - return new GrottoResult(Set.copyOf(positions), RiverCaveRejection.NONE); - } - - private static GrottoResult rejected(RiverCaveRejection rejection) { - return new GrottoResult(Set.of(), rejection); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveFluidKind.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveFluidKind.java deleted file mode 100644 index 34c93559b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveFluidKind.java +++ /dev/null @@ -1,6 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public enum RiverCaveFluidKind { - RIVER, - DEEP_POOL -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveFluidPolicy.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveFluidPolicy.java deleted file mode 100644 index c84e9c3b7..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveFluidPolicy.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public enum RiverCaveFluidPolicy { - REJECT_EXISTING, - ALLOW_COMPATIBLE, - REPLACE_CONTAINED -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveGrottoShape.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveGrottoShape.java deleted file mode 100644 index 5d57d209b..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveGrottoShape.java +++ /dev/null @@ -1,21 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -@FunctionalInterface -public interface RiverCaveGrottoShape { - RiverCaveGrottoShape ELLIPSOID = (source, settings, dx, dy, dz) -> { - double horizontalRadius = settings.grottoHorizontalRadius(); - double verticalRadius = settings.grottoVerticalRadius(); - double normalized = ((double) dx * dx / (horizontalRadius * horizontalRadius)) - + ((double) dy * dy / (verticalRadius * verticalRadius)) - + ((double) dz * dz / (horizontalRadius * horizontalRadius)); - return normalized <= 1D; - }; - - boolean contains( - RiverCaveSource source, - RiverCavePlannerSettings settings, - int offsetX, - int offsetY, - int offsetZ - ); -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveHydrology.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveHydrology.java deleted file mode 100644 index 5c1be7dc9..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveHydrology.java +++ /dev/null @@ -1,99 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import art.arcane.volmlib.util.matter.MatterCavern; - -import java.util.Objects; -import java.util.Optional; - -public final class RiverCaveHydrology { - private static final byte LIQUID_FLUID = 1; - private static final byte LIQUID_FORCED_AIR = 3; - - private final RiverCaveAction action; - private final String floodedBiomeKey; - private final RiverCaveFluidKind fluidKind; - private final MatterCavern cavern; - - public RiverCaveHydrology( - RiverCaveAction action, - String floodedBiomeKey, - RiverCaveFluidKind fluidKind - ) { - this.action = Objects.requireNonNull(action); - this.floodedBiomeKey = floodedBiomeKey == null ? "" : floodedBiomeKey.trim(); - this.fluidKind = Objects.requireNonNull(fluidKind); - this.cavern = switch (action) { - case WET_SOURCE, FALLING_FLUID -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FLUID); - case DRY_AIR -> new MatterCavern(true, this.floodedBiomeKey, LIQUID_FORCED_AIR); - case SEAL_GUARD -> null; - }; - } - - public static RiverCaveHydrology of(RiverCaveAction action) { - return new RiverCaveHydrology(action, "", RiverCaveFluidKind.RIVER); - } - - public static RiverCaveHydrology of(RiverCaveAction action, RiverCaveFluidKind fluidKind) { - return new RiverCaveHydrology(action, "", fluidKind); - } - - public Optional floodedBiome() { - return floodedBiomeKey.isEmpty() ? Optional.empty() : Optional.of(floodedBiomeKey); - } - - public boolean carves() { - return action != RiverCaveAction.SEAL_GUARD; - } - - public boolean isWet() { - return action == RiverCaveAction.WET_SOURCE || action == RiverCaveAction.FALLING_FLUID; - } - - public boolean isFalling() { - return action == RiverCaveAction.FALLING_FLUID; - } - - public boolean protectsPlacement() { - return action == RiverCaveAction.SEAL_GUARD || isWet(); - } - - public MatterCavern asCavern() { - return cavern; - } - - public RiverCaveAction action() { - return action; - } - - public String floodedBiomeKey() { - return floodedBiomeKey; - } - - public RiverCaveFluidKind fluidKind() { - return fluidKind; - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (!(object instanceof RiverCaveHydrology hydrology)) { - return false; - } - return action == hydrology.action - && floodedBiomeKey.equals(hydrology.floodedBiomeKey) - && fluidKind == hydrology.fluidKind; - } - - @Override - public int hashCode() { - return (31 * ((31 * action.hashCode()) + floodedBiomeKey.hashCode())) + fluidKind.hashCode(); - } - - @Override - public String toString() { - return "RiverCaveHydrology[action=" + action + ", floodedBiomeKey=" + floodedBiomeKey - + ", fluidKind=" + fluidKind + "]"; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveHydrologyStorage.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveHydrologyStorage.java deleted file mode 100644 index 3adfaf956..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveHydrologyStorage.java +++ /dev/null @@ -1,41 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import art.arcane.volmlib.util.mantle.runtime.Mantle; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; -import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; -import art.arcane.volmlib.util.matter.Matter; - -public final class RiverCaveHydrologyStorage { - private RiverCaveHydrologyStorage() { - } - - public static RiverCaveHydrology getIfPresent(Mantle mantle, int x, int y, int z) { - if (y < 0 || y >= mantle.getWorldHeight()) { - return null; - } - int chunkX = x >> 4; - int chunkZ = z >> 4; - TectonicPlate plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5)); - if (plate == null || plate.isClosed()) { - return null; - } - MantleChunk chunk = plate.get(chunkX & 31, chunkZ & 31); - return getIfPresent(chunk, x, y, z); - } - - public static RiverCaveHydrology getIfPresent(MantleChunk chunk, int x, int y, int z) { - if (chunk == null || y < 0) { - return null; - } - int section = y >> 4; - if (!chunk.exists(section)) { - return null; - } - Matter matter = chunk.get(section); - if (matter == null || !matter.hasSlice(RiverCaveHydrology.class)) { - return null; - } - return matter.getSlice(RiverCaveHydrology.class) - .get(x & 15, y & 15, z & 15); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveMode.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveMode.java deleted file mode 100644 index a617b2baa..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveMode.java +++ /dev/null @@ -1,9 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public enum RiverCaveMode { - CLOSED_COMPONENT, - GENERATED_GROTTO, - GROTTO_OR_CLOSED_COMPONENT, - WATERFALL_POOL, - DEEP_POOL -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlan.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlan.java deleted file mode 100644 index 3c38fd0c1..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlan.java +++ /dev/null @@ -1,31 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import java.util.Map; -import java.util.Objects; -import java.util.OptionalLong; - -public record RiverCavePlan( - RiverCaveSource source, - RiverCaveRejection rejection, - Map actions, - Map baselinePreconditions, - OptionalLong arbitrationWinnerSourceId -) { - public RiverCavePlan { - Objects.requireNonNull(source); - Objects.requireNonNull(rejection); - actions = Map.copyOf(Objects.requireNonNull(actions)); - baselinePreconditions = Map.copyOf(Objects.requireNonNull(baselinePreconditions)); - Objects.requireNonNull(arbitrationWinnerSourceId); - if (rejection != RiverCaveRejection.NONE && (!actions.isEmpty() || !baselinePreconditions.isEmpty())) { - throw new IllegalArgumentException("Rejected cave plans cannot contain mutations or preconditions"); - } - if (rejection != RiverCaveRejection.OVERLAPPING_SOURCE && arbitrationWinnerSourceId.isPresent()) { - throw new IllegalArgumentException("Only overlap rejections can name an arbitration winner"); - } - } - - public boolean accepted() { - return rejection == RiverCaveRejection.NONE; - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlannerSettings.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlannerSettings.java deleted file mode 100644 index a7aab41e5..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlannerSettings.java +++ /dev/null @@ -1,104 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import java.util.Objects; - -public record RiverCavePlannerSettings( - int maxHorizontalRadius, - int maxDepth, - int maxFloodVolume, - int maxThroatLength, - int throatRadius, - int grottoHorizontalRadius, - int grottoVerticalRadius, - int dryHeadroom, - RiverCaveFluidPolicy existingFluidPolicy, - RiverCaveGrottoShape grottoShape, - int maxClosedComponentHorizontalRadius, - int maxClosedComponentDepth -) { - public RiverCavePlannerSettings( - int maxHorizontalRadius, - int maxDepth, - int maxFloodVolume, - int maxThroatLength, - int throatRadius, - int grottoHorizontalRadius, - int grottoVerticalRadius, - int dryHeadroom, - RiverCaveFluidPolicy existingFluidPolicy, - RiverCaveGrottoShape grottoShape - ) { - this( - maxHorizontalRadius, - maxDepth, - maxFloodVolume, - maxThroatLength, - throatRadius, - grottoHorizontalRadius, - grottoVerticalRadius, - dryHeadroom, - existingFluidPolicy, - grottoShape, - maxHorizontalRadius, - maxDepth - ); - } - - public RiverCavePlannerSettings( - int maxHorizontalRadius, - int maxDepth, - int maxFloodVolume, - int maxThroatLength, - int grottoHorizontalRadius, - int grottoVerticalRadius, - RiverCaveFluidPolicy existingFluidPolicy - ) { - this( - maxHorizontalRadius, - maxDepth, - maxFloodVolume, - maxThroatLength, - 1, - grottoHorizontalRadius, - grottoVerticalRadius, - 0, - existingFluidPolicy, - RiverCaveGrottoShape.ELLIPSOID - ); - } - - public RiverCavePlannerSettings { - Objects.requireNonNull(existingFluidPolicy); - Objects.requireNonNull(grottoShape); - if (maxHorizontalRadius < 1) { - throw new IllegalArgumentException("maxHorizontalRadius must be positive"); - } - if (maxDepth < 1) { - throw new IllegalArgumentException("maxDepth must be positive"); - } - if (maxFloodVolume < 1) { - throw new IllegalArgumentException("maxFloodVolume must be positive"); - } - if (maxThroatLength < 1) { - throw new IllegalArgumentException("maxThroatLength must be positive"); - } - if (throatRadius < 1) { - throw new IllegalArgumentException("throatRadius must be positive"); - } - if (grottoHorizontalRadius < 1) { - throw new IllegalArgumentException("grottoHorizontalRadius must be positive"); - } - if (grottoVerticalRadius < 1) { - throw new IllegalArgumentException("grottoVerticalRadius must be positive"); - } - if (dryHeadroom < 0) { - throw new IllegalArgumentException("dryHeadroom cannot be negative"); - } - if (maxClosedComponentHorizontalRadius < 1) { - throw new IllegalArgumentException("maxClosedComponentHorizontalRadius must be positive"); - } - if (maxClosedComponentDepth < 1) { - throw new IllegalArgumentException("maxClosedComponentDepth must be positive"); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlanningResult.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlanningResult.java deleted file mode 100644 index a1cd46868..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCavePlanningResult.java +++ /dev/null @@ -1,17 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public record RiverCavePlanningResult( - List plans, - Map actions, - Map baselinePreconditions -) { - public RiverCavePlanningResult { - plans = List.copyOf(Objects.requireNonNull(plans)); - actions = Map.copyOf(Objects.requireNonNull(actions)); - baselinePreconditions = Map.copyOf(Objects.requireNonNull(baselinePreconditions)); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveRejection.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveRejection.java deleted file mode 100644 index 5ae85fda6..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveRejection.java +++ /dev/null @@ -1,21 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -public enum RiverCaveRejection { - NONE, - INVALID_SOURCE, - NO_CAVE_TARGET, - THROAT_LIMIT, - RADIUS_LIMIT, - DEPTH_LIMIT, - VOLUME_LIMIT, - WORLD_BOUNDARY, - OPEN_SURFACE, - LAVA_CONTACT, - EXISTING_FLUID, - INCOMPATIBLE_FLUID, - GROTTO_INTERSECTION, - GROTTO_SHELL_OPEN, - DRY_HEADROOM_LIMIT, - WATERFALL_SHAFT_OPEN, - OVERLAPPING_SOURCE -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveSource.java b/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveSource.java deleted file mode 100644 index 592d96cf2..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/cave/RiverCaveSource.java +++ /dev/null @@ -1,17 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import java.util.Objects; - -public record RiverCaveSource( - long sourceId, - CavePosition entry, - CavePosition target, - int waterHeadY, - RiverCaveMode mode -) { - public RiverCaveSource { - Objects.requireNonNull(entry); - Objects.requireNonNull(target); - Objects.requireNonNull(mode); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/EffectiveRiverSettings.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/EffectiveRiverSettings.java deleted file mode 100644 index acf613ff1..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/EffectiveRiverSettings.java +++ /dev/null @@ -1,161 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverBiomes; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.object.IrisRiverOverride; -import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; -import art.arcane.iris.engine.object.IrisRiverTerminalMode; -import art.arcane.volmlib.util.collection.KList; - -import java.util.List; -import java.util.Objects; - -public record EffectiveRiverSettings( - boolean allowSources, - IrisRiverRoutingPolicy routingPolicy, - double routingCostMultiplier, - double widthMultiplier, - double bankWidthMultiplier, - double depthMultiplier, - double maxIncisionMultiplier, - double continuationChanceMultiplier, - double caveEntryMultiplier, - IrisRiverTerminalMode terminalMode, - boolean terminalModeOverridden, - List channelBiomes, - List bankBiomes, - List mouthBiomes, - List dryBiomes, - List floodedCaveBiomes -) { - public EffectiveRiverSettings { - Objects.requireNonNull(routingPolicy); - Objects.requireNonNull(terminalMode); - channelBiomes = List.copyOf(Objects.requireNonNull(channelBiomes)); - bankBiomes = List.copyOf(Objects.requireNonNull(bankBiomes)); - mouthBiomes = List.copyOf(Objects.requireNonNull(mouthBiomes)); - dryBiomes = List.copyOf(Objects.requireNonNull(dryBiomes)); - floodedCaveBiomes = List.copyOf(Objects.requireNonNull(floodedCaveBiomes)); - } - - public static EffectiveRiverSettings resolve( - IrisRiverNetwork network, - IrisRegion region, - IrisBiome naturalBiome - ) { - Objects.requireNonNull(network); - IrisRiverBiomes biomes = network.getBiomes() == null ? new IrisRiverBiomes() : network.getBiomes(); - Builder builder = new Builder( - network.getTerrain().getTerminalMode(), - copy(biomes.getChannel()), - copy(biomes.getBank()), - copy(biomes.getMouth()), - copy(biomes.getDry()), - copy(biomes.getFloodedCave()) - ); - if (region != null) { - builder.apply(region.getRiverOverride()); - } - if (naturalBiome != null) { - builder.apply(naturalBiome.getRiverOverride()); - } - return builder.build(); - } - - private static List copy(KList values) { - return values == null ? List.of() : List.copyOf(values); - } - - private static final class Builder { - private boolean allowSources = true; - private IrisRiverRoutingPolicy routingPolicy = IrisRiverRoutingPolicy.ALLOW; - private double routingCostMultiplier = 1D; - private double widthMultiplier = 1D; - private double bankWidthMultiplier = 1D; - private double depthMultiplier = 1D; - private double maxIncisionMultiplier = 1D; - private double continuationChanceMultiplier = 1D; - private double caveEntryMultiplier = 1D; - private IrisRiverTerminalMode terminalMode; - private boolean terminalModeOverridden; - private List channelBiomes; - private List bankBiomes; - private List mouthBiomes; - private List dryBiomes; - private List floodedCaveBiomes; - - private Builder( - IrisRiverTerminalMode terminalMode, - List channelBiomes, - List bankBiomes, - List mouthBiomes, - List dryBiomes, - List floodedCaveBiomes - ) { - this.terminalMode = terminalMode == null ? IrisRiverTerminalMode.DRY_CHANNEL : terminalMode; - this.channelBiomes = channelBiomes; - this.bankBiomes = bankBiomes; - this.mouthBiomes = mouthBiomes; - this.dryBiomes = dryBiomes; - this.floodedCaveBiomes = floodedCaveBiomes; - } - - private void apply(IrisRiverOverride override) { - if (override == null) { - return; - } - allowSources = override.getAllowSources() == null ? allowSources : override.getAllowSources(); - routingPolicy = override.getRoutingPolicy() == null ? routingPolicy : override.getRoutingPolicy(); - routingCostMultiplier = value(override.getRoutingCostMultiplier(), routingCostMultiplier); - widthMultiplier = value(override.getWidthMultiplier(), widthMultiplier); - bankWidthMultiplier = value(override.getBankWidthMultiplier(), bankWidthMultiplier); - depthMultiplier = value(override.getDepthMultiplier(), depthMultiplier); - maxIncisionMultiplier = value(override.getMaxIncisionMultiplier(), maxIncisionMultiplier); - continuationChanceMultiplier = value( - override.getContinuationChanceMultiplier(), - continuationChanceMultiplier - ); - caveEntryMultiplier = value(override.getCaveEntryMultiplier(), caveEntryMultiplier); - if (override.getTerminalMode() != null) { - terminalMode = override.getTerminalMode(); - terminalModeOverridden = true; - } - channelBiomes = replace(override.getChannelBiomes(), channelBiomes); - bankBiomes = replace(override.getBankBiomes(), bankBiomes); - mouthBiomes = replace(override.getMouthBiomes(), mouthBiomes); - dryBiomes = replace(override.getDryBiomes(), dryBiomes); - floodedCaveBiomes = replace(override.getFloodedCaveBiomes(), floodedCaveBiomes); - } - - private EffectiveRiverSettings build() { - return new EffectiveRiverSettings( - allowSources, - routingPolicy, - routingCostMultiplier, - widthMultiplier, - bankWidthMultiplier, - depthMultiplier, - maxIncisionMultiplier, - continuationChanceMultiplier, - caveEntryMultiplier, - terminalMode, - terminalModeOverridden, - channelBiomes, - bankBiomes, - mouthBiomes, - dryBiomes, - floodedCaveBiomes - ); - } - - private static double value(Double configured, double inherited) { - return configured == null || !Double.isFinite(configured) ? inherited : Math.max(0D, configured); - } - - private static List replace(KList configured, List inherited) { - return configured == null ? inherited : List.copyOf(configured); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntime.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntime.java deleted file mode 100644 index 5c5a21cf5..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntime.java +++ /dev/null @@ -1,1586 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.IRare; -import art.arcane.iris.engine.object.InferredType; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisGeneratorStyle; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.object.IrisRiverCaveMode; -import art.arcane.iris.engine.object.IrisRiverCaves; -import art.arcane.iris.engine.object.IrisRiverDeepPools; -import art.arcane.iris.engine.object.IrisRiverNoiseChance; -import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; -import art.arcane.iris.engine.object.IrisRiverTerminalMode; -import art.arcane.iris.engine.object.IrisRiverTerrain; -import art.arcane.iris.engine.object.IrisRiverTopology; -import art.arcane.iris.engine.object.IrisRiverWater; -import art.arcane.iris.engine.object.IrisRiverWaterMode; -import art.arcane.iris.engine.object.IrisRiverWorm; -import art.arcane.iris.engine.object.IrisStyledRange; -import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.iris.engine.river.RiverAnchor; -import art.arcane.iris.engine.river.RiverEdgeId; -import art.arcane.iris.engine.river.RiverNetworkOptions; -import art.arcane.iris.engine.river.RiverPolyline; -import art.arcane.iris.engine.river.RiverReach; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverRoutingContext; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.RiverTerrainNodeSample; -import art.arcane.iris.engine.river.RiverTerrainSourceSample; -import art.arcane.iris.engine.river.RiverTerrainSampler; -import art.arcane.iris.engine.river.RiverTerminalPolicy; -import art.arcane.iris.engine.river.RiverTile; -import art.arcane.iris.engine.river.RiverTileCache; -import art.arcane.iris.engine.river.RiverTopologyComplexity; -import art.arcane.iris.engine.river.RiverWorm; -import art.arcane.iris.util.project.interpolation.NoiseBounds; -import art.arcane.iris.util.project.noise.CNG; -import art.arcane.iris.util.project.stream.ProceduralStream; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.math.RNG; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReferenceArray; - -public final class IrisRiverRuntime implements AutoCloseable { - static final int MAXIMUM_REACH_FEASIBILITY_SAMPLES = 65; - static final int FEASIBILITY_REJECT = 0; - static final int FEASIBILITY_ACCEPT = 1; - static final int FEASIBILITY_SAMPLE_BED = 2; - private static final long SOURCE_NOISE_SALT = 0x243F6A8885A308D3L; - private static final long CONTINUATION_NOISE_SALT = 0x13198A2E03707344L; - private static final long INCISION_NOISE_SALT = 0xA4093822299F31D0L; - private static final long INCISION_GATE_SALT = 0x082EFA98EC4E6C89L; - private static final long ROUTING_NOISE_SALT = 0x452821E638D01377L; - private static final long WIDTH_NOISE_SALT = 0xBE5466CF34E90C6CL; - private static final long BANK_NOISE_SALT = 0xC0AC29B7C97C50DDL; - private static final long DEPTH_NOISE_SALT = 0x3F84D5B5B5470917L; - private static final long BED_NOISE_SALT = 0xD1310BA698DFB5ACL; - private static final long BIOME_NOISE_SALT = 0x2FFD72DBD01ADFB7L; - private static final long CAVE_ENTRY_NOISE_SALT = 0xB8E1AFED6A267E96L; - private static final long CAVE_ENTRY_GATE_SALT = 0xBA7C9045F12C7F99L; - private static final long DEEP_POOL_REACH_NOISE_SALT = 0x7137449123EF65CDL; - private static final long DEEP_POOL_REACH_GATE_SALT = 0xE9B5DBA58189DBBCL; - private static final long TUNNEL_FLOOR_NOISE_SALT = 0x8CB92BA72F3D8DD7L; - private static final long TUNNEL_ROOF_NOISE_SALT = 0xDB4F0B9175AE2165L; - private static final long TUNNEL_WIDTH_NOISE_SALT = 0xC6EF372FE94F82BEL; - private static final long FLOODED_CAVE_BIOME_SALT = 0x24A19947B3916CF7L; - private static final long TERMINAL_CAVE_ANCHOR_SALT = 0x9E3779B97F4A7C15L; - private static final int TILE_CACHE_SIZE = 32; - private static final int TUNNEL_SAMPLE_CHUNK_CACHE_SIZE = 4_096; - private static final Object ABSENT_TUNNEL_SAMPLE = new Object(); - - private final long seed; - private final IrisRiverNetwork configuration; - private final IrisData data; - private final int riverFluidHeight; - private final int dimensionFluidHeight; - private final boolean boreMantleActive; - private final boolean caveHydrologyActive; - private final boolean blockingRoutingPossible; - private final boolean variableMaxIncisionPossible; - private final boolean biomeRiverOverridesPossible; - private final RiverHeightBoundsSampler naturalHeightBounds; - private final ProceduralStream naturalHeight; - private final ProceduralStream naturalSlope; - private final ProceduralStream naturalOcean; - private final ProceduralStream naturalBiome; - private final ProceduralStream region; - private final IrisRiverTerrain terrain; - private final IrisRiverWater water; - private final IrisRiverCaves caves; - private final CNG sourceNoise; - private final CNG continuationNoise; - private final CNG incisionNoise; - private final CNG routingNoise; - private final CNG widthNoise; - private final CNG bankNoise; - private final CNG depthNoise; - private final CNG bedNoise; - private final CNG biomeNoise; - private final CNG caveEntryNoise; - private final CNG deepPoolReachNoise; - private final CNG tunnelFloorNoise; - private final CNG tunnelRoofNoise; - private final CNG tunnelWidthNoise; - private final art.arcane.iris.engine.river.RiverNetwork network; - private final RuntimeTerrainSampler terrainSampler; - private final RiverTileCache tileCache; - private final Cache tunnelSampleCache; - private final ConcurrentHashMap settingsCache; - private final ConcurrentHashMap> biomePoolCache; - - public IrisRiverRuntime(IrisRiverRuntimeContext context) { - Objects.requireNonNull(context); - seed = context.seed(); - configuration = context.configuration(); - data = context.data(); - riverFluidHeight = context.riverFluidHeight(); - dimensionFluidHeight = context.dimensionFluidHeight(); - boreMantleActive = context.boreMantleActive(); - caveHydrologyActive = context.caveHydrologyActive(); - blockingRoutingPossible = context.blockingRoutingPossible(); - variableMaxIncisionPossible = context.variableMaxIncisionPossible(); - biomeRiverOverridesPossible = context.biomeRiverOverridesPossible(); - naturalHeightBounds = context.naturalHeightBounds(); - naturalHeight = context.naturalHeight(); - naturalSlope = context.naturalSlope(); - naturalOcean = context.naturalOcean(); - naturalBiome = context.naturalBiome(); - region = context.region(); - terrain = Objects.requireNonNull(configuration.getTerrain()); - RiverTopologyComplexity.requireSafeTunnelPlan( - terrain.getMaxChannelWidth(), - maximumTunnelWidthMultiplier(terrain), - terrain.getTunnelMouthBlend() - ); - water = Objects.requireNonNull(configuration.getWater()); - caves = configuration.getCaves() == null ? new IrisRiverCaves() : configuration.getCaves(); - IrisRiverTopology topology = Objects.requireNonNull(configuration.getTopology()); - sourceNoise = noise(topology.getSource(), SOURCE_NOISE_SALT); - continuationNoise = noise(topology.getContinuation(), CONTINUATION_NOISE_SALT); - incisionNoise = noise(terrain.getIncision(), INCISION_NOISE_SALT); - routingNoise = noise(topology.getRoutingStyle(), ROUTING_NOISE_SALT); - widthNoise = noise(terrain.getChannelWidth(), WIDTH_NOISE_SALT); - bankNoise = noise(terrain.getBankWidth(), BANK_NOISE_SALT); - depthNoise = noise(terrain.getDepth(), DEPTH_NOISE_SALT); - bedNoise = noise(terrain.getBedRoughnessStyle(), BED_NOISE_SALT); - biomeNoise = noise(configuration.getBiomes().getSelectionStyle(), BIOME_NOISE_SALT); - caveEntryNoise = noise(caves.getEntry(), CAVE_ENTRY_NOISE_SALT); - IrisRiverDeepPools deepPools = caves.getDeepPools(); - deepPoolReachNoise = noise( - deepPools == null ? null : deepPools.getReach(), - DEEP_POOL_REACH_NOISE_SALT - ); - tunnelFloorNoise = noise(terrain.getTunnelFloorStyle(), TUNNEL_FLOOR_NOISE_SALT); - tunnelRoofNoise = noise(terrain.getTunnelRoofStyle(), TUNNEL_ROOF_NOISE_SALT); - tunnelWidthNoise = noise(terrain.getTunnelWidthMultiplier(), TUNNEL_WIDTH_NOISE_SALT); - settingsCache = new ConcurrentHashMap<>(); - biomePoolCache = new ConcurrentHashMap<>(); - RiverNetworkOptions options = options(topology, terrain); - network = new art.arcane.iris.engine.river.RiverNetwork(options); - terrainSampler = new RuntimeTerrainSampler(topology); - tileCache = new RiverTileCache( - TILE_CACHE_SIZE, - (tileX, tileZ) -> network.buildTile(tileX, tileZ, terrainSampler) - ); - tunnelSampleCache = Caffeine.newBuilder() - .maximumSize(TUNNEL_SAMPLE_CHUNK_CACHE_SIZE) - .build(); - } - - public IrisRiverSurfaceSample sample(double x, double z) { - ResolvedRiverColumn column = resolveColumn(x, z); - if (column == null) { - return IrisRiverSurfaceSample.none(naturalHeight.get(x, z), dimensionFluidHeight); - } - if (column.subterranean()) { - return new IrisRiverSurfaceSample( - column.river(), - column.naturalHeight(), - column.naturalHeight(), - column.waterSurfaceY(), - true, - false - ); - } - double carveWeight = StrictMath.pow( - clamp01(column.river().carveWeight()), - Math.max(0.125D, terrain.getBankExponent()) - ); - if (column.river().terminal() && shouldTaperTerminal(column.reach())) { - carveWeight *= terminalWeight( - terrain.getTerminalTaper(), - column.reach().polyline().length(), - column.river().alongReach() - ); - } - double terrainHeight = incisedHeight( - column.naturalHeight(), - column.bedHeight(), - carveWeight, - column.maximumIncision() - ); - boolean wet = column.river().state() == RiverRouteState.WET; - boolean surfaceFluid = wet && Math.round(terrainHeight) < Math.round(column.waterSurfaceY()); - return new IrisRiverSurfaceSample( - column.river(), - column.naturalHeight(), - terrainHeight, - wet ? column.waterSurfaceY() : terrainHeight, - false, - surfaceFluid - ); - } - - public IrisRiverTunnelSample sampleTunnel(double x, double z) { - double mouthBlend = terrain.getTunnelMouthBlend(); - double maximumMultiplier = maximumTunnelWidthMultiplier(terrain); - double maximumExtraRadius = terrain.getMaxChannelWidth() * 0.5D * (maximumMultiplier - 1D) - + mouthBlend; - RiverTile tile = tileAt(x, z); - RiverSample river = tile.sampleExpanded(x, z, maximumExtraRadius); - if (!river.present() || river.state() != RiverRouteState.WET) { - return null; - } - double widthMultiplier = styled( - terrain.getTunnelWidthMultiplier(), - tunnelWidthNoise, - (int) StrictMath.round(x), - (int) StrictMath.round(z), - 1D - ); - double channelRadius = river.width() * 0.5D * widthMultiplier; - if (river.distance() > channelRadius + mouthBlend) { - return null; - } - ResolvedRiverColumn column = resolveColumn(x, z, tile, river); - if (!column.subterranean()) { - return null; - } - double mouthFactor = tunnelMouthFactor(column, mouthBlend); - channelRadius += mouthBlend * mouthFactor; - if (column.river().distance() > channelRadius) { - return null; - } - double normalizedDistance = channelRadius <= 0D - ? 1D - : clamp01(column.river().distance() / channelRadius); - double profile = StrictMath.sqrt(Math.max(0D, 1D - normalizedDistance * normalizedDistance)); - int waterHeadY = (int) Math.round(column.waterSurfaceY()); - double floorOffset = tunnelFloorNoise.fitDouble( - -terrain.getTunnelFloorVariation(), - terrain.getTunnelFloorVariation(), - x, - z - ); - int bedY = shapedTunnelBedY(waterHeadY, column.bedHeight(), profile, floorOffset); - double roofOffset = tunnelRoofNoise.fitDouble( - -terrain.getTunnelRoofVariation(), - terrain.getTunnelRoofVariation(), - x, - z - ); - int ceilingY = shapedTunnelCeilingY( - waterHeadY, - caves.getDryHeadroom() * column.reach().roofScaleAt(column.river().alongReach()) - + mouthBlend * mouthFactor, - profile, - roofOffset - ); - return new IrisRiverTunnelSample(column.river(), bedY, waterHeadY, ceilingY); - } - - public IrisRiverTunnelSample sampleTunnel(int x, int z) { - long chunkKey = ((long) (x >> 4) << 32) ^ ((z >> 4) & 0xFFFFFFFFL); - TunnelSampleChunk chunk = tunnelSampleCache.get(chunkKey, ignored -> new TunnelSampleChunk()); - return chunk.sample(this, x, z); - } - - static int shapedTunnelBedY( - int waterHeadY, - double baseBedY, - double profile, - double floorOffset - ) { - double baseDepth = Math.max(1D, waterHeadY - baseBedY); - double shapedDepth = Math.max(1D, (baseDepth + floorOffset) * clamp01(profile)); - return waterHeadY - Math.max(1, (int) StrictMath.ceil(shapedDepth)); - } - - static int shapedTunnelCeilingY( - int waterHeadY, - double dryHeadroom, - double profile, - double roofOffset - ) { - double shapedHeadroom = Math.max( - 0D, - (Math.max(0D, dryHeadroom) + roofOffset) * clamp01(profile) - ); - return waterHeadY + (int) StrictMath.ceil(shapedHeadroom); - } - - private ResolvedRiverColumn resolveColumn(double x, double z) { - return resolveColumn(x, z, 0D); - } - - private ResolvedRiverColumn resolveColumn(double x, double z, double additionalRadius) { - RiverTile tile = tileAt(x, z); - RiverSample river = tile.sampleExpanded(x, z, additionalRadius); - if (!river.present()) { - return null; - } - return resolveColumn(x, z, tile, river); - } - - private ResolvedRiverColumn resolveColumn(double x, double z, RiverTile tile, RiverSample river) { - double sampledNaturalHeight = naturalHeight.get(x, z); - RiverReach reach = tile.reach(river.reachId()); - IrisRegion sampledRegion = region.get(x, z); - IrisBiome sampledBiome = naturalBiome.get(x, z); - EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome); - double waterSurfaceY = river.state() == RiverRouteState.WET - ? waterSurface(reach, river.alongReach(), isNaturalOcean(sampledBiome)) - : sampledNaturalHeight; - double bedHeight = river.state() == RiverRouteState.WET - ? waterSurfaceY - river.depth() + bedRoughness(x, z) - : sampledNaturalHeight - river.depth() + bedRoughness(x, z); - double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier()); - double cappedSurface = incisedHeight( - sampledNaturalHeight, - bedHeight, - 1D, - maximumIncision - ); - return new ResolvedRiverColumn( - river, - reach, - sampledNaturalHeight, - waterSurfaceY, - bedHeight, - maximumIncision, - boreMantleActive - && river.state() == RiverRouteState.WET - && Math.round(cappedSurface) >= Math.round(waterSurfaceY) - ); - } - - private double tunnelMouthFactor(ResolvedRiverColumn column, double mouthBlend) { - if (mouthBlend <= 0D) { - return 0D; - } - double length = column.reach().polyline().length(); - if (length <= 0D) { - return 0D; - } - double offset = mouthBlend / length; - double alongReach = column.river().alongReach(); - return Math.max( - tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach - offset), mouthBlend), - tunnelMouthFactor(column.reach(), alongReach, clamp01(alongReach + offset), mouthBlend) - ); - } - - private double tunnelMouthFactor( - RiverReach reach, - double subterraneanAlong, - double candidateOpenAlong, - double mouthBlend - ) { - if (candidateOpenAlong == subterraneanAlong - || isCenterlineSubterranean(reach, candidateOpenAlong)) { - return 0D; - } - double openAlong = candidateOpenAlong; - double solidAlong = subterraneanAlong; - for (int iteration = 0; iteration < 5; iteration++) { - double midpoint = (openAlong + solidAlong) * 0.5D; - if (isCenterlineSubterranean(reach, midpoint)) { - solidAlong = midpoint; - } else { - openAlong = midpoint; - } - } - double distance = StrictMath.abs(solidAlong - subterraneanAlong) * reach.polyline().length(); - return clamp01(1D - distance / mouthBlend); - } - - private boolean isCenterlineSubterranean(RiverReach reach, double alongReach) { - CenterlinePosition center = centerlinePosition(reach, alongReach); - IrisRegion sampledRegion = region.get(center.x(), center.z()); - IrisBiome sampledBiome = naturalBiome.get(center.x(), center.z()); - double head = waterSurface(reach, alongReach, isNaturalOcean(sampledBiome)); - double centerNaturalHeight = naturalHeight.get(center.x(), center.z()); - double centerBedHeight = head - reach.depthAt(alongReach) + bedRoughness(center.x(), center.z()); - EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome); - double maximumIncision = Math.max(0D, terrain.getMaxIncision() * settings.maxIncisionMultiplier()); - double cappedSurface = incisedHeight(centerNaturalHeight, centerBedHeight, 1D, maximumIncision); - return Math.round(cappedSurface) >= Math.round(head); - } - - private static CenterlinePosition centerlinePosition(RiverReach reach, double alongReach) { - double targetDistance = clamp01(alongReach) * reach.polyline().length(); - for (int point = 0; point < reach.polyline().size() - 1; point++) { - double segmentStart = reach.polyline().cumulativeLength(point); - double segmentEnd = reach.polyline().cumulativeLength(point + 1); - if (targetDistance > segmentEnd && point < reach.polyline().size() - 2) { - continue; - } - double segmentLength = segmentEnd - segmentStart; - double t = segmentLength <= 0D ? 0D : (targetDistance - segmentStart) / segmentLength; - return new CenterlinePosition( - reach.polyline().x(point) - + (reach.polyline().x(point + 1) - reach.polyline().x(point)) * t, - reach.polyline().z(point) - + (reach.polyline().z(point + 1) - reach.polyline().z(point)) * t - ); - } - int last = reach.polyline().size() - 1; - return new CenterlinePosition(reach.polyline().x(last), reach.polyline().z(last)); - } - - public RiverTile tileAt(double x, double z) { - int blockX = clampToInt(StrictMath.floor(x)); - int blockZ = clampToInt(StrictMath.floor(z)); - return tileCache.get(network.tileXForBlock(blockX), network.tileZForBlock(blockZ)); - } - - public RiverSample sampleFootprint( - double minimumX, - double minimumZ, - double maximumX, - double maximumZ - ) { - if (!Double.isFinite(minimumX) || !Double.isFinite(minimumZ) - || !Double.isFinite(maximumX) || !Double.isFinite(maximumZ) - || minimumX > maximumX || minimumZ > maximumZ) { - throw new IllegalArgumentException("River footprint bounds must be finite and ordered"); - } - double centerX = minimumX * 0.5D + maximumX * 0.5D; - double centerZ = minimumZ * 0.5D + maximumZ * 0.5D; - return tileAt(centerX, centerZ).sampleFootprint(minimumX, minimumZ, maximumX, maximumZ); - } - - public boolean hasRiverFootprint( - int minimumX, - int minimumZ, - int maximumX, - int maximumZ - ) { - if (minimumX >= maximumX || minimumZ >= maximumZ) { - return false; - } - int minimumTileX = network.tileXForBlock(minimumX); - int minimumTileZ = network.tileZForBlock(minimumZ); - int maximumTileX = network.tileXForBlock(maximumX - 1); - int maximumTileZ = network.tileZForBlock(maximumZ - 1); - for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) { - for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) { - RiverSample sample = tileCache.get(tileX, tileZ).sampleFootprint( - minimumX, - minimumZ, - maximumX, - maximumZ - ); - if (sample.present()) { - return true; - } - } - } - return false; - } - - public List candidateAnchors( - int minimumX, - int minimumZ, - int maximumX, - int maximumZ, - double spacing, - long salt - ) { - if (minimumX >= maximumX || minimumZ >= maximumZ) { - return List.of(); - } - int minimumTileX = network.tileXForBlock(minimumX); - int minimumTileZ = network.tileZForBlock(minimumZ); - int maximumTileX = network.tileXForBlock(maximumX - 1); - int maximumTileZ = network.tileZForBlock(maximumZ - 1); - ArrayList anchors = new ArrayList<>(); - Set seen = new HashSet<>(); - for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) { - for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) { - RiverTile tile = tileCache.get(tileX, tileZ); - List candidates = tile.candidateAnchors( - minimumX, - minimumZ, - maximumX, - maximumZ, - spacing, - salt - ); - for (RiverAnchor anchor : candidates) { - if (seen.add(anchor.stableId())) { - anchors.add(anchor); - } - } - addTerminalCaveAnchors( - tile, - minimumX, - minimumZ, - maximumX, - maximumZ, - spacing, - salt, - anchors, - seen - ); - } - } - return List.copyOf(anchors); - } - - public EffectiveRiverSettings settingsAt(double x, double z) { - IrisRegion sampledRegion = region.get(x, z); - IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(x, z) : null; - return settingsFor(sampledRegion, sampledBiome); - } - - private EffectiveRiverSettings settingsFor(IrisRegion sampledRegion, IrisBiome sampledBiome) { - IdentitySettingsKey key = new IdentitySettingsKey(sampledRegion, sampledBiome); - return settingsCache.computeIfAbsent( - key, - ignored -> EffectiveRiverSettings.resolve(configuration, sampledRegion, sampledBiome) - ); - } - - public IrisRiverCaves caveSettings() { - return caves; - } - - public double maximumChannelWidth() { - return terrain.getMaxChannelWidth(); - } - - public double maximumTunnelWidthMultiplier() { - return maximumTunnelWidthMultiplier(terrain); - } - - public double tunnelMouthBlend() { - return terrain.getTunnelMouthBlend(); - } - - public int maximumTunnelHeadroom() { - return caves.getDryHeadroom() - + (int) StrictMath.ceil(terrain.getTunnelRoofVariation()) - + (int) StrictMath.ceil(terrain.getTunnelMouthBlend()); - } - - public boolean acceptsCaveAnchor(RiverAnchor anchor) { - Objects.requireNonNull(anchor); - IrisRiverCaves caves = caveSettings(); - if (anchor.state() != RiverRouteState.WET - || !caveHydrologyActive - || caves.getMode() == IrisRiverCaveMode.SEALED - || caves.getMaximumPerReach() <= 0) { - return false; - } - RiverReach reach = tileAt(anchor.x(), anchor.z()).reach(anchor.reachId()); - if (reach == null || reach.state() != RiverRouteState.WET) { - return false; - } - TerminalCaveAnchor terminal = terminalCaveAnchor(reach); - if (terminal != null) { - return anchor.stableId() == terminal.stableId() - && caves.getMode() != IrisRiverCaveMode.SEALED; - } - double firstDistance = unit(art.arcane.iris.engine.river.RiverNetwork.mix( - reach.id().stableId() ^ anchor.samplingSalt() - )) * anchor.samplingSpacing(); - double anchorDistance = firstDistance + anchor.index() * anchor.samplingSpacing(); - if (anchorDistance >= reach.polyline().length()) { - return false; - } - int accepted = 0; - for (int index = 0; index <= anchor.index(); index++) { - double distance = firstDistance + index * anchor.samplingSpacing(); - TerminalPosition position = positionAt(reach.polyline(), distance); - long stableId = art.arcane.iris.engine.river.RiverNetwork.mix( - reach.id().stableId() - ^ anchor.samplingSalt() - ^ (long) index * 0x9E3779B97F4A7C15L - ); - if (!caveEntryEligible(caves, stableId, position.x(), position.z())) { - continue; - } - if (!isCaveAnchorSourceable(position.x(), position.z())) { - continue; - } - if (index == anchor.index()) { - return stableId == anchor.stableId() && accepted < caves.getMaximumPerReach(); - } - accepted++; - if (accepted >= caves.getMaximumPerReach()) { - return false; - } - } - return false; - } - - public boolean acceptsDeepPoolAnchor(RiverAnchor anchor) { - Objects.requireNonNull(anchor); - IrisRiverDeepPools deepPools = caves.getDeepPools(); - if (deepPools == null - || !deepPools.isEnabled() - || deepPools.getMaximumPerReach() <= 0 - || anchor.state() != RiverRouteState.WET - || !caveHydrologyActive) { - return false; - } - RiverReach reach = tileAt(anchor.x(), anchor.z()).reach(anchor.reachId()); - if (reach == null || reach.state() != RiverRouteState.WET) { - return false; - } - if (!deepPoolReachEligible(deepPools, reach)) { - return false; - } - TerminalCaveAnchor terminal = terminalCaveAnchor(reach); - if (terminal != null && anchor.stableId() == terminal.stableId()) { - return false; - } - double firstDistance = unit(art.arcane.iris.engine.river.RiverNetwork.mix( - reach.id().stableId() ^ anchor.samplingSalt() - )) * anchor.samplingSpacing(); - double anchorDistance = firstDistance + anchor.index() * anchor.samplingSpacing(); - if (anchorDistance >= reach.polyline().length()) { - return false; - } - int accepted = 0; - for (int index = 0; index <= anchor.index(); index++) { - long stableId = art.arcane.iris.engine.river.RiverNetwork.mix( - reach.id().stableId() - ^ anchor.samplingSalt() - ^ (long) index * 0x9E3779B97F4A7C15L - ); - if (index == anchor.index()) { - return stableId == anchor.stableId() && accepted < deepPools.getMaximumPerReach(); - } - accepted++; - if (accepted >= deepPools.getMaximumPerReach()) { - return false; - } - } - return false; - } - - private boolean isCaveAnchorSourceable(double x, double z) { - IrisRiverSurfaceSample surface = sample(x, z); - if (surface.river().present() - && surface.river().state() == RiverRouteState.WET - && surface.river().section() == RiverSection.CHANNEL - && surface.surfaceFluid()) { - return true; - } - return sampleTunnel(x, z) != null; - } - - public boolean isTerminalCaveAnchor(RiverAnchor anchor) { - TerminalCaveAnchor terminal = terminalCaveAnchor(anchor); - return terminal != null && anchor.stableId() == terminal.stableId(); - } - - public String selectFloodedCaveBiome(RiverAnchor anchor) { - Objects.requireNonNull(anchor); - List biomes = settingsAt(anchor.x(), anchor.z()).floodedCaveBiomes(); - if (biomes.isEmpty()) { - return ""; - } - long hash = art.arcane.iris.engine.river.RiverNetwork.mix( - seed ^ anchor.stableId() ^ FLOODED_CAVE_BIOME_SALT); - int index = Math.floorMod(hash, biomes.size()); - String selected = biomes.get(index); - return selected == null ? "" : selected.trim(); - } - - public IrisBiome selectSurfaceBiome(IrisRiverSurfaceSample sample, double x, double z) { - RiverSample river = sample.river(); - if (!river.present() || sample.subterranean()) { - return null; - } - EffectiveRiverSettings settings = settingsAt(x, z); - PoolSelection selection = poolFor(river.section(), settings); - if (selection.keys().isEmpty()) { - return null; - } - BiomePoolKey key = new BiomePoolKey(selection.keys(), selection.type()); - List candidates = biomePoolCache.computeIfAbsent(key, this::loadBiomePool); - if (candidates.isEmpty()) { - return null; - } - double selector = clamp01(biomeNoise.fitDouble(0D, 1D, x, z)); - IrisBiome selected = IRare.pick(candidates, selector); - return selected == null ? null : selected.withInferredType(selection.type()); - } - - public int completedTileCount() { - return tileCache.completedSize(); - } - - boolean allowsReach(RiverRoutingContext context) { - return terrainSampler.allowsReach(Objects.requireNonNull(context)); - } - - RiverTerrainNodeSample sampleNode(int blockX, int blockZ) { - return terrainSampler.sampleNode(blockX, blockZ); - } - - RiverTerrainSourceSample sampleSource(int blockX, int blockZ) { - return terrainSampler.sampleSource(blockX, blockZ); - } - - @Override - public void close() { - tileCache.close(); - tunnelSampleCache.invalidateAll(); - settingsCache.clear(); - biomePoolCache.clear(); - } - - private RiverNetworkOptions options(IrisRiverTopology topology, IrisRiverTerrain riverTerrain) { - double dryChance = clamp01(riverTerrain.getDryContinuationChance()); - return RiverNetworkOptions.builder(seed) - .cellSize(topology.getCellSize()) - .tileCells(topology.getTileCells()) - .siteJitter(topology.getSiteJitter()) - .maxRouteReaches(topology.getMaxRouteReaches()) - .minimumSourcesPerTile(topology.getMinimumSourcesPerTile()) - .downstreamCandidateLimit(Math.max(1, Math.min(8, topology.getSinkSearchReaches() + 1))) - .routingBasinCells(topology.getRoutingBasinCells()) - .routingDeviationScaleCells(topology.getRoutingDeviationScaleCells()) - .routingDeviationStrengthCells(topology.getRoutingDeviationStrengthCells()) - .routingPlateauHeight(topology.getRoutingPlateauHeight()) - .hydraulicBaseHeight(riverFluidHeight) - .requireOcean(topology.isRequireOcean()) - .sourceChance(chance(topology.getSource())) - .reachChance(chance(topology.getContinuation())) - .dryChannelChance(dryChance) - .terrainHeightWeight(topology.getTerrainHeightWeight()) - .routingNoiseWeight(0D) - .flowAlignmentWeight(topology.getFlowAlignmentWeight()) - .confluenceWeight(topology.getConfluenceWeight()) - .oceanAttraction(topology.getOceanAttraction()) - .channelWidth(mid(riverTerrain.getChannelWidth(), 12D)) - .bankWidth(mid(riverTerrain.getBankWidth(), 8D)) - .depth(mid(riverTerrain.getDepth(), 4D)) - .channelRadiusBonus(riverTerrain.getChannelRadiusBonus()) - .maxChannelWidth(riverTerrain.getMaxChannelWidth()) - .maxBankWidth(riverTerrain.getMaxBankWidth()) - .maxDepth(riverTerrain.getMaxDepth()) - .orderWidthFactor(riverTerrain.getOrderWidthFactor()) - .orderDepthFactor(riverTerrain.getOrderDepthFactor()) - .maximumReachRadius(maximumReachRadius(topology, riverTerrain)) - .worms(worms(riverTerrain)) - .build(); - } - - private List worms(IrisRiverTerrain riverTerrain) { - if (riverTerrain.getWorms() == null || riverTerrain.getWorms().isEmpty()) { - throw new IllegalArgumentException("River terrain must configure at least one Perlin worm"); - } - ArrayList worms = new ArrayList<>(riverTerrain.getWorms().size()); - for (IrisRiverWorm configured : riverTerrain.getWorms()) { - if (configured == null) { - throw new IllegalArgumentException("River terrain worms must not contain null entries"); - } - worms.add(worm(configured)); - } - return List.copyOf(worms); - } - - private RiverWorm worm(IrisRiverWorm configured) { - if (configured.getChildren() == null) { - throw new IllegalArgumentException("River worm children must be an array"); - } - ArrayList children = new ArrayList<>(configured.getChildren().size()); - for (IrisRiverWorm child : configured.getChildren()) { - if (child == null) { - throw new IllegalArgumentException("River worm children must not contain null entries"); - } - children.add(worm(child)); - } - return new RiverWorm( - configured.getId(), - configured.getSeed(), - configured.getWeight(), - configured.getWavelength(), - configured.getDetailWavelength(), - configured.getTortuosity(), - configured.getDetailTortuosity(), - configured.getMaxOffset(), - configured.getSegments(), - configured.getWidthMultiplier(), - configured.getBankMultiplier(), - configured.getDepthMultiplier(), - configured.getBodyWavelength(), - configured.getBodyDetailWavelength(), - configured.getBodyDetailInfluence(), - configured.getWidthVariation(), - configured.getBankVariation(), - configured.getDepthVariation(), - configured.getRoofVariation(), - configured.getBranchCap(), - configured.getBranchDecay(), - configured.getConfluenceMultiplier(), - configured.getChildChance(), - configured.getBranchChildChance(), - List.copyOf(children) - ); - } - - private void addTerminalCaveAnchors( - RiverTile tile, - int minimumX, - int minimumZ, - int maximumX, - int maximumZ, - double spacing, - long salt, - List anchors, - Set seen - ) { - for (RiverReach reach : tile.reaches()) { - TerminalCaveAnchor terminal = terminalCaveAnchor(reach); - if (terminal == null - || terminal.x() < minimumX - || terminal.x() >= maximumX - || terminal.z() < minimumZ - || terminal.z() >= maximumZ - || !seen.add(terminal.stableId())) { - continue; - } - anchors.add(new RiverAnchor( - reach.id(), - 0, - terminal.stableId(), - spacing, - salt, - terminal.x(), - terminal.z(), - terminal.alongReach(), - reach.state(), - reach.flow(), - reach.order() - )); - } - } - - private TerminalCaveAnchor terminalCaveAnchor(RiverAnchor anchor) { - RiverReach reach = tileAt(anchor.x(), anchor.z()).reach(anchor.reachId()); - return reach == null ? null : terminalCaveAnchor(reach); - } - - private boolean caveEntryEligible( - IrisRiverCaves caves, - long stableId, - double x, - double z - ) { - EffectiveRiverSettings settings = settingsAt(x, z); - double chance = clamp01(effectiveChance( - caves.getEntry(), - caveEntryNoise, - (int) StrictMath.floor(x), - (int) StrictMath.floor(z) - ) * settings.caveEntryMultiplier()); - long hash = art.arcane.iris.engine.river.RiverNetwork.mix( - seed ^ stableId ^ CAVE_ENTRY_GATE_SALT - ); - return unit(hash) < chance; - } - - private boolean deepPoolReachEligible( - IrisRiverDeepPools deepPools, - RiverReach reach - ) { - CenterlinePosition center = centerlinePosition(reach, 0.5D); - EffectiveRiverSettings settings = settingsAt(center.x(), center.z()); - double chance = clamp01(effectiveChance( - deepPools.getReach(), - deepPoolReachNoise, - (int) StrictMath.floor(center.x()), - (int) StrictMath.floor(center.z()) - ) * settings.caveEntryMultiplier()); - long hash = art.arcane.iris.engine.river.RiverNetwork.mix( - seed ^ reach.id().stableId() ^ DEEP_POOL_REACH_GATE_SALT - ); - return unit(hash) < chance; - } - - private TerminalCaveAnchor terminalCaveAnchor(RiverReach reach) { - if (!caveHydrologyActive || !reach.terminal() || reach.state() != RiverRouteState.WET) { - return null; - } - RiverPolyline polyline = reach.polyline(); - double length = polyline.length(); - double taperDistance = Math.min(length, Math.max(0D, terrain.getTerminalTaper())); - double targetDistance = taperDistance == 0D ? length : length - (taperDistance * 0.5D); - TerminalPosition position = positionAt(polyline, targetDistance); - EffectiveRiverSettings settings = settingsAt(reach.to().x(), reach.to().z()); - if (settings.terminalMode() != IrisRiverTerminalMode.SINKHOLE_GROTTO) { - return null; - } - long stableId = art.arcane.iris.engine.river.RiverNetwork.mix( - reach.id().stableId() ^ TERMINAL_CAVE_ANCHOR_SALT - ); - return new TerminalCaveAnchor(stableId, position.x(), position.z(), position.alongReach()); - } - - private boolean shouldTaperTerminal(RiverReach reach) { - return settingsAt(reach.to().x(), reach.to().z()).terminalMode() - != IrisRiverTerminalMode.SINKHOLE_GROTTO; - } - - private TerminalPosition positionAt(RiverPolyline polyline, double targetDistance) { - double traversed = 0D; - for (int point = 0; point < polyline.size() - 1; point++) { - double startX = polyline.x(point); - double startZ = polyline.z(point); - double deltaX = polyline.x(point + 1) - startX; - double deltaZ = polyline.z(point + 1) - startZ; - double segmentLength = StrictMath.hypot(deltaX, deltaZ); - if (targetDistance <= traversed + segmentLength || point == polyline.size() - 2) { - double factor = segmentLength == 0D ? 0D : (targetDistance - traversed) / segmentLength; - factor = Math.max(0D, Math.min(1D, factor)); - double alongReach = polyline.length() == 0D ? 0D : targetDistance / polyline.length(); - return new TerminalPosition( - startX + (deltaX * factor), - startZ + (deltaZ * factor), - alongReach - ); - } - traversed += segmentLength; - } - return new TerminalPosition( - polyline.x(polyline.size() - 1), - polyline.z(polyline.size() - 1), - 1D - ); - } - - double waterSurface(RiverReach reach, double alongReach, boolean naturalOcean) { - if (naturalOcean) { - return dimensionFluidHeight; - } - if (reach == null || water.getMode() == IrisRiverWaterMode.FIXED) { - return riverFluidHeight; - } - return terracedWaterSurface( - reach.from().hydraulicHeight(), - reach.to().hydraulicHeight(), - reach.polyline().length(), - alongReach - ); - } - - double terracedWaterSurface( - double fromNaturalHeight, - double toNaturalHeight, - double reachLength, - double alongReach - ) { - int dropHeight = Math.max(1, water.getDropHeight()); - int fromHead = nodeWaterHead(fromNaturalHeight, dropHeight); - int toHead = nodeWaterHead(toNaturalHeight, dropHeight); - int headDelta = toHead - fromHead; - int availableDrops = StrictMath.abs(headDelta) / dropHeight; - if (availableDrops == 0) { - return fromHead; - } - double normalized = clamp01(alongReach); - double distance = normalized * Math.max(0D, reachLength); - double configuredPoolLength = Math.max(1D, water.getPoolLength()); - double requiredInteriorLength = configuredPoolLength * Math.max(0, availableDrops - 1); - double requiredTargetLength = configuredPoolLength * (availableDrops + 1D); - double dropSpacing; - double firstDrop; - if (reachLength >= requiredTargetLength) { - dropSpacing = configuredPoolLength; - firstDrop = (reachLength - requiredInteriorLength) * 0.5D; - } else { - dropSpacing = reachLength / (availableDrops + 1D); - firstDrop = dropSpacing; - } - int completedDrops; - if (normalized >= 1D || dropSpacing <= 0D) { - completedDrops = availableDrops; - } else if (distance < firstDrop) { - completedDrops = 0; - } else { - completedDrops = 1 + (int) StrictMath.floor((distance - firstDrop) / dropSpacing); - completedDrops = Math.min(availableDrops, completedDrops); - } - int direction = Integer.signum(headDelta); - return fromHead + direction * completedDrops * dropHeight; - } - - private double bedRoughness(double x, double z) { - return bedNoise.fitDouble( - -terrain.getBedRoughness(), - terrain.getBedRoughness(), - x, - z - ); - } - - private static double incisedHeight( - double naturalHeight, - double bedHeight, - double carveWeight, - double maximumIncision - ) { - double targetHeight = naturalHeight + (bedHeight - naturalHeight) * carveWeight; - double guardedTarget = Math.max(targetHeight, naturalHeight - maximumIncision); - return Math.min(naturalHeight, guardedTarget); - } - - private int nodeWaterHead(double naturalNodeHeight, int dropHeight) { - int availableRise = Math.max(0, water.getMaximumPoolRise()); - int maximumHead = riverFluidHeight + availableRise; - int naturalHead = (int) StrictMath.floor(naturalNodeHeight - 1D); - int clamped = Math.max(riverFluidHeight, Math.min(maximumHead, naturalHead)); - return riverFluidHeight + Math.floorDiv(clamped - riverFluidHeight, dropHeight) * dropHeight; - } - - private static boolean isNaturalOcean(IrisBiome biome) { - return biome != null && biome.getInferredType() == InferredType.SEA; - } - - static double terminalWeight(int terminalTaper, double reachLength, double alongReach) { - double taperFraction = Math.min( - 1D, - Math.max(0, terminalTaper) / Math.max(0.000001D, reachLength) - ); - double taperStart = 1D - taperFraction; - if (alongReach <= taperStart) { - return 1D; - } - return clamp01((1D - alongReach) / Math.max(0.000001D, taperFraction)); - } - - private PoolSelection poolFor(RiverSection section, EffectiveRiverSettings settings) { - return switch (section) { - case CHANNEL -> new PoolSelection(settings.channelBiomes(), InferredType.SEA); - case MOUTH -> new PoolSelection(settings.mouthBiomes(), InferredType.SEA); - case BANK -> new PoolSelection(settings.bankBiomes(), InferredType.SHORE); - case DRY_CHANNEL, DRY_BANK -> new PoolSelection(settings.dryBiomes(), InferredType.LAND); - case NONE -> new PoolSelection(List.of(), InferredType.LAND); - }; - } - - private List loadBiomePool(BiomePoolKey pool) { - KList loaded = data.getBiomeLoader().loadAll(new KList<>(pool.keys())); - ArrayList inferred = new ArrayList<>(loaded.size()); - for (IrisBiome biome : loaded) { - if (biome != null) { - inferred.add(biome.withInferredType(pool.type())); - } - } - return List.copyOf(inferred); - } - - private CNG noise(IrisRiverNoiseChance configured, long salt) { - IrisGeneratorStyle style = configured == null ? null : configured.getStyle(); - return noise(style, salt); - } - - private CNG noise(IrisStyledRange configured, long salt) { - IrisGeneratorStyle style = configured == null ? null : configured.getStyle(); - return noise(style, salt); - } - - private CNG noise(IrisGeneratorStyle configured, long salt) { - IrisGeneratorStyle style = configured == null ? new IrisGeneratorStyle(NoiseStyle.FLAT) : configured; - return style.createNoCache(new RNG(seed ^ salt), data); - } - - private double effectiveChance(IrisRiverNoiseChance configured, CNG noise, int x, int z) { - if (configured == null) { - return 1D; - } - double contribution = noise.fitDouble(-configured.getInfluence(), configured.getInfluence(), x, z); - return clamp01(configured.getChance() + contribution); - } - - private double chanceMultiplier(IrisRiverNoiseChance configured, CNG noise, int x, int z) { - double baseChance = chance(configured); - if (baseChance <= 0D) { - return 0D; - } - return effectiveChance(configured, noise, x, z) / baseChance; - } - - private static double chance(IrisRiverNoiseChance configured) { - return configured == null ? 1D : clamp01(configured.getChance()); - } - - private static double mid(IrisStyledRange range, double fallback) { - if (range == null || !Double.isFinite(range.getMin()) || !Double.isFinite(range.getMax())) { - return fallback; - } - return Math.max(0.000001D, (range.getMin() + range.getMax()) * 0.5D); - } - - private static double styled(IrisStyledRange range, CNG noise, int x, int z, double fallback) { - if (range == null || !Double.isFinite(range.getMin()) || !Double.isFinite(range.getMax())) { - return fallback; - } - double minimum = Math.min(range.getMin(), range.getMax()); - double maximum = Math.max(range.getMin(), range.getMax()); - if (minimum == maximum) { - return minimum; - } - return noise.fitDouble(minimum, maximum, x, z); - } - - private static double maximumReachRadius(IrisRiverTopology topology, IrisRiverTerrain riverTerrain) { - double surfaceRadius = riverTerrain.getMaxChannelWidth() * 0.5D - + riverTerrain.getMaxBankWidth(); - double tunnelRadius = riverTerrain.getMaxChannelWidth() * 0.5D - * maximumTunnelWidthMultiplier(riverTerrain) - + riverTerrain.getTunnelMouthBlend(); - return Math.max(surfaceRadius, tunnelRadius); - } - - private static double maximumTunnelWidthMultiplier(IrisRiverTerrain riverTerrain) { - IrisStyledRange configured = riverTerrain.getTunnelWidthMultiplier(); - if (configured == null - || !Double.isFinite(configured.getMin()) - || !Double.isFinite(configured.getMax())) { - return 1D; - } - return Math.max(1D, Math.max(configured.getMin(), configured.getMax())); - } - - private static double clamp01(double value) { - return Math.max(0D, Math.min(1D, value)); - } - - private static int clampToInt(double value) { - return (int) StrictMath.max(Integer.MIN_VALUE, StrictMath.min(Integer.MAX_VALUE, value)); - } - - private static double unit(long hash) { - return (hash >>> 11) * 0x1.0p-53; - } - - private static final class TunnelSampleChunk { - private final AtomicReferenceArray samples = new AtomicReferenceArray<>(256); - - private IrisRiverTunnelSample sample(IrisRiverRuntime runtime, int x, int z) { - int index = ((x & 15) << 4) | (z & 15); - Object cached = samples.get(index); - if (cached == null) { - IrisRiverTunnelSample computed = runtime.sampleTunnel((double) x, (double) z); - Object encoded = computed == null ? ABSENT_TUNNEL_SAMPLE : computed; - if (samples.compareAndSet(index, null, encoded)) { - cached = encoded; - } else { - cached = samples.get(index); - } - } - return cached == ABSENT_TUNNEL_SAMPLE ? null : (IrisRiverTunnelSample) cached; - } - } - - private final class RuntimeTerrainSampler implements RiverTerrainSampler { - private final IrisRiverTopology topology; - - private RuntimeTerrainSampler(IrisRiverTopology topology) { - this.topology = topology; - } - - @Override - public RiverTerrainNodeSample sampleNode(int blockX, int blockZ) { - boolean oceanIntent = Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ)); - boolean naturalHeightRequired = topology.getTerrainHeightWeight() > 0D - || water.getMode() != IrisRiverWaterMode.FIXED - || oceanIntent; - double sampledNaturalHeight = naturalHeightRequired - ? naturalHeight.get(blockX, blockZ) - : dimensionFluidHeight; - boolean sampledOcean = oceanIntent && isSubmergedOutlet(sampledNaturalHeight); - IrisRegion sampledRegion = region.get(blockX, blockZ); - IrisBiome sampledBiome = biomeRiverOverridesPossible ? naturalBiome.get(blockX, blockZ) : null; - EffectiveRiverSettings settings = settingsFor(sampledRegion, sampledBiome); - return new RiverTerrainNodeSample( - sampledNaturalHeight, - sampledOcean, - settings.routingPolicy() != IrisRiverRoutingPolicy.BLOCK, - routingCost(blockX, blockZ, settings) - ); - } - - @Override - public RiverTerrainSourceSample sampleSource(int blockX, int blockZ) { - EffectiveRiverSettings settings = settingsAt(blockX, blockZ); - double chanceMultiplier = settings.allowSources() - ? chanceMultiplier(topology.getSource(), sourceNoise, blockX, blockZ) - : 0D; - return new RiverTerrainSourceSample( - chanceMultiplier, - settings.routingPolicy() != IrisRiverRoutingPolicy.BLOCK, - isSubmergedOceanIntent(blockX, blockZ) - ); - } - - @Override - public double naturalHeight(int blockX, int blockZ) { - return IrisRiverRuntime.this.naturalHeight.get(blockX, blockZ); - } - - @Override - public boolean isOcean(int blockX, int blockZ) { - return isSubmergedOceanIntent(blockX, blockZ); - } - - private boolean isSubmergedOceanIntent(int blockX, int blockZ) { - return Boolean.TRUE.equals(naturalOcean.get(blockX, blockZ)) - && isSubmergedOutlet(naturalHeight.get(blockX, blockZ)); - } - - private boolean isSubmergedOutlet(double sampledNaturalHeight) { - return Math.round(sampledNaturalHeight) < Math.round(dimensionFluidHeight); - } - - @Override - public double routingCost(int blockX, int blockZ) { - EffectiveRiverSettings settings = settingsAt(blockX, blockZ); - return routingCost(blockX, blockZ, settings); - } - - @Override - public double sourceChanceMultiplier(int blockX, int blockZ) { - EffectiveRiverSettings settings = settingsAt(blockX, blockZ); - if (!settings.allowSources()) { - return 0D; - } - return chanceMultiplier(topology.getSource(), sourceNoise, blockX, blockZ); - } - - @Override - public double maximumSourceChanceMultiplier() { - IrisRiverNoiseChance configured = topology.getSource(); - if (configured == null) { - return 1D; - } - double baseChance = chance(configured); - if (baseChance <= 0D) { - return 0D; - } - return clamp01(baseChance + StrictMath.abs(configured.getInfluence())) / baseChance; - } - - @Override - public double reachChanceMultiplier(int blockX, int blockZ) { - EffectiveRiverSettings settings = settingsAt(blockX, blockZ); - return chanceMultiplier(topology.getContinuation(), continuationNoise, blockX, blockZ) - * settings.continuationChanceMultiplier(); - } - - @Override - public boolean allowsRiver(int blockX, int blockZ) { - return settingsAt(blockX, blockZ).routingPolicy() != IrisRiverRoutingPolicy.BLOCK; - } - - @Override - public boolean allowsReach(RiverRoutingContext context) { - if (!incisionGate(context)) { - return false; - } - if (boreMantleActive) { - if (!blockingRoutingPossible) { - return true; - } - return RiverPolylineProbe.all( - context.polyline(), - MAXIMUM_REACH_FEASIBILITY_SAMPLES, - (x, z, alongReach) -> settingsAt(x, z).routingPolicy() - != IrisRiverRoutingPolicy.BLOCK - ); - } - double depth = depth(context, mid(terrain.getDepth(), 4D)); - return RiverPolylineProbe.all( - context.polyline(), - MAXIMUM_REACH_FEASIBILITY_SAMPLES, - (x, z, alongReach) -> allowsReachSample(context, depth, alongReach, x, z) - ); - } - - private boolean allowsReachSample( - RiverRoutingContext context, - double depth, - double alongReach, - int x, - int z - ) { - double maximumIncision = terrain.getMaxIncision(); - if (blockingRoutingPossible || variableMaxIncisionPossible) { - EffectiveRiverSettings settings = settingsAt(x, z); - if (settings.routingPolicy() == IrisRiverRoutingPolicy.BLOCK) { - return false; - } - maximumIncision *= settings.maxIncisionMultiplier(); - } - double head = configuration.getWater().getMode() == IrisRiverWaterMode.FIXED - ? riverFluidHeight - : terracedWaterSurface( - context.from().hydraulicHeight(), - context.to().hydraulicHeight(), - context.polyline().length(), - alongReach - ); - int rangeDecision = boundedFeasibilityRange( - naturalHeightBounds.sample(x, z), - head, - maximumIncision - ); - if (rangeDecision == FEASIBILITY_ACCEPT) { - return true; - } - if (rangeDecision == FEASIBILITY_REJECT) { - return false; - } - double sampledNaturalHeight = naturalHeight(x, z); - int boundedDecision = boundedFeasibility( - sampledNaturalHeight, - head, - maximumIncision, - depth, - terrain.getBedRoughness()); - if (boundedDecision == FEASIBILITY_ACCEPT) { - return true; - } - if (boundedDecision == FEASIBILITY_REJECT) { - return false; - } - maximumIncision = Math.max(0D, maximumIncision); - double bedHeight = head - depth + bedRoughness(x, z); - double finalHeight = incisedHeight(sampledNaturalHeight, bedHeight, 1D, maximumIncision); - return Math.round(finalHeight) < Math.round(head); - } - - @Override - public double reachRoutingCost(RiverRoutingContext context) { - return routingCost(context.midpointX(), context.midpointZ()); - } - - @Override - public double flowNoise(double x, double z) { - return routingNoise.fitDouble(-1D, 1D, x, z); - } - - @Override - public double channelWidth(RiverRoutingContext context, double fallback) { - EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ()); - return styled(terrain.getChannelWidth(), widthNoise, context.midpointX(), context.midpointZ(), fallback) - * settings.widthMultiplier(); - } - - @Override - public double channelWidth( - RiverRoutingContext context, - double x, - double z, - double fallback - ) { - EffectiveRiverSettings settings = settingsAt(x, z); - return styled( - terrain.getChannelWidth(), - widthNoise, - (int) StrictMath.round(x), - (int) StrictMath.round(z), - fallback - ) - * settings.widthMultiplier(); - } - - @Override - public double bankWidth(RiverRoutingContext context, double fallback) { - EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ()); - return styled(terrain.getBankWidth(), bankNoise, context.midpointX(), context.midpointZ(), fallback) - * settings.bankWidthMultiplier(); - } - - @Override - public double bankWidth(RiverRoutingContext context, double x, double z, double fallback) { - EffectiveRiverSettings settings = settingsAt(x, z); - return styled( - terrain.getBankWidth(), - bankNoise, - (int) StrictMath.round(x), - (int) StrictMath.round(z), - fallback - ) * settings.bankWidthMultiplier(); - } - - @Override - public double depth(RiverRoutingContext context, double fallback) { - EffectiveRiverSettings settings = settingsAt(context.midpointX(), context.midpointZ()); - double configuredDepth = styled( - terrain.getDepth(), - depthNoise, - context.midpointX(), - context.midpointZ(), - fallback - ) * settings.depthMultiplier(); - return Math.min( - terrain.getMaxDepth(), - Math.max(1D + terrain.getBedRoughness(), configuredDepth) - ); - } - - @Override - public double depth(RiverRoutingContext context, double x, double z, double fallback) { - EffectiveRiverSettings settings = settingsAt(x, z); - double configuredDepth = styled( - terrain.getDepth(), - depthNoise, - (int) StrictMath.round(x), - (int) StrictMath.round(z), - fallback - ) * settings.depthMultiplier(); - return Math.min( - terrain.getMaxDepth(), - Math.max(1D + terrain.getBedRoughness(), configuredDepth) - ); - } - - @Override - public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) { - EffectiveRiverSettings settings = settingsAt(blockX, blockZ); - if (settings.terminalMode() == IrisRiverTerminalMode.SINKHOLE_GROTTO) { - return caveHydrologyActive ? RiverTerminalPolicy.WET : RiverTerminalPolicy.SUPPRESS; - } - if (!topology.isRequireOcean() && !settings.terminalModeOverridden()) { - return RiverTerminalPolicy.WET; - } - return switch (settings.terminalMode()) { - case SINKHOLE_GROTTO -> caveHydrologyActive - ? RiverTerminalPolicy.WET - : RiverTerminalPolicy.SUPPRESS; - case DRY_CHANNEL -> RiverTerminalPolicy.DRY; - case SUPPRESS -> RiverTerminalPolicy.SUPPRESS; - }; - } - - private double routingCost(int blockX, int blockZ, EffectiveRiverSettings settings) { - double noiseCost = routingNoise.fitDouble(0D, topology.getRoutingNoiseWeight(), blockX, blockZ); - double slopeWeight = topology.getTerrainSlopeWeight(); - double slopeCost = slopeWeight <= 0D - ? 0D - : Math.max(0D, naturalSlope.get(blockX, blockZ)) * slopeWeight; - double avoidance = settings.routingPolicy() == IrisRiverRoutingPolicy.AVOID - ? topology.getRoutingNoiseWeight() + topology.getOceanAttraction() + 64D - : 0D; - return (noiseCost + slopeCost + avoidance) * settings.routingCostMultiplier(); - } - - private boolean incisionGate(RiverRoutingContext context) { - IrisRiverNoiseChance configured = terrain.getIncision(); - double chance = effectiveChance( - configured, - incisionNoise, - context.midpointX(), - context.midpointZ() - ); - long hash = art.arcane.iris.engine.river.RiverNetwork.mix( - seed ^ context.edgeId().stableId() ^ INCISION_GATE_SALT - ); - return unit(hash) < chance; - } - - } - - static int boundedFeasibility( - double naturalHeight, - double head, - double maximumIncision, - double depth, - double maximumBedRoughness - ) { - long roundedHead = Math.round(head); - if (Math.round(naturalHeight) < roundedHead) { - return FEASIBILITY_ACCEPT; - } - double clampedIncision = Math.max(0D, maximumIncision); - if (Math.round(naturalHeight - clampedIncision) >= roundedHead) { - return FEASIBILITY_REJECT; - } - double maximumBedHeight = head - depth + Math.abs(maximumBedRoughness); - return Math.round(maximumBedHeight) < roundedHead - ? FEASIBILITY_ACCEPT - : FEASIBILITY_SAMPLE_BED; - } - - static int boundedFeasibilityRange( - NoiseBounds bounds, - double head, - double maximumIncision - ) { - double minimum = Math.min(bounds.min(), bounds.max()); - double maximum = Math.max(bounds.min(), bounds.max()); - long roundedHead = Math.round(head); - if (Math.round(maximum) < roundedHead) { - return FEASIBILITY_ACCEPT; - } - if (Math.round(minimum - Math.max(0D, maximumIncision)) >= roundedHead) { - return FEASIBILITY_REJECT; - } - return FEASIBILITY_SAMPLE_BED; - } - - private record ResolvedRiverColumn( - RiverSample river, - RiverReach reach, - double naturalHeight, - double waterSurfaceY, - double bedHeight, - double maximumIncision, - boolean subterranean - ) { - } - - private record CenterlinePosition(double x, double z) { - } - - private record PoolSelection(List keys, InferredType type) { - } - - private record TerminalPosition(double x, double z, double alongReach) { - } - - private record TerminalCaveAnchor(long stableId, double x, double z, double alongReach) { - } - - private record BiomePoolKey(List keys, InferredType type) { - private BiomePoolKey { - keys = List.copyOf(keys); - Objects.requireNonNull(type); - } - } - - private static final class IdentitySettingsKey { - private final IrisRegion region; - private final IrisBiome biome; - private final int hash; - - private IdentitySettingsKey(IrisRegion region, IrisBiome biome) { - this.region = region; - this.biome = biome; - hash = 31 * System.identityHashCode(region) + System.identityHashCode(biome); - } - - @Override - public boolean equals(Object other) { - return other instanceof IdentitySettingsKey key - && key.region == region - && key.biome == biome; - } - - @Override - public int hashCode() { - return hash; - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntimeContext.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntimeContext.java deleted file mode 100644 index 27a1e69a5..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntimeContext.java +++ /dev/null @@ -1,39 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.util.project.stream.ProceduralStream; - -import java.util.Objects; - -public record IrisRiverRuntimeContext( - long seed, - IrisRiverNetwork configuration, - IrisData data, - int riverFluidHeight, - int dimensionFluidHeight, - boolean boreMantleActive, - boolean caveHydrologyActive, - boolean blockingRoutingPossible, - boolean variableMaxIncisionPossible, - boolean biomeRiverOverridesPossible, - RiverHeightBoundsSampler naturalHeightBounds, - ProceduralStream naturalHeight, - ProceduralStream naturalSlope, - ProceduralStream naturalOcean, - ProceduralStream naturalBiome, - ProceduralStream region -) { - public IrisRiverRuntimeContext { - Objects.requireNonNull(configuration); - Objects.requireNonNull(data); - Objects.requireNonNull(naturalHeightBounds); - Objects.requireNonNull(naturalHeight); - Objects.requireNonNull(naturalSlope); - Objects.requireNonNull(naturalOcean); - Objects.requireNonNull(naturalBiome); - Objects.requireNonNull(region); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverSurfaceSample.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverSurfaceSample.java deleted file mode 100644 index 3c14e5a9f..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverSurfaceSample.java +++ /dev/null @@ -1,33 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.engine.river.RiverSample; - -import java.util.Objects; - -public record IrisRiverSurfaceSample( - RiverSample river, - double naturalHeight, - double terrainHeight, - double waterSurfaceY, - boolean subterranean, - boolean surfaceFluid -) { - public IrisRiverSurfaceSample { - Objects.requireNonNull(river); - if (!Double.isFinite(naturalHeight) || !Double.isFinite(terrainHeight) - || !Double.isFinite(waterSurfaceY)) { - throw new IllegalArgumentException("River surface values must be finite"); - } - } - - public static IrisRiverSurfaceSample none(double naturalHeight, double naturalWaterSurfaceY) { - return new IrisRiverSurfaceSample( - RiverSample.none(), - naturalHeight, - naturalHeight, - naturalWaterSurfaceY, - false, - Math.round(naturalHeight) < Math.round(naturalWaterSurfaceY) - ); - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverTunnelSample.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverTunnelSample.java deleted file mode 100644 index 9019cc944..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/IrisRiverTunnelSample.java +++ /dev/null @@ -1,22 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.engine.river.RiverSample; - -import java.util.Objects; - -public record IrisRiverTunnelSample( - RiverSample river, - int bedY, - int waterHeadY, - int ceilingY -) { - public IrisRiverTunnelSample { - Objects.requireNonNull(river); - if (!river.present()) { - throw new IllegalArgumentException("A river tunnel sample requires a present river"); - } - if (bedY >= waterHeadY || ceilingY < waterHeadY) { - throw new IllegalArgumentException("River tunnel vertical bounds are invalid"); - } - } -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/RiverHeightBoundsSampler.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/RiverHeightBoundsSampler.java deleted file mode 100644 index 803a91f70..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/RiverHeightBoundsSampler.java +++ /dev/null @@ -1,8 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.util.project.interpolation.NoiseBounds; - -@FunctionalInterface -public interface RiverHeightBoundsSampler { - NoiseBounds sample(int blockX, int blockZ); -} diff --git a/core/src/main/java/art/arcane/iris/engine/river/runtime/RiverPolylineProbe.java b/core/src/main/java/art/arcane/iris/engine/river/runtime/RiverPolylineProbe.java deleted file mode 100644 index 028e14f7d..000000000 --- a/core/src/main/java/art/arcane/iris/engine/river/runtime/RiverPolylineProbe.java +++ /dev/null @@ -1,56 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.engine.river.RiverPolyline; - -import java.util.Objects; - -final class RiverPolylineProbe { - private RiverPolylineProbe() { - } - - static boolean all(RiverPolyline polyline, int maximumSamples, CellPredicate predicate) { - Objects.requireNonNull(polyline); - Objects.requireNonNull(predicate); - if (maximumSamples < 2) { - throw new IllegalArgumentException("River polyline probing requires at least two samples"); - } - double totalLength = polyline.length(); - int desiredSamples = totalLength >= Integer.MAX_VALUE - ? Integer.MAX_VALUE - : (int) StrictMath.ceil(totalLength) + 1; - int sampleCount = StrictMath.max(2, StrictMath.min(maximumSamples, desiredSamples)); - int segment = 0; - for (int sample = 0; sample < sampleCount; sample++) { - double alongReach = (double) sample / (sampleCount - 1); - double targetDistance = totalLength * alongReach; - while (segment < polyline.size() - 2 - && targetDistance > polyline.cumulativeLength(segment + 1)) { - segment++; - } - double segmentStart = polyline.cumulativeLength(segment); - double segmentEnd = polyline.cumulativeLength(segment + 1); - double segmentLength = segmentEnd - segmentStart; - double factor = segmentLength <= 0D ? 0D : (targetDistance - segmentStart) / segmentLength; - double x = polyline.x(segment) - + (polyline.x(segment + 1) - polyline.x(segment)) * factor; - double z = polyline.z(segment) - + (polyline.z(segment + 1) - polyline.z(segment)) * factor; - if (!predicate.test(clampRound(x), clampRound(z), alongReach)) { - return false; - } - } - return true; - } - - private static int clampRound(double value) { - return (int) StrictMath.max( - Integer.MIN_VALUE, - StrictMath.min(Integer.MAX_VALUE, StrictMath.round(value)) - ); - } - - @FunctionalInterface - interface CellPredicate { - boolean test(int blockX, int blockZ, double alongReach); - } -} diff --git a/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java index 662c6e5ba..c2f4fb608 100644 --- a/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java +++ b/core/src/main/java/art/arcane/iris/util/project/hunk/view/ChunkDataHunkHolder.java @@ -76,11 +76,7 @@ public class ChunkDataHunkHolder extends AtomicHunk { } public void apply() { - applyTo(chunk); - } - - public void applyTo(ChunkData target) { - if (INMS.get().applyChunkDataBlocks(target, this)) { + if (INMS.get().applyChunkDataBlocks(chunk, this)) { return; } @@ -99,7 +95,7 @@ public class ChunkDataHunkHolder extends AtomicHunk { block = custom.getBase(); } if (block == null) { - flushRun(target, x, z, runStart, y, activeBlock); + flushRun(x, z, runStart, y, activeBlock); activeBlock = null; runStart = -1; continue; @@ -109,27 +105,27 @@ public class ChunkDataHunkHolder extends AtomicHunk { continue; } - flushRun(target, x, z, runStart, y, activeBlock); + flushRun(x, z, runStart, y, activeBlock); activeBlock = block; runStart = y; } - flushRun(target, x, z, runStart, height, activeBlock); + flushRun(x, z, runStart, height, activeBlock); } } } - private void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) { + private void flushRun(int x, int z, int startY, int endY, BlockData block) { if (block == null || startY < 0 || endY <= startY) { return; } - int minY = target.getMinHeight(); + int minY = chunk.getMinHeight(); if (endY - startY == 1) { - target.setBlock(x, startY + minY, z, block); + chunk.setBlock(x, startY + minY, z, block); return; } - target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block); + chunk.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block); } } diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java index 187f38498..83097b3cb 100644 --- a/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java +++ b/core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java @@ -25,7 +25,6 @@ import art.arcane.iris.util.project.matter.slices.EntityMatter; import art.arcane.iris.util.project.matter.slices.IdentifierMatter; import art.arcane.iris.util.project.matter.slices.NativeStructureOwnershipMatter; import art.arcane.iris.util.project.matter.slices.PlatformBlockMatter; -import art.arcane.iris.util.project.matter.slices.RiverCaveHydrologyMatter; import art.arcane.iris.util.project.matter.slices.SpawnerMatter; import art.arcane.iris.util.project.matter.slices.TileMatter; import art.arcane.iris.util.project.matter.slices.TreeBlockMaterialMatter; @@ -65,7 +64,6 @@ public final class IrisMatterSupport { IrisMatter.registerSliceType(new IdentifierMatter()); IrisMatter.registerSliceType(new NativeStructureOwnershipMatter()); IrisMatter.registerSliceType(new PlatformBlockMatter()); - IrisMatter.registerSliceType(new RiverCaveHydrologyMatter()); IrisMatter.registerSliceType(new SpawnerMatter()); IrisMatter.registerSliceType(new TileMatter()); IrisMatter.registerSliceType(new TreeBlockMaterialMatter()); diff --git a/core/src/main/java/art/arcane/iris/util/project/matter/slices/RiverCaveHydrologyMatter.java b/core/src/main/java/art/arcane/iris/util/project/matter/slices/RiverCaveHydrologyMatter.java deleted file mode 100644 index d18492c5b..000000000 --- a/core/src/main/java/art/arcane/iris/util/project/matter/slices/RiverCaveHydrologyMatter.java +++ /dev/null @@ -1,76 +0,0 @@ -package art.arcane.iris.util.project.matter.slices; - -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.volmlib.util.data.palette.Palette; -import art.arcane.volmlib.util.matter.Sliced; -import art.arcane.volmlib.util.matter.slices.RawMatter; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -@Sliced -public final class RiverCaveHydrologyMatter extends RawMatter { - public RiverCaveHydrologyMatter() { - this(1, 1, 1); - } - - public RiverCaveHydrologyMatter(int width, int height, int depth) { - super(width, height, depth, RiverCaveHydrology.class); - } - - @Override - public Palette getGlobalPalette() { - return null; - } - - @Override - public void writeNode(RiverCaveHydrology hydrology, DataOutputStream output) throws IOException { - output.writeByte(actionCode(hydrology.action())); - output.writeByte(fluidKindCode(hydrology.fluidKind())); - output.writeUTF(hydrology.floodedBiomeKey()); - } - - @Override - public RiverCaveHydrology readNode(DataInputStream input) throws IOException { - RiverCaveAction action = actionFromCode(input.readUnsignedByte()); - RiverCaveFluidKind fluidKind = fluidKindFromCode(input.readUnsignedByte()); - return new RiverCaveHydrology(action, input.readUTF(), fluidKind); - } - - private int actionCode(RiverCaveAction action) { - return switch (action) { - case WET_SOURCE -> 1; - case FALLING_FLUID -> 2; - case DRY_AIR -> 3; - case SEAL_GUARD -> 4; - }; - } - - private RiverCaveAction actionFromCode(int code) throws IOException { - return switch (code) { - case 1 -> RiverCaveAction.WET_SOURCE; - case 2 -> RiverCaveAction.FALLING_FLUID; - case 3 -> RiverCaveAction.DRY_AIR; - case 4 -> RiverCaveAction.SEAL_GUARD; - default -> throw new IOException("Unknown river cave hydrology action code " + code); - }; - } - - private int fluidKindCode(RiverCaveFluidKind fluidKind) { - return switch (fluidKind) { - case RIVER -> 1; - case DEEP_POOL -> 2; - }; - } - - private RiverCaveFluidKind fluidKindFromCode(int code) throws IOException { - return switch (code) { - case 1 -> RiverCaveFluidKind.RIVER; - case 2 -> RiverCaveFluidKind.DEEP_POOL; - default -> throw new IOException("Unknown river cave fluid-kind code " + code); - }; - } -} diff --git a/core/src/main/resources/languages/de_DE.json b/core/src/main/resources/languages/de_DE.json index 297203907..146e807ea 100644 --- a/core/src/main/resources/languages/de_DE.json +++ b/core/src/main/resources/languages/de_DE.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biom-Meer", "iris.desktop.vision.mode.region": "Region", "iris.desktop.vision.mode.cave_land": "Höhlenland", - "iris.desktop.vision.mode.river": "Flussnetz", "iris.desktop.vision.mode.height": "Höhe", "iris.desktop.vision.mode.object_load": "Objektlast", "iris.desktop.vision.mode.decorator_load": "Dekorlast", diff --git a/core/src/main/resources/languages/es_ES.json b/core/src/main/resources/languages/es_ES.json index 5b98d090d..481a9bc21 100644 --- a/core/src/main/resources/languages/es_ES.json +++ b/core/src/main/resources/languages/es_ES.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Mar del bioma", "iris.desktop.vision.mode.region": "Región", "iris.desktop.vision.mode.cave_land": "Tierra de cuevas", - "iris.desktop.vision.mode.river": "Red fluvial", "iris.desktop.vision.mode.height": "Altura", "iris.desktop.vision.mode.object_load": "Carga de objetos", "iris.desktop.vision.mode.decorator_load": "Carga de decoradores", diff --git a/core/src/main/resources/languages/fi_FI.json b/core/src/main/resources/languages/fi_FI.json index 9f3f0c947..b69e97388 100644 --- a/core/src/main/resources/languages/fi_FI.json +++ b/core/src/main/resources/languages/fi_FI.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biome-meri", "iris.desktop.vision.mode.region": "Alue", "iris.desktop.vision.mode.cave_land": "Luolamaa", - "iris.desktop.vision.mode.river": "Jokiverkosto", "iris.desktop.vision.mode.height": "Korkeus", "iris.desktop.vision.mode.object_load": "Kohteen kuormitus", "iris.desktop.vision.mode.decorator_load": "Koristuskuorma", diff --git a/core/src/main/resources/languages/fr_FR.json b/core/src/main/resources/languages/fr_FR.json index 6bbaa1be8..fe86fd6e9 100644 --- a/core/src/main/resources/languages/fr_FR.json +++ b/core/src/main/resources/languages/fr_FR.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Mer du biome", "iris.desktop.vision.mode.region": "Région", "iris.desktop.vision.mode.cave_land": "Terre des grottes", - "iris.desktop.vision.mode.river": "Réseau fluvial", "iris.desktop.vision.mode.height": "Hauteur", "iris.desktop.vision.mode.object_load": "Charge des objets", "iris.desktop.vision.mode.decorator_load": "Charge des décorateurs", diff --git a/core/src/main/resources/languages/he_IL.json b/core/src/main/resources/languages/he_IL.json index 0c4bc61a4..efdfe91e0 100644 --- a/core/src/main/resources/languages/he_IL.json +++ b/core/src/main/resources/languages/he_IL.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "הים Biome", "iris.desktop.vision.mode.region": "אזור", "iris.desktop.vision.mode.cave_land": "ארץ המערה", - "iris.desktop.vision.mode.river": "רשת נהרות", "iris.desktop.vision.mode.height": "גובה", "iris.desktop.vision.mode.object_load": "עומס אובייקטים", "iris.desktop.vision.mode.decorator_load": "עומס דקורטיבי", diff --git a/core/src/main/resources/languages/it_IT.json b/core/src/main/resources/languages/it_IT.json index 80e9730b4..427e8e066 100644 --- a/core/src/main/resources/languages/it_IT.json +++ b/core/src/main/resources/languages/it_IT.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Mare dei biomi", "iris.desktop.vision.mode.region": "Regione", "iris.desktop.vision.mode.cave_land": "Terreno della grotta", - "iris.desktop.vision.mode.river": "Rete fluviale", "iris.desktop.vision.mode.height": "Altezza", "iris.desktop.vision.mode.object_load": "Carico oggetto", "iris.desktop.vision.mode.decorator_load": "Carico decorativo", diff --git a/core/src/main/resources/languages/ja-JP.json b/core/src/main/resources/languages/ja-JP.json index 10e7d9d22..ed77dd64c 100644 --- a/core/src/main/resources/languages/ja-JP.json +++ b/core/src/main/resources/languages/ja-JP.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "海洋バイオーム", "iris.desktop.vision.mode.region": "リージョン", "iris.desktop.vision.mode.cave_land": "洞窟地形", - "iris.desktop.vision.mode.river": "河川網", "iris.desktop.vision.mode.height": "高さ", "iris.desktop.vision.mode.object_load": "オブジェクト負荷", "iris.desktop.vision.mode.decorator_load": "デコレーター負荷", diff --git a/core/src/main/resources/languages/ko_KR.json b/core/src/main/resources/languages/ko_KR.json index 086f3c05e..d700c7faf 100644 --- a/core/src/main/resources/languages/ko_KR.json +++ b/core/src/main/resources/languages/ko_KR.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biome 바다", "iris.desktop.vision.mode.region": "주요 특징", "iris.desktop.vision.mode.cave_land": "동굴 땅", - "iris.desktop.vision.mode.river": "하천망", "iris.desktop.vision.mode.height": "고도:", "iris.desktop.vision.mode.object_load": "객체 부하", "iris.desktop.vision.mode.decorator_load": "장식자 짐", diff --git a/core/src/main/resources/languages/lt_LT.json b/core/src/main/resources/languages/lt_LT.json index 4ed1ba76f..3448c7ce8 100644 --- a/core/src/main/resources/languages/lt_LT.json +++ b/core/src/main/resources/languages/lt_LT.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biominė jūra", "iris.desktop.vision.mode.region": "Regionas", "iris.desktop.vision.mode.cave_land": "Kavos žemė", - "iris.desktop.vision.mode.river": "Upių tinklas", "iris.desktop.vision.mode.height": "Aukštis", "iris.desktop.vision.mode.object_load": "Objekto apkrova", "iris.desktop.vision.mode.decorator_load": "Decorator apkrova", diff --git a/core/src/main/resources/languages/nl_NL.json b/core/src/main/resources/languages/nl_NL.json index b889f0919..dc4e1b3d1 100644 --- a/core/src/main/resources/languages/nl_NL.json +++ b/core/src/main/resources/languages/nl_NL.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biome zee", "iris.desktop.vision.mode.region": "Gebieden", "iris.desktop.vision.mode.cave_land": "Grotland", - "iris.desktop.vision.mode.river": "Riviernetwerk", "iris.desktop.vision.mode.height": "Hoogte", "iris.desktop.vision.mode.object_load": "Objectbelasting", "iris.desktop.vision.mode.decorator_load": "Decoratorbelasting", diff --git a/core/src/main/resources/languages/pl_PL.json b/core/src/main/resources/languages/pl_PL.json index 75ddd814a..4b05f9155 100644 --- a/core/src/main/resources/languages/pl_PL.json +++ b/core/src/main/resources/languages/pl_PL.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Morze Biome", "iris.desktop.vision.mode.region": "Obszar", "iris.desktop.vision.mode.cave_land": "Kraina jaskiń", - "iris.desktop.vision.mode.river": "Sieć rzeczna", "iris.desktop.vision.mode.height": "Wysokość", "iris.desktop.vision.mode.object_load": "Ładunek obiektu", "iris.desktop.vision.mode.decorator_load": "Obciążenie dekoratora", diff --git a/core/src/main/resources/languages/pt_PT.json b/core/src/main/resources/languages/pt_PT.json index 4cc9466c6..1ed63969a 100644 --- a/core/src/main/resources/languages/pt_PT.json +++ b/core/src/main/resources/languages/pt_PT.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Mar de bioma", "iris.desktop.vision.mode.region": "Região", "iris.desktop.vision.mode.cave_land": "Terras das cavernas", - "iris.desktop.vision.mode.river": "Rede fluvial", "iris.desktop.vision.mode.height": "Altura", "iris.desktop.vision.mode.object_load": "Carregamento do objeto", "iris.desktop.vision.mode.decorator_load": "Carga do decorador", diff --git a/core/src/main/resources/languages/ru_RU.json b/core/src/main/resources/languages/ru_RU.json index 8e8932d74..2db029713 100644 --- a/core/src/main/resources/languages/ru_RU.json +++ b/core/src/main/resources/languages/ru_RU.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Биологическое море", "iris.desktop.vision.mode.region": "регион", "iris.desktop.vision.mode.cave_land": "Пещерная земля", - "iris.desktop.vision.mode.river": "Речная сеть", "iris.desktop.vision.mode.height": "высота", "iris.desktop.vision.mode.object_load": "Объектная нагрузка", "iris.desktop.vision.mode.decorator_load": "Загрузка декоратора", diff --git a/core/src/main/resources/languages/tr_TR.json b/core/src/main/resources/languages/tr_TR.json index debb5802a..3a47f9418 100644 --- a/core/src/main/resources/languages/tr_TR.json +++ b/core/src/main/resources/languages/tr_TR.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biome deniz", "iris.desktop.vision.mode.region": "Bölge Bölgesi", "iris.desktop.vision.mode.cave_land": "Cave land Cave", - "iris.desktop.vision.mode.river": "Nehir ağı", "iris.desktop.vision.mode.height": "Yükseklik", "iris.desktop.vision.mode.object_load": "Object yükü", "iris.desktop.vision.mode.decorator_load": "Decorator yükü", diff --git a/core/src/main/resources/languages/vi_VI.json b/core/src/main/resources/languages/vi_VI.json index 73a65069e..1b5a31618 100644 --- a/core/src/main/resources/languages/vi_VI.json +++ b/core/src/main/resources/languages/vi_VI.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "Biển lấp lánh", "iris.desktop.vision.mode.region": "Vùng", "iris.desktop.vision.mode.cave_land": "Vùng đất hang động", - "iris.desktop.vision.mode.river": "Mạng lưới sông ngòi", "iris.desktop.vision.mode.height": "Chiều cao", "iris.desktop.vision.mode.object_load": "Trọng tải đối tượng", "iris.desktop.vision.mode.decorator_load": "Tải bộ phân giải", diff --git a/core/src/main/resources/languages/zh_CN.json b/core/src/main/resources/languages/zh_CN.json index 983026bab..2e3ed665c 100644 --- a/core/src/main/resources/languages/zh_CN.json +++ b/core/src/main/resources/languages/zh_CN.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "生物海", "iris.desktop.vision.mode.region": "地区", "iris.desktop.vision.mode.cave_land": "洞穴土地", - "iris.desktop.vision.mode.river": "河流网络", "iris.desktop.vision.mode.height": "高度", "iris.desktop.vision.mode.object_load": "对象负载", "iris.desktop.vision.mode.decorator_load": "加载装饰器", diff --git a/core/src/main/resources/languages/zh_TW.json b/core/src/main/resources/languages/zh_TW.json index 7f0e835ed..6e1984825 100644 --- a/core/src/main/resources/languages/zh_TW.json +++ b/core/src/main/resources/languages/zh_TW.json @@ -1398,7 +1398,6 @@ "iris.desktop.vision.mode.biome_sea": "生物海", "iris.desktop.vision.mode.region": "地區", "iris.desktop.vision.mode.cave_land": "洞穴土地", - "iris.desktop.vision.mode.river": "河流網絡", "iris.desktop.vision.mode.height": "高度", "iris.desktop.vision.mode.object_load": "物件負載", "iris.desktop.vision.mode.decorator_load": "載入裝飾器", diff --git a/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java b/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java index 14e63055e..1b2a22a73 100644 --- a/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java +++ b/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java @@ -57,7 +57,7 @@ public class VisionRenderControllerTest { VisionRenderController.TileKey baseline = new VisionRenderController.TileKey(4L, RenderType.BIOME, 4D, 8L, 9L); assertNotEquals(baseline, new VisionRenderController.TileKey(5L, RenderType.BIOME, 4D, 8L, 9L)); - assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.RIVER, 4D, 8L, 9L)); + assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.HEIGHT, 4D, 8L, 9L)); assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.BIOME, 4.5D, 8L, 9L)); assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.BIOME, 4D, 7L, 9L)); } diff --git a/core/src/test/java/art/arcane/iris/core/gui/VisionRenderTypeTest.java b/core/src/test/java/art/arcane/iris/core/gui/VisionRenderTypeTest.java index 3c6022c8c..f3e634030 100644 --- a/core/src/test/java/art/arcane/iris/core/gui/VisionRenderTypeTest.java +++ b/core/src/test/java/art/arcane/iris/core/gui/VisionRenderTypeTest.java @@ -7,7 +7,6 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; public class VisionRenderTypeTest { @Test @@ -19,12 +18,6 @@ public class VisionRenderTypeTest { } } - @Test - public void riverRenderTypeUsesTheRiverNetworkLabel() { - assertSame(DesktopUiMessages.VISION_MODE_RIVER, VisionGUI.modeKey(RenderType.RIVER)); - assertEquals("iris.desktop.vision.mode.river", DesktopUiMessages.VISION_MODE_RIVER.id()); - } - @Test public void teleportCoordinatesFloorAcrossTheNegativeOrigin() { assertEquals(-1, VisionGUI.floorWorldCoordinate(-0.01D)); diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java index e34d6ca2e..594e61883 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackDownloaderTest.java @@ -651,35 +651,6 @@ public class PackDownloaderTest { assertEquals(0, PackDownloader.downloadLockCount()); } - @Test - public void invalidBuiltInRiverSchemaPreservesExistingTarget() throws Exception { - File packsFolder = temp.newFolder("invalid-river-packs"); - File target = writePack(packsFolder.toPath().resolve("overworld"), "overworld", "old"); - File extracted = writePack(temp.newFolder("invalid-river-source").toPath(), "overworld", "new"); - Files.writeString( - extracted.toPath().resolve("dimensions/overworld.json"), - "{\"name\":\"Overworld\",\"regions\":[\"local\"],\"logicalHeight\":256," - + "\"dimensionHeight\":{\"min\":-64,\"max\":320},\"rivers\":{\"enabled\":true," - + "\"terrain\":{},\"water\":{\"mode\":\"SEA_LEVEL\"}}}", - StandardCharsets.UTF_8 - ); - - PackDownloader.PackInstallResult result = PackDownloader.installExtractedPack( - packsFolder, - extracted, - true, - "overworld", - ignored -> { - } - ); - - assertNull(result); - assertEquals("old", Files.readString(target.toPath().resolve("state.txt"), StandardCharsets.UTF_8)); - assertTrue(Files.isRegularFile(target.toPath().resolve("regions/local.json"))); - assertTransactionStateClean(packsFolder); - assertEquals(0, PackDownloader.downloadLockCount()); - } - @Test public void forceOverwriteReplacesEnginelessLoadedPackData() throws Exception { // A registered loader with no engines is a stale catalog registration (startup diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackRiverValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackRiverValidatorTest.java deleted file mode 100644 index 4b26d4988..000000000 --- a/core/src/test/java/art/arcane/iris/core/pack/PackRiverValidatorTest.java +++ /dev/null @@ -1,1122 +0,0 @@ -package art.arcane.iris.core.pack; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class PackRiverValidatorTest { - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void acceptsEnabledNetworkWithFallbackBiomePools() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "logicalHeight": 256, - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "biomes": { - "channel": [], - "bank": [], - "mouth": [], - "dry": [], - "floodedCave": [] - } - } - } - """); - - PackValidationResult result = PackValidator.validate(pack); - - assertTrue(result.getBlockingErrors().toString(), result.isLoadable()); - } - - @Test - public void acceptsRequiredValidWormProfiles() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [ - { - "id": "floodplain_trunk", - "seed": 17, - "weight": 2.5, - "wavelength": 1536, - "detailWavelength": 192, - "tortuosity": 0.65, - "detailTortuosity": 0.2, - "maxOffset": 420, - "segments": 56, - "widthMultiplier": 1.4, - "bankMultiplier": 1.2, - "depthMultiplier": 0.8, - "bodyWavelength": 1400, - "bodyDetailWavelength": 320, - "widthVariation": 0.65, - "bankVariation": 0.75, - "depthVariation": 0.45, - "roofVariation": 0.55, - "branchCap": 3, - "branchDecay": 0.25, - "confluenceMultiplier": 1.5, - "childChance": 0.2, - "branchChildChance": 0.6, - "children": [ - { - "id": "floodplain_tributary", - "seed": 29, - "weight": 1, - "wavelength": 640, - "detailWavelength": 128, - "tortuosity": 0.8, - "detailTortuosity": 0.3, - "maxOffset": 300, - "segments": 48, - "widthMultiplier": 0.7, - "bankMultiplier": 0.9, - "depthMultiplier": 1.3, - "bodyWavelength": 180, - "bodyDetailWavelength": 48, - "widthVariation": 0.8, - "bankVariation": 0.8, - "depthVariation": 0.65, - "roofVariation": 0.75, - "branchCap": 2, - "branchDecay": 0.1, - "confluenceMultiplier": 0.75, - "childChance": 0, - "branchChildChance": 0, - "children": [] - } - ] - } - ] - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertTrue(result.errors().toString(), result.errors().isEmpty()); - } - - @Test - public void rejectsInvalidWormHierarchyIdentifiersRangesAndChildren() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [ - { - "id": "trunk", - "seed": 17, - "bodyWavelength": 7, - "bodyDetailWavelength": 16385, - "bodyDetailInfluence": 1.1, - "widthVariation": -0.1, - "bankVariation": 0.876, - "depthVariation": -0.1, - "roofVariation": 0.876, - "branchCap": 0, - "branchDecay": 2, - "confluenceMultiplier": 9, - "childChance": -0.1, - "branchChildChance": 1.1, - "children": "tributary" - }, - {"id": "trunk", "seed": 29}, - {"id": "Bad ID", "seed": 31} - ] - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "rivers.terrain.worms[0].bodyWavelength must be at least 8"); - assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailWavelength must be at most 16384"); - assertContains(result.errors(), "rivers.terrain.worms[0].bodyDetailInfluence must be at most 1"); - assertContains(result.errors(), "rivers.terrain.worms[0].widthVariation must be at least 0"); - assertContains(result.errors(), "rivers.terrain.worms[0].bankVariation must be at most 0.875"); - assertContains(result.errors(), "rivers.terrain.worms[0].depthVariation must be at least 0"); - assertContains(result.errors(), "rivers.terrain.worms[0].roofVariation must be at most 0.875"); - assertContains(result.errors(), "rivers.terrain.worms[0].branchCap must be at least 1"); - assertContains(result.errors(), "rivers.terrain.worms[0].branchDecay must be at most 1"); - assertContains(result.errors(), "rivers.terrain.worms[0].confluenceMultiplier must be at most 8"); - assertContains(result.errors(), "rivers.terrain.worms[0].childChance must be at least 0"); - assertContains(result.errors(), "rivers.terrain.worms[0].branchChildChance must be at most 1"); - assertContains(result.errors(), "rivers.terrain.worms[0].children must be an array"); - assertContains(result.errors(), "rivers.terrain.worms[1].id must be unique inside the worm hierarchy"); - assertContains(result.errors(), - "rivers.terrain.worms[2].id must use 1 to 64 lowercase letters, digits, underscores, or hyphens"); - } - - @Test - public void rejectsWormHierarchyDepthAndProfileLimits() throws Exception { - File excessiveDepth = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [ - { - "id": "level_1", - "seed": 1, - "children": [ - { - "id": "level_2", - "seed": 2, - "children": [ - { - "id": "level_3", - "seed": 3, - "children": [ - { - "id": "level_4", - "seed": 4, - "children": [ - {"id": "level_5", "seed": 5} - ] - } - ] - } - ] - } - ] - } - ] - } - } - } - """); - File excessiveRoots = packWithWorms(wormHierarchy(17, 0)); - File excessiveProfiles = packWithWorms(wormHierarchy(16, 8)); - - PackRiverValidator.Validation depthResult = validate(excessiveDepth); - PackRiverValidator.Validation rootResult = validate(excessiveRoots); - PackRiverValidator.Validation profileResult = validate(excessiveProfiles); - - assertContains(depthResult.errors(), "children exceeds the maximum hierarchy depth of 4"); - assertContains(rootResult.errors(), "rivers.terrain.worms must contain at most 16 root profiles"); - assertContains(profileResult.errors(), "rivers.terrain.worms hierarchy must contain at most 128 profiles"); - } - - @Test - public void rejectsMissingAndEmptyWormLists() throws Exception { - File missing = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {} - } - } - """); - File empty = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": []} - } - } - """); - - PackRiverValidator.Validation missingResult = validate(missing); - PackRiverValidator.Validation emptyResult = validate(empty); - - assertContains(missingResult.errors(), - "rivers.terrain.worms must be an array with at least one Perlin-worm profile"); - assertContains(emptyResult.errors(), - "rivers.terrain.worms must contain at least one Perlin-worm profile"); - } - - @Test - public void rejectsNullEnabledNetworkSections() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "topology": null, - "terrain": null, - "water": null, - "biomes": null, - "caves": null - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "rivers.topology must be an object."); - assertContains(result.errors(), "rivers.terrain must be an object."); - assertContains(result.errors(), "rivers.water must be an object."); - assertContains(result.errors(), "rivers.biomes must be an object."); - assertContains(result.errors(), "rivers.caves must be an object."); - } - - @Test - public void rejectsInvalidFiniteRangesAndMalformedStyles() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "topology": { - "siteJitter": 1e400, - "tileCells": 1, - "minimumSourcesPerTile": 2, - "sinkSearchReaches": 8, - "routingBasinCells": 7, - "routingDeviationScaleCells": 7, - "routingDeviationStrengthCells": 33, - "routingPlateauHeight": 0, - "flowAlignmentWeight": 1025, - "confluenceWeight": -1, - "routingStyle": {"zoom": 0} - }, - "terrain": { - "worms": [{"id": "river"}], - "channelWidth": {"min": 40, "max": 12}, - "depth": {}, - "tunnelWidthMultiplier": {"min": 0.5, "max": 9}, - "tunnelMouthBlend": 17, - "tunnelFloorVariation": 9, - "tunnelRoofVariation": 17, - "tunnelFloorStyle": {"zoom": 0}, - "tunnelRoofStyle": {"zoom": 0} - }, - "caves": { - "parentBiomeInheritance": 2 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "rivers.topology.siteJitter must be a number."); - assertContains(result.errors(), "rivers.topology.minimumSourcesPerTile must not exceed tileCells squared."); - assertContains(result.errors(), "rivers.topology.sinkSearchReaches must be at most 7"); - assertContains(result.errors(), "rivers.topology.routingBasinCells must be at least 8"); - assertContains(result.errors(), "rivers.topology.routingDeviationScaleCells must be at least 8"); - assertContains(result.errors(), "rivers.topology.routingDeviationStrengthCells must be at most 32"); - assertContains(result.errors(), "rivers.topology.routingPlateauHeight must be at least 1"); - assertContains(result.errors(), "rivers.topology.flowAlignmentWeight must be at most 1024"); - assertContains(result.errors(), "rivers.topology.confluenceWeight must be at least 0"); - assertContains(result.errors(), "rivers.topology.routingStyle.zoom must be at least"); - assertContains(result.errors(), "rivers.terrain.channelWidth.min must not exceed"); - assertContains(result.errors(), "rivers.terrain.depth must set min and max explicitly."); - assertContains(result.errors(), "rivers.terrain.tunnelWidthMultiplier.min must be at least 1"); - assertContains(result.errors(), "rivers.terrain.tunnelWidthMultiplier.max must be at most 8"); - assertContains(result.errors(), "rivers.terrain.tunnelMouthBlend must be at most 16"); - assertContains(result.errors(), "rivers.terrain.tunnelFloorVariation must be at most 8"); - assertContains(result.errors(), "rivers.terrain.tunnelRoofVariation must be at most 16"); - assertContains(result.errors(), "rivers.terrain.tunnelFloorStyle.zoom must be at least"); - assertContains(result.errors(), "rivers.terrain.tunnelRoofStyle.zoom must be at least"); - assertContains(result.errors(), "rivers.caves.parentBiomeInheritance must be at most 1"); - } - - @Test - public void rejectsTerraceDropsAbovePermittedRise() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "water": { - "mode": "TERRACED", - "maximumPoolRise": 2, - "dropHeight": 3 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "rivers.water.dropHeight must not exceed maximumPoolRise"); - } - - @Test - public void acceptsIndependentCaveLavaRiverHeightAndPalette() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "dimensionHeight": {"min": -64, "max": 320}, - "fluidHeight": 63, - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "water": { - "mode": "FIXED", - "fluidHeight": -48, - "fluidPalette": { - "palette": [{"block": "minecraft:lava"}] - } - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertTrue(result.errors().toString(), result.errors().isEmpty()); - } - - @Test - public void acceptsSparseBlobbyDeepLavaPoolsWithIndependentHeight() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "dimensionHeight": {"min": -256, "max": 512}, - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "caves": { - "deepPools": { - "enabled": true, - "reach": { - "chance": 0.08, - "influence": 0.04, - "style": {"style": "IRIS", "zoom": 4096} - }, - "minimumSpacing": 768, - "maximumPerReach": 1, - "minimumFluidY": -224, - "maximumFluidY": -108, - "searchRadius": 20, - "searchAttempts": 12, - "horizontalRadius": 24, - "verticalRadius": 10, - "dryHeadroom": 5, - "shapeStyle": {"style": "IRIS", "zoom": 12}, - "shapeVariation": 0.6, - "warpStyle": {"style": "IRIS", "zoom": 24}, - "warpStrength": 8, - "maximumVolume": 65536, - "fluidPalette": { - "palette": [{"block": "minecraft:lava"}] - } - } - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertTrue(result.errors().toString(), result.errors().isEmpty()); - } - - @Test - public void rejectsUnsafeDeepPoolEnvelopeShapeAndPalette() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "dimensionHeight": {"min": -64, "max": 320}, - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "caves": { - "deepPools": { - "enabled": true, - "reach": {"chance": 0}, - "minimumFluidY": -90, - "maximumFluidY": -120, - "searchRadius": 120, - "horizontalRadius": 20, - "verticalRadius": 8, - "dryHeadroom": 8, - "maximumVolume": 64, - "fluidPalette": {"palette": []} - } - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "deepPools.minimumFluidY must not exceed maximumFluidY"); - assertContains(result.errors(), "deepPools.dryHeadroom must be smaller than verticalRadius"); - assertContains(result.errors(), "deepPools.searchRadius plus horizontalRadius must not exceed 128"); - assertContains(result.errors(), "deepPools.maximumVolume must be at least"); - assertContains(result.errors(), "deepPools.fluidPalette.palette must contain at least one fluid block"); - assertContains(result.errors(), "deepPools fluid range and chamber envelope must remain inside dimensionHeight"); - assertContains(result.warnings(), "deepPools is enabled but its reach gate cannot accept any pools"); - } - - @Test - public void rejectsRetiredWaterModeInvalidPaletteAndOutOfBoundsHeight() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "dimensionHeight": {"min": -64, "max": 320}, - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "water": { - "mode": "SEA_LEVEL", - "fluidHeight": -80, - "fluidPalette": {"palette": []} - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "rivers.water.mode must be one of"); - assertContains(result.errors(), "rivers.water.fluidHeight must remain inside dimensionHeight"); - assertContains(result.errors(), "rivers.water.fluidPalette.palette must contain at least one fluid block"); - } - - @Test - public void rejectsPathologicalCombinedTopologyComplexity() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "topology": { - "cellSize": 64, - "tileCells": 64, - "siteJitter": 0.49, - "maxRouteReaches": 256 - }, - "terrain": { - "channelWidth": {"min": 2048, "max": 2048}, - "bankWidth": {"min": 2048, "max": 2048}, - "orderWidthFactor": 8, - "worms": [{"id": "river", "maxOffset": 1024, "segments": 64}] - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "exceeds the safe derived complexity budget"); - assertContains(result.errors(), "source window requires"); - assertContains(result.errors(), "increase cellSize or reduce tileCells"); - } - - @Test - public void rejectsRiverGeometryCapsOutsideSupportedRanges() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [{"id": "river"}], - "maxChannelWidth": 0, - "maxBankWidth": -1, - "maxDepth": 513 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "rivers.terrain.maxChannelWidth"); - assertContains(result.errors(), "rivers.terrain.maxBankWidth"); - assertContains(result.errors(), "rivers.terrain.maxDepth"); - } - - @Test - public void rejectsTunnelFootprintsAboveDerivedHydrologyBudget() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [{"id": "river"}], - "maxChannelWidth": 2048, - "tunnelMouthBlend": 16 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "exceeds the safe derived hydrology budget"); - assertContains(result.errors(), "columns per generated chunk"); - } - - @Test - public void rejectsMissingBiomeReferencesAndWarnsAboutInferredRoles() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "biomes": { - "channel": ["biome", "missing"], - "bank": ["biome"] - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "references missing biome 'missing'"); - assertContains(result.warnings(), "inferred river role SEA"); - assertContains(result.warnings(), "inferred river role SHORE"); - } - - @Test - public void netherRiverBiomesKeepNetherDerivativesWithoutOverworldRoleWarnings() throws Exception { - File pack = pack(""" - { - "environment": "NETHER", - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "biomes": { - "channel": ["biome"], - "bank": ["biome"] - } - } - } - """); - write(pack, "biomes/biome.json", """ - { - "name": "Nether River", - "derivative": "minecraft:basalt_deltas", - "vanillaDerivative": "minecraft:basalt_deltas" - } - """); - write(pack, "regions/region.json", """ - { - "landBiomes": ["biome"], - "riverOverride": { - "channelBiomes": ["biome"], - "bankBiomes": ["biome"] - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertFalse(result.errors().toString(), contains(result.errors(), "biome")); - assertFalse(result.warnings().toString(), contains(result.warnings(), "inferred river role")); - } - - @Test - public void validatesRegionAndBiomeOverridesWhenRiversAreEnabled() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]} - } - } - """); - write(pack, "regions/region.json", """ - { - "landBiomes": ["biome"], - "riverOverride": { - "routingCostMultiplier": -1, - "channelBiomes": ["missing"] - } - } - """); - write(pack, "biomes/biome.json", """ - { - "name": "Biome", - "riverOverride": { - "allowSources": "yes", - "bankBiomes": [] - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "Region 'region' riverOverride.routingCostMultiplier must be at least"); - assertContains(result.errors(), "Region 'region' riverOverride.channelBiomes[0] references missing biome"); - assertContains(result.errors(), "Biome 'biome' riverOverride.allowSources must be a boolean"); - } - - @Test - public void rejectsZeroChannelAndDepthMultipliersInsteadOfRestoringFallbackGeometry() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]} - } - } - """); - write(pack, "regions/region.json", """ - { - "landBiomes": ["biome"], - "riverOverride": { - "widthMultiplier": 0, - "depthMultiplier": 0 - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "riverOverride.widthMultiplier must be at least 1.0E-4"); - assertContains(result.errors(), "riverOverride.depthMultiplier must be at least 1.0E-4"); - } - - @Test - public void rejectsMathematicallyImpossibleGeneratedGrotto() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "caves": { - "mode": "GENERATE_GROTTO", - "throatRadius": 4, - "grottoHorizontalRadius": 4, - "grottoVerticalRadius": 4, - "grottoWarpStrength": 3, - "dryHeadroom": 9, - "maxFloodRadius": 6, - "maxFloodDepth": 6, - "maxFloodVolume": 64 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "throatRadius must be smaller than both grotto radii"); - assertContains(result.errors(), "dryHeadroom must fit inside the generated grotto height"); - assertContains(result.errors(), "maxFloodRadius must be at least 8"); - assertContains(result.errors(), "maxFloodDepth must be at least 8"); - assertContains(result.errors(), "maxFloodVolume must be at least"); - } - - @Test - public void warnsWhenActiveCaveEntryGateCannotProduceConnections() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "caves": { - "mode": "FLOOD_CLOSED_COMPONENT", - "maximumPerReach": 0, - "maxBoreDepth": 64, - "maxFloodDepth": 32 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.warnings(), "entry gate cannot accept any connections"); - assertContains(result.warnings(), "maxBoreDepth exceeds maxFloodDepth"); - } - - @Test - public void rejectsSinkholeTerminalWithoutCaveHydrology() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [{"id": "river"}], - "terminalMode": "SINKHOLE_GROTTO" - }, - "caves": {"mode": "SEALED"} - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "SINKHOLE_GROTTO requires a non-SEALED caves.mode"); - } - - @Test - public void rejectsSinkholeTerminalWhenPerReachCapDisablesItsForcedAnchor() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [{"id": "river"}], - "terminalMode": "SINKHOLE_GROTTO" - }, - "caves": { - "mode": "GENERATE_GROTTO", - "maximumPerReach": 0 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "SINKHOLE_GROTTO requires caves.maximumPerReach above zero"); - } - - @Test - public void rejectsSinkholeTerminalWhenCarvingIsDisabled() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "carvingEnabled": false, - "rivers": { - "enabled": true, - "terrain": { - "worms": [{"id": "river"}], - "terminalMode": "SINKHOLE_GROTTO" - }, - "caves": {"mode": "GENERATE_GROTTO"} - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "SINKHOLE_GROTTO requires carvingEnabled to be true"); - } - - @Test - public void forcedSinkholeValidatesGrottoEnvelopeInClosedComponentModeOnce() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": { - "worms": [{"id": "river"}], - "terminalMode": "SINKHOLE_GROTTO" - }, - "caves": { - "mode": "FLOOD_CLOSED_COMPONENT", - "fallback": "SEALED", - "grottoHorizontalRadius": 12, - "grottoWarpStrength": 2, - "maxFloodRadius": 12 - } - } - } - """); - - PackRiverValidator.Validation result = validate(pack); - - String fragment = "rivers.caves.maxFloodRadius must be at least 15"; - assertContains(result.errors(), fragment); - assertTrue(result.errors().toString(), count(result.errors(), fragment) == 1); - } - - @Test - public void rejectsReachableRegionSinkholeAgainstInactiveDimensionHydrology() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "caves": {"mode": "SEALED"} - } - } - """); - write(pack, "regions/region.json", """ - { - "landBiomes": ["biome"], - "riverOverride": {"terminalMode": "SINKHOLE_GROTTO"} - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "Region 'region' riverOverride.terminalMode SINKHOLE_GROTTO requires a non-SEALED caves.mode"); - } - - @Test - public void rejectsReachableChildBiomeSinkholeAgainstDisabledCarving() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "carvingEnabled": false, - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "caves": {"mode": "GENERATE_GROTTO"} - } - } - """); - write(pack, "biomes/biome.json", """ - { - "name": "Biome", - "derivative": "minecraft:plains", - "children": ["child"] - } - """); - write(pack, "biomes/child.json", """ - { - "name": "Child", - "derivative": "minecraft:forest", - "riverOverride": {"terminalMode": "SINKHOLE_GROTTO"} - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "Biome 'child' riverOverride.terminalMode SINKHOLE_GROTTO requires carvingEnabled to be true"); - } - - @Test - public void rejectsTransitiveFinalTerrainExpressionDependency() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "topology": { - "routingStyle": {"expression": "route"} - } - } - } - """); - write(pack, "expressions/route.json", """ - { - "variables": [ - { - "name": "nested", - "styleValue": {"expression": "terrain"} - } - ], - "expression": "nested" - } - """); - write(pack, "expressions/terrain.json", """ - { - "functions": [ - { - "name": "height", - "engineStreamValue": "HEIGHT" - } - ], - "expression": "height(x,z)" - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "engineStreamValue 'HEIGHT'"); - assertContains(result.errors(), "would recurse during river generation"); - } - - @Test - public void rejectsFinalTerrainDependencyInsideExpressionEntrySnippet() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "topology": { - "routingStyle": {"expression": "route"} - } - } - } - """); - write(pack, "expressions/route.json", """ - { - "variables": ["snippet/expression-load/final-height"], - "expression": "height" - } - """); - write(pack, "snippet/expression-load/final-height.json", """ - { - "name": "height", - "engineStreamValue": "HEIGHT_OR_FLUID" - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "engineStreamValue 'HEIGHT_OR_FLUID'"); - } - - @Test - public void rejectsCyclicStyleSnippetDependenciesWithoutRecursing() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "topology": { - "routingStyle": "snippet/style/first" - } - } - } - """); - write(pack, "snippet/style/first.json", "{\"fracture\":\"snippet/style/second\"}"); - write(pack, "snippet/style/second.json", "{\"fracture\":\"snippet/style/first\"}"); - - PackRiverValidator.Validation result = validate(pack); - - assertContains(result.errors(), "cyclic river-noise style snippet dependency"); - } - - @Test - public void acceptsNaturalHeightExpressionDependency() throws Exception { - File pack = pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": [{"id": "river"}]}, - "topology": { - "routingStyle": {"expression": "route"} - } - } - } - """); - write(pack, "expressions/route.json", """ - { - "variables": [ - { - "name": "height", - "engineStreamValue": "NATURAL_HEIGHT" - } - ], - "expression": "height" - } - """); - - PackRiverValidator.Validation result = validate(pack); - - assertFalse(result.errors().toString(), contains(result.errors(), "engineStreamValue")); - } - - private PackRiverValidator.Validation validate(File pack) { - File[] dimensions = new File(pack, "dimensions").listFiles( - file -> file.isFile() && file.getName().endsWith(".json")); - return PackRiverValidator.validate(pack, dimensions); - } - - private File pack(String dimensionJson) throws Exception { - File pack = temporaryFolder.newFolder("pack-" + System.nanoTime()); - write(pack, "dimensions/main.json", dimensionJson); - write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}"); - write(pack, "biomes/biome.json", - "{\"name\":\"Biome\",\"derivative\":\"minecraft:plains\"}"); - return pack; - } - - private File packWithWorms(String worms) throws Exception { - return pack(""" - { - "regions": ["region"], - "rivers": { - "enabled": true, - "terrain": {"worms": %s} - } - } - """.formatted(worms)); - } - - private String wormHierarchy(int rootCount, int childrenPerRoot) { - StringBuilder hierarchy = new StringBuilder("["); - int seed = 1; - for (int rootIndex = 0; rootIndex < rootCount; rootIndex++) { - if (rootIndex > 0) { - hierarchy.append(','); - } - hierarchy.append("{\"id\":\"root_") - .append(rootIndex) - .append("\",\"seed\":") - .append(seed++); - if (childrenPerRoot > 0) { - hierarchy.append(",\"children\":["); - for (int childIndex = 0; childIndex < childrenPerRoot; childIndex++) { - if (childIndex > 0) { - hierarchy.append(','); - } - hierarchy.append("{\"id\":\"child_") - .append(rootIndex) - .append('_') - .append(childIndex) - .append("\",\"seed\":") - .append(seed++) - .append('}'); - } - hierarchy.append(']'); - } - hierarchy.append('}'); - } - return hierarchy.append(']').toString(); - } - - private void assertContains(List messages, String fragment) { - assertTrue(messages.toString(), contains(messages, fragment)); - } - - private boolean contains(List messages, String fragment) { - for (String message : messages) { - if (message.contains(fragment)) { - return true; - } - } - return false; - } - - private int count(List messages, String fragment) { - int matches = 0; - for (String message : messages) { - if (message.contains(fragment)) { - matches++; - } - } - return matches; - } - - private void write(File root, String relative, String content) throws Exception { - Path target = new File(root, relative).toPath(); - Files.createDirectories(target.getParent()); - Files.writeString(target, content, StandardCharsets.UTF_8); - } -} diff --git a/core/src/test/java/art/arcane/iris/core/project/IrisRiverSchemaTest.java b/core/src/test/java/art/arcane/iris/core/project/IrisRiverSchemaTest.java deleted file mode 100644 index 4619e1632..000000000 --- a/core/src/test/java/art/arcane/iris/core/project/IrisRiverSchemaTest.java +++ /dev/null @@ -1,192 +0,0 @@ -package art.arcane.iris.core.project; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.IrisRegistrant; -import art.arcane.iris.core.loader.ResourceLoader; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisBlockData; -import art.arcane.iris.engine.object.IrisExpression; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.object.IrisRiverOverride; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.json.JSONArray; -import art.arcane.volmlib.util.json.JSONObject; -import art.arcane.iris.spi.IrisPlatform; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformRegistries; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class IrisRiverSchemaTest { - private IrisPlatform previousPlatform; - - @Before - public void bindPlatform() { - previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; - if (previousPlatform != null) { - IrisPlatforms.unbind(); - } - IrisPlatform platform = mock(IrisPlatform.class); - PlatformRegistries registries = mock(PlatformRegistries.class); - when(platform.registries()).thenReturn(registries); - when(registries.blockTypeKeys()).thenReturn(List.of()); - IrisPlatforms.bind(platform); - } - - @After - public void restorePlatform() { - IrisPlatforms.unbind(); - if (previousPlatform != null) { - IrisPlatforms.bind(previousPlatform); - } - } - - @Test - public void riverNetworkSchemaExposesNestedNoiseLimitsModesAndBiomePools() { - JSONObject schema = new SchemaBuilder(IrisRiverNetwork.class, schemaData()).construct(); - JSONObject definitions = schema.getJSONObject("definitions"); - JSONObject properties = schema.getJSONObject("properties"); - JSONObject topology = referencedProperties(definitions, properties.getJSONObject("topology")); - JSONObject source = referencedProperties(definitions, topology.getJSONObject("source")); - JSONObject terrainDefinition = referencedDefinition(definitions, properties.getJSONObject("terrain")); - JSONObject terrain = terrainDefinition.getJSONObject("properties"); - JSONObject worms = terrain.getJSONObject("worms"); - JSONObject worm = referencedProperties(definitions, worms.getJSONObject("items")); - JSONObject water = referencedProperties(definitions, properties.getJSONObject("water")); - JSONObject biomes = referencedProperties(definitions, properties.getJSONObject("biomes")); - JSONObject caves = referencedProperties(definitions, properties.getJSONObject("caves")); - JSONObject deepPools = referencedProperties(definitions, caves.getJSONObject("deepPools")); - - assertEquals("boolean", properties.getJSONObject("enabled").getString("type")); - assertEquals(64, topology.getJSONObject("cellSize").getInt("minimum")); - assertEquals(4096, topology.getJSONObject("cellSize").getInt("maximum")); - assertEquals(7, topology.getJSONObject("sinkSearchReaches").getInt("maximum")); - assertEquals(8, topology.getJSONObject("routingBasinCells").getInt("minimum")); - assertEquals(256, topology.getJSONObject("routingBasinCells").getInt("maximum")); - assertEquals(0D, source.getJSONObject("chance").getDouble("minimum"), 0D); - assertEquals(1D, source.getJSONObject("chance").getDouble("maximum"), 0D); - assertEquals(1D, terrain.getJSONObject("maxChannelWidth").getDouble("minimum"), 0D); - assertEquals(2048D, terrain.getJSONObject("maxChannelWidth").getDouble("maximum"), 0D); - assertEquals(0D, terrain.getJSONObject("maxBankWidth").getDouble("minimum"), 0D); - assertEquals(512D, terrain.getJSONObject("maxDepth").getDouble("maximum"), 0D); - assertTrue(arrayContains(terrainDefinition.getJSONArray("required"), "worms")); - assertEquals("array", worms.getString("type")); - assertEquals(1, worms.getInt("minItems")); - assertEquals(0.000001D, worm.getJSONObject("weight").getDouble("minimum"), 0D); - assertEquals(16384D, worm.getJSONObject("wavelength").getDouble("maximum"), 0D); - assertEquals(1D, worm.getJSONObject("tortuosity").getDouble("maximum"), 0D); - assertEquals(1024D, worm.getJSONObject("maxOffset").getDouble("maximum"), 0D); - assertEquals(64, worm.getJSONObject("segments").getInt("maximum")); - assertEquals(0.125D, worm.getJSONObject("widthMultiplier").getDouble("minimum"), 0D); - assertEquals(8D, worm.getJSONObject("bankMultiplier").getDouble("maximum"), 0D); - assertEquals(8D, worm.getJSONObject("depthMultiplier").getDouble("maximum"), 0D); - assertEquals(8D, worm.getJSONObject("bodyWavelength").getDouble("minimum"), 0D); - assertEquals(16384D, worm.getJSONObject("bodyDetailWavelength").getDouble("maximum"), 0D); - assertEquals(1D, worm.getJSONObject("bodyDetailInfluence").getDouble("maximum"), 0D); - assertEquals(0.875D, worm.getJSONObject("widthVariation").getDouble("maximum"), 0D); - assertEquals(0.875D, worm.getJSONObject("bankVariation").getDouble("maximum"), 0D); - assertEquals(0.875D, worm.getJSONObject("depthVariation").getDouble("maximum"), 0D); - assertEquals(0.875D, worm.getJSONObject("roofVariation").getDouble("maximum"), 0D); - assertEquals(8, worm.getJSONObject("branchCap").getInt("maximum")); - assertEquals(1D, worm.getJSONObject("branchDecay").getDouble("maximum"), 0D); - assertEquals(8D, worm.getJSONObject("confluenceMultiplier").getDouble("maximum"), 0D); - assertEquals(1D, worm.getJSONObject("childChance").getDouble("maximum"), 0D); - assertEquals(1D, worm.getJSONObject("branchChildChance").getDouble("maximum"), 0D); - assertEquals("array", worm.getJSONObject("children").getString("type")); - assertEquals(List.of("FIXED", "TERRACED"), enumValues(definitions, water.getJSONObject("mode"))); - assertEquals(-2048, water.getJSONObject("fluidHeight").getInt("minimum")); - assertEquals(2048, water.getJSONObject("fluidHeight").getInt("maximum")); - assertTrue(water.has("fluidPalette")); - assertEquals(64D, terrain.getJSONObject("channelRadiusBonus").getDouble("maximum"), 0D); - assertEquals("array", biomes.getJSONObject("channel").getString("type")); - assertEquals("#/definitions/erzbiomes", - biomes.getJSONObject("channel").getJSONObject("items").getString("$ref")); - assertTrue(properties.has("terrain")); - assertTrue(properties.has("caves")); - assertEquals("boolean", deepPools.getJSONObject("enabled").getString("type")); - assertEquals(-2048, deepPools.getJSONObject("minimumFluidY").getInt("minimum")); - assertEquals(2048, deepPools.getJSONObject("maximumFluidY").getInt("maximum")); - assertEquals(128, deepPools.getJSONObject("horizontalRadius").getInt("maximum")); - assertEquals(64, deepPools.getJSONObject("verticalRadius").getInt("maximum")); - assertEquals(0.75D, deepPools.getJSONObject("shapeVariation").getDouble("maximum"), 0D); - assertEquals(64D, deepPools.getJSONObject("warpStrength").getDouble("maximum"), 0D); - assertTrue(deepPools.has("reach")); - assertTrue(deepPools.has("shapeStyle")); - assertTrue(deepPools.has("warpStyle")); - assertTrue(deepPools.has("fluidPalette")); - } - - @Test - public void overrideSchemaKeepsEveryFieldOptionalAndTyped() { - JSONObject schema = new SchemaBuilder(IrisRiverOverride.class, schemaData()).construct(); - JSONObject properties = schema.getJSONObject("properties"); - - assertTrue(!schema.has("required") || schema.getJSONArray("required").length() == 0); - assertEquals("boolean", properties.getJSONObject("allowSources").getString("type")); - assertEquals("number", properties.getJSONObject("routingCostMultiplier").getString("type")); - assertEquals("array", properties.getJSONObject("channelBiomes").getString("type")); - assertEquals("#/definitions/erzbiomes", - properties.getJSONObject("floodedCaveBiomes").getJSONObject("items").getString("$ref")); - } - - @SuppressWarnings("unchecked") - private static IrisData schemaData() { - IrisData data = mock(IrisData.class); - ResourceLoader biomeLoader = mock(ResourceLoader.class); - ResourceLoader expressionLoader = mock(ResourceLoader.class); - ResourceLoader blockLoader = mock(ResourceLoader.class); - KMap, ResourceLoader> loaders = new KMap<>(); - loaders.put(IrisBiome.class, biomeLoader); - loaders.put(IrisExpression.class, expressionLoader); - when(data.getBlockLoader()).thenReturn(blockLoader); - when(data.getLoaders()).thenReturn(loaders); - when(data.getPossibleSnippets(anyString())).thenReturn(new KList<>()); - when(biomeLoader.getPossibleKeys()).thenReturn(new String[]{"river/channel"}); - when(biomeLoader.getFolderName()).thenReturn("biomes"); - when(biomeLoader.getResourceTypeName()).thenReturn("Biome"); - when(blockLoader.getPossibleKeys()).thenReturn(new String[0]); - when(expressionLoader.getPossibleKeys()).thenReturn(new String[0]); - when(expressionLoader.getFolderName()).thenReturn("expressions"); - when(expressionLoader.getResourceTypeName()).thenReturn("Expression"); - return data; - } - - private static JSONObject referencedProperties(JSONObject definitions, JSONObject reference) { - return referencedDefinition(definitions, reference).getJSONObject("properties"); - } - - private static JSONObject referencedDefinition(JSONObject definitions, JSONObject reference) { - String key = reference.getString("$ref").substring("#/definitions/".length()); - return definitions.getJSONObject(key); - } - - private static boolean arrayContains(JSONArray values, String expected) { - for (int index = 0; index < values.length(); index++) { - if (expected.equals(values.getString(index))) { - return true; - } - } - return false; - } - - private static List enumValues(JSONObject definitions, JSONObject reference) { - String key = reference.getString("$ref").substring("#/definitions/".length()); - JSONArray values = definitions.getJSONObject(key).getJSONArray("oneOf"); - List names = new ArrayList<>(values.length()); - for (int index = 0; index < values.length(); index++) { - names.add(values.getJSONObject(index).getString("const")); - } - return names; - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexGeneratorOrderingTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexGeneratorOrderingTest.java new file mode 100644 index 000000000..bc16f376f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexGeneratorOrderingTest.java @@ -0,0 +1,148 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.object.IrisGenerator; +import art.arcane.iris.engine.object.IrisInterpolator; +import art.arcane.iris.util.project.interpolation.InterpolationMethod; +import org.junit.Test; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import static org.junit.Assert.assertEquals; + +public class IrisComplexGeneratorOrderingTest { + private static final int[][] PERMUTATIONS = { + {0, 1, 2, 3}, + {3, 2, 1, 0}, + {1, 3, 0, 2}, + {2, 0, 3, 1} + }; + + @Test + public void frozenGroupsUseStableOrderAcrossInsertionPermutations() { + IrisInterpolator bilinearWide = interpolator(InterpolationMethod.BILINEAR, 16D); + IrisInterpolator starcast = interpolator(InterpolationMethod.STARCAST_3, 8D); + IrisInterpolator bilinearNarrow = interpolator(InterpolationMethod.BILINEAR, 4D); + List generators = List.of( + generator("zeta", starcast, 1L, 0D), + generator("beta", bilinearWide, 2L, 0D), + generator("alpha", bilinearWide, 3L, 0D), + generator("delta", bilinearNarrow, 4L, 0D) + ); + + String expected = "BILINEAR@4.0[delta]|BILINEAR@16.0[alpha,beta]|STARCAST_3@8.0[zeta]"; + for (int[] permutation : PERMUTATIONS) { + IrisComplex.GeneratorGroup[] groups = IrisComplex.freezeGeneratorGroups( + groupByInterpolator(generators, permutation) + ); + assertEquals(expected, signature(groups)); + } + } + + @Test + public void frozenGeneratorOrderProducesBitIdenticalOutputAcrossInsertionPermutations() { + IrisInterpolator interpolator = interpolator(InterpolationMethod.BILINEAR_STARCAST_6, 7D); + List generators = List.of( + generator("alpha", interpolator, 1L, 1.0E16D), + generator("beta", interpolator, 2L, -1.0E16D), + generator("gamma", interpolator, 3L, 1D), + generator("omega", interpolator, 4L, 0D) + ); + long expectedBits = Double.doubleToRawLongBits(0.25D); + + for (int[] permutation : PERMUTATIONS) { + IrisComplex.GeneratorGroup[] groups = IrisComplex.freezeGeneratorGroups( + groupByInterpolator(generators, permutation) + ); + double output = 0D; + for (IrisComplex.GeneratorGroup group : groups) { + output += IrisComplex.averageGeneratorHeights( + group.generators(), + 0D, + 1D, + 0D, + 0D, + 0L + ); + } + assertEquals(expectedBits, Double.doubleToRawLongBits(output)); + } + } + + @Test + public void emptyGeneratorCollectionFreezesAndAccumulatesAsEmpty() { + Map> generators = Map.of(); + + assertEquals(0, IrisComplex.freezeGeneratorGroups(generators).length); + assertEquals(0D, IrisComplex.averageGeneratorHeights( + new IrisGenerator[0], + -1D, + 1D, + 0D, + 0D, + 0L + ), 0D); + } + + private IrisInterpolator interpolator(InterpolationMethod function, double horizontalScale) { + return new IrisInterpolator() + .setFunction(function) + .setHorizontalScale(horizontalScale); + } + + private FixedGenerator generator( + String loadKey, + IrisInterpolator interpolator, + long seed, + double height + ) { + FixedGenerator generator = new FixedGenerator(height); + generator.setLoadKey(loadKey); + generator.setInterpolator(interpolator); + generator.setSeed(seed); + return generator; + } + + private Map> groupByInterpolator( + List generators, + int[] permutation + ) { + Map> groups = new LinkedHashMap<>(); + for (int index : permutation) { + IrisGenerator generator = generators.get(index); + groups.computeIfAbsent(generator.getInterpolator(), ignored -> new LinkedHashSet<>()).add(generator); + } + return groups; + } + + private String signature(IrisComplex.GeneratorGroup[] groups) { + StringJoiner groupSignature = new StringJoiner("|"); + for (IrisComplex.GeneratorGroup group : groups) { + StringJoiner generatorKeys = new StringJoiner(","); + for (IrisGenerator generator : group.generators()) { + generatorKeys.add(generator.getLoadKey()); + } + groupSignature.add(group.interpolator().getFunction().name() + + "@" + group.interpolator().getHorizontalScale() + + "[" + generatorKeys + "]"); + } + return groupSignature.toString(); + } + + private static final class FixedGenerator extends IrisGenerator { + private final double height; + + private FixedGenerator(double height) { + this.height = height; + } + + @Override + public double getHeight(double x, double z, long seed) { + return height; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java index a97aeda8b..46f6cd2de 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexGridBoundsCacheTest.java @@ -61,6 +61,51 @@ public class IrisComplexGridBoundsCacheTest { assertEquals(1, interpolator.getInvocations()); } + @Test + public void horizontalGridAxisMatchesLegacyBilerpBitForBit() throws Exception { + IrisComplex complex = createComplex(); + Method gridSampleBounds = gridSampleBoundsMethod(); + CoordinateInterpolator interpolator = new CoordinateInterpolator(); + double x = 67D; + double z = -32D; + + NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBounds, interpolator, x, z); + NoiseBounds expected = legacyGridSampleBounds(x, z); + + assertBoundsBitsEqual(expected, actual); + assertEquals(2, interpolator.getInvocations()); + } + + @Test + public void verticalGridAxisMatchesLegacyBilerpBitForBitAtNegativeCoordinates() throws Exception { + IrisComplex complex = createComplex(); + Method gridSampleBounds = gridSampleBoundsMethod(); + CoordinateInterpolator interpolator = new CoordinateInterpolator(); + double x = -32D; + double z = -29D; + + NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBounds, interpolator, x, z); + NoiseBounds expected = legacyGridSampleBounds(x, z); + + assertBoundsBitsEqual(expected, actual); + assertEquals(2, interpolator.getInvocations()); + } + + @Test + public void interiorGridSampleMatchesLegacyBilerpBitForBit() throws Exception { + IrisComplex complex = createComplex(); + Method gridSampleBounds = gridSampleBoundsMethod(); + CoordinateInterpolator interpolator = new CoordinateInterpolator(); + double x = -29D; + double z = 67D; + + NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBounds, interpolator, x, z); + NoiseBounds expected = legacyGridSampleBounds(x, z); + + assertBoundsBitsEqual(expected, actual); + assertEquals(4, interpolator.getInvocations()); + } + private IrisComplex createComplex() throws Exception { IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS); @@ -133,6 +178,50 @@ public class IrisComplexGridBoundsCacheTest { return (NoiseBounds) method.invoke(complex, null, interpolator, 0, new IrisGenerator[0], x, z); } + private NoiseBounds legacyGridSampleBounds(double x, double z) { + int grid = 4; + int xi = (int) Math.floor(x); + int zi = (int) Math.floor(z); + int mask = grid - 1; + int gx = xi & ~mask; + int gz = zi & ~mask; + double fx = (x - gx) / grid; + double fz = (z - gz) / grid; + NoiseBounds b00 = packedCoordinateBounds(gx, gz); + NoiseBounds b10 = packedCoordinateBounds(gx + grid, gz); + NoiseBounds b01 = packedCoordinateBounds(gx, gz + grid); + NoiseBounds b11 = packedCoordinateBounds(gx + grid, gz + grid); + return new NoiseBounds( + legacyBiLerp(b00.min(), b10.min(), b01.min(), b11.min(), fx, fz), + legacyBiLerp(b00.max(), b10.max(), b01.max(), b11.max(), fx, fz) + ); + } + + private NoiseBounds packedCoordinateBounds(int x, int z) { + return new NoiseBounds( + (float) CoordinateInterpolator.low(x, z), + (float) CoordinateInterpolator.high(x, z) + ); + } + + private double legacyBiLerp( + double v00, + double v10, + double v01, + double v11, + double fx, + double fz + ) { + double a = v00 + ((v10 - v00) * fx); + double b = v01 + ((v11 - v01) * fx); + return a + ((b - a) * fz); + } + + private void assertBoundsBitsEqual(NoiseBounds expected, NoiseBounds actual) { + assertEquals(Double.doubleToRawLongBits(expected.min()), Double.doubleToRawLongBits(actual.min())); + assertEquals(Double.doubleToRawLongBits(expected.max()), Double.doubleToRawLongBits(actual.max())); + } + private float unpackLow(long packed) { return Float.intBitsToFloat((int) (packed >>> 32)); } @@ -159,4 +248,26 @@ public class IrisComplexGridBoundsCacheTest { return invocations.get(); } } + + private static final class CoordinateInterpolator extends IrisInterpolator { + private final AtomicInteger invocations = new AtomicInteger(); + + @Override + public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) { + invocations.incrementAndGet(); + return new NoiseBounds(low(x, z), high(x, z)); + } + + private static double low(double x, double z) { + return x * 0.125D - z * 0.0625D - 7.75D; + } + + private static double high(double x, double z) { + return x * -0.03125D + z * 0.1875D + 12.5D; + } + + private int getInvocations() { + return invocations.get(); + } + } } diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexInferredBiomeStreamCacheTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexInferredBiomeStreamCacheTest.java new file mode 100644 index 000000000..a8fc613c7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexInferredBiomeStreamCacheTest.java @@ -0,0 +1,139 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.object.InferredType; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.interpolation.Interpolated; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class IrisComplexInferredBiomeStreamCacheTest { + @Test + public void compilesOncePerRegionIdentityAndPreservesSampling() { + IrisRegion first = new IrisRegion(); + IrisRegion second = new IrisRegion(); + AtomicInteger compilations = new AtomicInteger(); + IdentityHashMap> expected = new IdentityHashMap<>(); + List compilationOrder = new ArrayList<>(); + + Map>> streams = + IrisComplex.compileInferredBiomeStreams( + List.of(first, first, second), + (region, inferredType) -> { + compilations.incrementAndGet(); + compilationOrder.add(inferredType); + IrisBiome biome = new IrisBiome().setInferredType(inferredType); + expected.computeIfAbsent(region, ignored -> new EnumMap<>(InferredType.class)) + .put(inferredType, biome); + return constant(biome); + } + ); + + assertEquals(8, compilations.get()); + assertEquals(List.of( + InferredType.LAND, + InferredType.CAVE, + InferredType.SEA, + InferredType.SHORE, + InferredType.LAND, + InferredType.CAVE, + InferredType.SEA, + InferredType.SHORE + ), compilationOrder); + assertEquals(2, streams.size()); + assertNotSame(streams.get(first), streams.get(second)); + for (InferredType inferredType : InferredType.values()) { + ProceduralStream firstStream = IrisComplex.preparedInferredBiomeStream( + streams, first, inferredType); + assertSame(firstStream, IrisComplex.preparedInferredBiomeStream(streams, first, inferredType)); + assertSame(expected.get(first).get(inferredType), firstStream.get(13D, 17D)); + assertSame( + expected.get(second).get(inferredType), + IrisComplex.preparedInferredBiomeStream(streams, second, inferredType).get(13D, 17D) + ); + } + assertThrows(UnsupportedOperationException.class, streams::clear); + assertThrows( + UnsupportedOperationException.class, + () -> streams.get(first).put(InferredType.LAND, constant(new IrisBiome())) + ); + } + + @Test + public void rejectsOnlyUnpreparedRegionIdentities() { + IrisRegion prepared = new IrisRegion(); + IrisRegion unprepared = new IrisRegion(); + unprepared.setLoadKey("missing"); + Map>> streams = + IrisComplex.compileInferredBiomeStreams( + List.of(prepared), + (region, inferredType) -> constant(new IrisBiome().setInferredType(inferredType)) + ); + + IllegalStateException error = assertThrows( + IllegalStateException.class, + () -> IrisComplex.preparedInferredBiomeStream(streams, unprepared, InferredType.LAND) + ); + + assertTrue(error.getMessage().contains("missing")); + } + + @Test + public void immutableCacheSupportsConcurrentReads() throws Exception { + IrisRegion region = new IrisRegion(); + IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND); + Map>> streams = + IrisComplex.compileInferredBiomeStreams( + List.of(region), + (preparedRegion, inferredType) -> constant( + inferredType == InferredType.LAND + ? biome + : new IrisBiome().setInferredType(inferredType) + ) + ); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> reads = new ArrayList<>(); + for (int task = 0; task < 32; task++) { + reads.add(executor.submit(() -> { + IrisBiome sampled = null; + for (int read = 0; read < 1_000; read++) { + sampled = IrisComplex.preparedInferredBiomeStream( + streams, region, InferredType.LAND).get(read, -read); + } + return sampled; + })); + } + for (Future read : reads) { + assertSame(biome, read.get(5, TimeUnit.SECONDS)); + } + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + + private static ProceduralStream constant(IrisBiome biome) { + return ProceduralStream.of( + (x, z) -> biome, + Interpolated.of(value -> 0D, value -> biome) + ); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexSurfaceBiomeTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexSurfaceBiomeTest.java index 677157c2f..5142dd313 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisComplexSurfaceBiomeTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexSurfaceBiomeTest.java @@ -1,162 +1,16 @@ package art.arcane.iris.engine; import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverOverride; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; import art.arcane.iris.util.project.stream.ProceduralStream; -import art.arcane.iris.util.project.stream.interpolation.Interpolated; import org.junit.Test; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; public class IrisComplexSurfaceBiomeTest { - @Test - public void focusBiomeWithoutOverrideIgnoresOverridesOutsideFocus() { - IrisBiome focusBiome = new IrisBiome(); - IrisBiome unreachableBiome = new IrisBiome().setRiverOverride(new IrisRiverOverride()); - - assertFalse(IrisComplex.biomeRiverOverridesPossible(focusBiome, List.of(unreachableBiome))); - } - - @Test - public void focusBiomeOverrideAlwaysEnablesBiomeSampling() { - IrisBiome focusBiome = new IrisBiome().setRiverOverride(new IrisRiverOverride()); - - assertTrue(IrisComplex.biomeRiverOverridesPossible(focusBiome, List.of())); - } - - @Test - public void nonFocusBiomeSamplingTracksReachableOverrides() { - assertFalse(IrisComplex.biomeRiverOverridesPossible(null, List.of(new IrisBiome()))); - assertTrue(IrisComplex.biomeRiverOverridesPossible( - null, - List.of(new IrisBiome().setRiverOverride(new IrisRiverOverride())) - )); - } - - @Test - public void maxIncisionCapabilityIgnoresIdentityOverrides() { - assertFalse(IrisComplex.changesMaxIncision(null)); - assertFalse(IrisComplex.changesMaxIncision(new IrisRiverOverride())); - assertFalse(IrisComplex.changesMaxIncision( - new IrisRiverOverride().setMaxIncisionMultiplier(1D))); - assertTrue(IrisComplex.changesMaxIncision( - new IrisRiverOverride().setMaxIncisionMultiplier(0.75D))); - } - - @Test - public void naturalOceanMaskTracksContinentalIntent() { - assertTrue(IrisComplex.createNaturalOceanStream( - constantType(InferredType.SEA), - null - ).get(0D, 0D)); - assertFalse(IrisComplex.createNaturalOceanStream( - constantType(InferredType.LAND), - null - ).get(0D, 0D)); - } - - @Test - public void naturalOceanMaskDoesNotSampleNaturalHeight() { - AtomicInteger heightSamples = new AtomicInteger(); - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> { - heightSamples.incrementAndGet(); - return 62D; - }); - - Boolean ocean = IrisComplex.createNaturalOceanStream( - constantType(InferredType.LAND), - null - ).get(8D, -3D); - - assertFalse(ocean); - assertEquals(0, heightSamples.get()); - } - - @Test - public void focusNaturalOceanMaskIgnoresHeightForEverySurfaceType() { - AtomicInteger heightSamples = new AtomicInteger(); - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> { - heightSamples.incrementAndGet(); - return -1_000D; - }); - - assertTrue(IrisComplex.createNaturalOceanStream( - constantType(InferredType.LAND), - new IrisBiome().setInferredType(InferredType.SEA) - ).get(0D, 0D)); - assertFalse(IrisComplex.createNaturalOceanStream( - constantType(InferredType.SEA), - new IrisBiome().setInferredType(InferredType.LAND) - ).get(0D, 0D)); - assertFalse(IrisComplex.createNaturalOceanStream( - constantType(InferredType.SEA), - new IrisBiome().setInferredType(InferredType.SHORE) - ).get(0D, 0D)); - assertEquals(0, heightSamples.get()); - } - - @Test - public void fixedOceanMaskUsesContinentalIntentWithoutSamplingHeight() { - AtomicInteger heightSamples = new AtomicInteger(); - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> { - heightSamples.incrementAndGet(); - return -1_000D; - }); - - Boolean ocean = IrisComplex.createNaturalOceanStream( - constantType(InferredType.SEA), - null - ).get(8D, -3D); - - assertTrue(ocean); - assertEquals(0, heightSamples.get()); - } - - @Test - public void emptyRiverPoolsKeepWetChannelsAquaticAndDryChannelsLand() { - assertEquals(InferredType.SEA, IrisComplex.directRiverFallback(sample(RiverRouteState.WET, RiverSection.CHANNEL))); - assertEquals(InferredType.SEA, IrisComplex.directRiverFallback(sample(RiverRouteState.WET, RiverSection.MOUTH))); - assertEquals(InferredType.LAND, IrisComplex.directRiverFallback(sample(RiverRouteState.DRY, RiverSection.DRY_BANK))); - assertNull(IrisComplex.directRiverFallback(sample(RiverRouteState.WET, RiverSection.BANK))); - } - - @Test - public void elevatedRiverBankUsesTheLocalWaterHead() { - IrisBiome base = mock(IrisBiome.class); - IrisBiome sea = mock(IrisBiome.class); - IrisRegion region = mock(IrisRegion.class); - doReturn(false).when(base).isShore(); - doReturn(false).when(base).isAquatic(); - doReturn(3D).when(region).getShoreHeight(12D, 18D); - - IrisBiome resolved = IrisComplex.resolveSurfaceBiome( - 68D, - base, - region, - 12D, - 18D, - 70D, - constant(base), - constant(sea), - constant(base)); - - assertSame(sea, resolved); - } @Test public void shorelineHeightSelectsShoreBiome() { IrisBiome base = mock(IrisBiome.class); @@ -208,15 +62,4 @@ public class IrisComplexSurfaceBiomeTest { doReturn(biome).when(stream).get(anyDouble(), anyDouble()); return stream; } - - private static ProceduralStream constantType(InferredType type) { - return ProceduralStream.of( - (x, z) -> type, - Interpolated.of(value -> 0D, value -> type) - ); - } - - private static RiverSample sample(RiverRouteState state, RiverSection section) { - return new RiverSample(true, state, section, 0D, 0.5D, 1D, 1, 1, 10D, 5D, 3D, false, null); - } } diff --git a/core/src/test/java/art/arcane/iris/engine/actuator/IrisDecorantActuatorRiverTest.java b/core/src/test/java/art/arcane/iris/engine/actuator/IrisDecorantActuatorRiverTest.java deleted file mode 100644 index 38872700c..000000000 --- a/core/src/test/java/art/arcane/iris/engine/actuator/IrisDecorantActuatorRiverTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package art.arcane.iris.engine.actuator; - -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class IrisDecorantActuatorRiverTest { - @Test - public void dryRiverSurfaceDoesNotRunShorelineDecoration() { - IrisRiverSurfaceSample dry = new IrisRiverSurfaceSample( - river(RiverRouteState.DRY, RiverSection.DRY_CHANNEL), - 70D, - 65D, - 65D, - false, - false - ); - IrisRiverSurfaceSample wet = new IrisRiverSurfaceSample( - river(RiverRouteState.WET, RiverSection.CHANNEL), - 70D, - 65D, - 65D, - false, - true - ); - - assertFalse(IrisDecorantActuator.shouldDecorateShoreline(dry, 65)); - assertTrue(IrisDecorantActuator.shouldDecorateShoreline(wet, 65)); - assertTrue(IrisDecorantActuator.shouldDecorateShoreline(IrisRiverSurfaceSample.none(65D, 65D), 65)); - } - - private static RiverSample river(RiverRouteState state, RiverSection section) { - return new RiverSample(true, state, section, 0D, 0.5D, 1D, 1, 1, 10D, 5D, 3D, false, null); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java index 897360900..bd2740099 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisDecoratorCaveContextTest.java @@ -1,7 +1,6 @@ package art.arcane.iris.engine.decorator; import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.SeedManager; import art.arcane.iris.engine.object.InferredType; @@ -11,7 +10,6 @@ import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.hunk.Hunk; -import art.arcane.iris.util.project.stream.ProceduralStream; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -33,11 +31,6 @@ public class IrisDecoratorCaveContextTest { IrisDimension dimension = mock(IrisDimension.class); doReturn(63).when(dimension).getFluidHeight(); doReturn(dimension).when(engine).getDimension(); - IrisComplex complex = mock(IrisComplex.class); - ProceduralStream fluidStream = mock(ProceduralStream.class); - doReturn(complex).when(engine).getComplex(); - doReturn(fluidStream).when(complex).getRiverWaterSurfaceStream(); - doReturn(63D).when(fluidStream).get(anyDouble(), anyDouble()); IrisDecorator decorator = mock(IrisDecorator.class); doReturn(true).when(decorator).passesChanceGate(any(), anyDouble(), anyDouble(), any()); diff --git a/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java b/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java index 101b6eee8..578f2eafb 100644 --- a/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java +++ b/core/src/test/java/art/arcane/iris/engine/decorator/IrisShoreLineDecoratorTest.java @@ -98,7 +98,6 @@ public class IrisShoreLineDecoratorTest { IrisSlopeClip slope = mock(IrisSlopeClip.class); PlatformBlockState decorant = mock(PlatformBlockState.class); ProceduralStream heightStream = mock(ProceduralStream.class); - ProceduralStream fluidStream = mock(ProceduralStream.class); when(engine.getCacheID()).thenReturn(1); when(engine.getSeedManager()).thenReturn(seedManager); @@ -108,9 +107,7 @@ public class IrisShoreLineDecoratorTest { when(engine.getComplex()).thenReturn(complex); when(complex.getFluidHeight()).thenReturn((double) FLUID_HEIGHT); when(complex.getHeightStream()).thenReturn(heightStream); - when(complex.getRiverWaterSurfaceStream()).thenReturn(fluidStream); when(heightStream.get(anyDouble(), anyDouble())).thenReturn((double) FLUID_HEIGHT - 1); - when(fluidStream.get(anyDouble(), anyDouble())).thenReturn((double) FLUID_HEIGHT); when(engine.getData()).thenReturn(data); when(biome.getDecoratorBucket(IrisDecorationPart.SHORE_LINE)) .thenReturn(new IrisDecorator[]{decorator}); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java b/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java index 9ca27f964..a07d078ab 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/IrisStructureLocatorContractTest.java @@ -417,7 +417,7 @@ public class IrisStructureLocatorContractTest { assertEquals(IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED, result.status()); assertFalse(result.found()); - verify(engine, times(8_192)).getComplex(); + verify(engine, times(4_096)).getComplex(); } @Test diff --git a/core/src/test/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlannerTest.java b/core/src/test/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlannerTest.java index 0b0c617f7..45a201202 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/NativeStructurePlacementPlannerTest.java @@ -1,6 +1,5 @@ package art.arcane.iris.engine.framework; -import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.object.IrisNativeStructure; import art.arcane.iris.engine.object.IrisNativeStructureDecision; import art.arcane.iris.engine.object.IrisDimension; @@ -10,7 +9,6 @@ import art.arcane.iris.engine.object.IrisStructureTerrain; import art.arcane.iris.engine.object.IrisStructureTerrainMode; import art.arcane.iris.engine.object.NativeStructureGenerationStatus; import art.arcane.iris.engine.object.StructureDistribution; -import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import org.junit.Test; @@ -22,7 +20,6 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class NativeStructurePlacementPlannerTest { @@ -115,20 +112,6 @@ public class NativeStructurePlacementPlannerTest { assertNotNull(NativeStructurePlacementPlanner.planAt(engine, placement, 0, 0)); } - @Test - public void submergedCheckUsesTheColumnRiverHead() { - Engine engine = engine(77L, -64, 384, 66); - IrisComplex complex = mock(IrisComplex.class); - @SuppressWarnings("unchecked") - ProceduralStream riverHead = mock(ProceduralStream.class); - when(engine.getComplex()).thenReturn(complex); - when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead); - when(riverHead.get(8, 8)).thenReturn(68D); - - assertTrue(NativeStructurePlacementPlanner.isSubmerged(engine, 8, 8)); - verify(riverHead).get(8, 8); - } - @Test public void duplicateSelectedStructureUsesOneDeterministicPlan() { Engine engine = engine(77L, -64, 384, 150); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolverTest.java b/core/src/test/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolverTest.java index 013b440af..7e0456a31 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolverTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/StructureCaveAnchorResolverTest.java @@ -3,16 +3,8 @@ package art.arcane.iris.engine.framework; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisStructureAnchorMode; import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.spi.IrisPlatform; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.spi.PlatformRegistries; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; -import org.junit.After; -import org.junit.Before; import org.junit.Test; import java.util.Arrays; @@ -27,7 +19,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; @@ -36,22 +27,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class StructureCaveAnchorResolverTest { - @Before - public void bindPlatform() { - IrisPlatforms.unbind(); - PlatformBlockState block = mock(PlatformBlockState.class); - PlatformRegistries registries = mock(PlatformRegistries.class); - when(registries.block(anyString())).thenReturn(block); - IrisPlatform platform = mock(IrisPlatform.class); - when(platform.registries()).thenReturn(registries); - IrisPlatforms.bind(platform); - } - - @After - public void unbindPlatform() { - IrisPlatforms.unbind(); - } - @Test public void floorRequiresSolidBoundaryAndUpwardClearance() { IntPredicate carved = carvedAt(10, 11, 12, 13); @@ -180,21 +155,6 @@ public class StructureCaveAnchorResolverTest { true, new MatterCavern(false, "", (byte) 0), 20, 8)); } - @Test - public void wetAndSealHydrologyCannotBecomeStructureAnchors() { - MatterCavern fluid = new MatterCavern(true, "", (byte) 1); - MatterCavern forcedAir = new MatterCavern(true, "", (byte) 3); - - assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid( - true, fluid, RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), 20, 8)); - assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid( - true, fluid, RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), 20, 8)); - assertFalse(StructureCaveAnchorResolver.acceptsAnchorFluid( - true, null, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), 20, 8)); - assertTrue(StructureCaveAnchorResolver.acceptsAnchorFluid( - false, forcedAir, RiverCaveHydrology.of(RiverCaveAction.DRY_AIR), 0, 8)); - } - private static Engine engineWithFloorAnchor(MatterCavern anchorCavern, int surfaceHeight) { Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); IrisDimension dimension = mock(IrisDimension.class); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementScopeTest.java b/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementScopeTest.java index 20b9785de..f23863fd8 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementScopeTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/StructurePlacementScopeTest.java @@ -66,7 +66,7 @@ public class StructurePlacementScopeTest { } @Test - public void dimensionOnlyPlacementsDoNotResolveRiverInclusiveBiomeScopes() { + public void dimensionOnlyPlacementsDoNotResolveBiomeScopes() { IrisStructurePlacement dimensionPlacement = new IrisStructurePlacement(); Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); IrisComplex complex = engine.getComplex(); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererPerformanceContractTest.java b/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererPerformanceContractTest.java index 39a4bac1f..c1f8e4729 100644 --- a/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererPerformanceContractTest.java +++ b/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererPerformanceContractTest.java @@ -71,33 +71,25 @@ public class IrisRendererPerformanceContractTest { } @Test - public void studioHeightUsesNaturalTerrainWhileProtocolRenderRemainsExact() { + public void studioAndProtocolHeightUseOrdinaryTerrain() { Engine engine = mock(Engine.class); IrisComplex complex = mock(IrisComplex.class); @SuppressWarnings("unchecked") - ProceduralStream natural = mock(ProceduralStream.class); - @SuppressWarnings("unchecked") ProceduralStream exact = mock(ProceduralStream.class); when(engine.getComplex()).thenReturn(complex); when(engine.getHeight()).thenReturn(320); - when(complex.getNaturalHeightStream()).thenReturn(natural); when(complex.getHeightStream()).thenReturn(exact); - when(natural.getDouble(0D, 0D)).thenReturn(80D); when(exact.getDouble(0D, 0D)).thenReturn(96D); IrisRenderer renderer = new IrisRenderer(engine); renderer.renderStudio(0D, 0D, 1D, 1, RenderType.HEIGHT, () -> false); - - verify(natural).getDouble(0D, 0D); - verifyNoInteractions(exact); - renderer.render(0D, 0D, 1D, 1, RenderType.HEIGHT); - verify(exact).getDouble(0D, 0D); + verify(exact, times(2)).getDouble(0D, 0D); } @Test - public void studioBiomeAndContinentAvoidRiverAdjustedEngineLookups() { + public void studioBiomeAndContinentAvoidEngineBiomeLookups() { Engine engine = mock(Engine.class); IrisComplex complex = mock(IrisComplex.class); IrisBiome biome = mock(IrisBiome.class); @@ -144,12 +136,12 @@ public class IrisRendererPerformanceContractTest { Engine engine = mock(Engine.class); IrisComplex complex = mock(IrisComplex.class); @SuppressWarnings("unchecked") - ProceduralStream natural = mock(ProceduralStream.class); + ProceduralStream exact = mock(ProceduralStream.class); AtomicInteger samples = new AtomicInteger(); when(engine.getComplex()).thenReturn(complex); when(engine.getHeight()).thenReturn(320); - when(complex.getNaturalHeightStream()).thenReturn(natural); - when(natural.getDouble(org.mockito.ArgumentMatchers.anyDouble(), org.mockito.ArgumentMatchers.anyDouble())) + when(complex.getHeightStream()).thenReturn(exact); + when(exact.getDouble(org.mockito.ArgumentMatchers.anyDouble(), org.mockito.ArgumentMatchers.anyDouble())) .thenAnswer(invocation -> { samples.incrementAndGet(); double x = invocation.getArgument(0); diff --git a/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererRiverTest.java b/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererRiverTest.java deleted file mode 100644 index bac4ac05c..000000000 --- a/core/src/test/java/art/arcane/iris/engine/framework/render/IrisRendererRiverTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package art.arcane.iris.engine.framework.render; - -import art.arcane.iris.engine.IrisComplex; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.runtime.IrisRiverRuntime; -import art.arcane.iris.util.project.stream.ProceduralStream; -import org.junit.Test; - -import java.awt.Color; -import java.awt.image.BufferedImage; -import java.util.EnumMap; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class IrisRendererRiverTest { - @Test - public void everyRiverSectionHasItsDiagnosticColor() { - Map expected = new EnumMap<>(RiverSection.class); - expected.put(RiverSection.CHANNEL, new Color(48, 112, 190).getRGB()); - expected.put(RiverSection.MOUTH, new Color(54, 164, 205).getRGB()); - expected.put(RiverSection.BANK, new Color(92, 146, 78).getRGB()); - expected.put(RiverSection.DRY_CHANNEL, new Color(171, 128, 68).getRGB()); - expected.put(RiverSection.DRY_BANK, new Color(132, 105, 62).getRGB()); - expected.put(RiverSection.NONE, new Color(28, 31, 38).getRGB()); - - assertEquals(RiverSection.values().length, expected.size()); - for (RiverSection section : RiverSection.values()) { - assertEquals(section.name(), expected.get(section).intValue(), IrisRenderer.riverColor(section)); - } - } - - @Test - public void riverRenderTypeSamplesOneWorldFootprintPerRenderedPixel() { - Engine engine = mock(Engine.class); - IrisComplex complex = mock(IrisComplex.class); - IrisRiverRuntime runtime = mock(IrisRiverRuntime.class); - RiverSample river = mock(RiverSample.class); - - when(engine.getComplex()).thenReturn(complex); - when(complex.getRiverRuntime()).thenReturn(runtime); - when(runtime.sampleFootprint(12D, -7D, 20D, 1D)).thenReturn(river); - when(river.present()).thenReturn(true); - when(river.section()).thenReturn(RiverSection.CHANNEL); - - BufferedImage image = new IrisRenderer(engine).renderStudio( - 12D, - -7D, - 8D, - 1, - RenderType.RIVER, - () -> false - ); - - assertEquals(new Color(48, 112, 190).getRGB(), image.getRGB(0, 0)); - verify(runtime).sampleFootprint(12D, -7D, 20D, 1D); - } - - @Test - public void biomeAtlasCompositesRiverChannelsOverTheFastBaseBiome() { - Engine engine = mock(Engine.class); - IrisComplex complex = mock(IrisComplex.class); - IrisRiverRuntime runtime = mock(IrisRiverRuntime.class); - RiverSample river = mock(RiverSample.class); - IrisBiome biome = mock(IrisBiome.class); - @SuppressWarnings("unchecked") - ProceduralStream base = mock(ProceduralStream.class); - when(engine.getComplex()).thenReturn(complex); - when(complex.getBaseBiomeStream()).thenReturn(base); - when(complex.getRiverRuntime()).thenReturn(runtime); - when(base.get(0D, 0D)).thenReturn(biome); - when(biome.getColor(engine, RenderType.BIOME)).thenReturn(Color.GREEN); - when(runtime.sampleFootprint(0D, 0D, 4D, 4D)).thenReturn(river); - when(river.present()).thenReturn(true); - when(river.section()).thenReturn(RiverSection.CHANNEL); - - BufferedImage image = new IrisRenderer(engine).renderStudio( - 0D, - 0D, - 4D, - 1, - RenderType.BIOME, - () -> false - ); - - assertEquals(new Color(48, 112, 190).getRGB(), image.getRGB(0, 0)); - verify(base).get(0D, 0D); - verify(runtime).sampleFootprint(0D, 0D, 4D, 4D); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/EngineMantleCleanupTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/EngineMantleCleanupTest.java index 87f62fc4e..cc1c8090f 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/EngineMantleCleanupTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/EngineMantleCleanupTest.java @@ -2,7 +2,6 @@ package art.arcane.iris.engine.mantle; import art.arcane.iris.core.link.Identifier; import art.arcane.iris.engine.framework.TreeBlockMaterial; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; @@ -138,7 +137,6 @@ public class EngineMantleCleanupTest { verify(chunk).deleteSlices(MatterCavern.class); verify(chunk, never()).deleteSlices(TileWrapper.class); verify(chunk, never()).deleteSlices(Identifier.class); - verify(chunk, never()).deleteSlices(RiverCaveHydrology.class); InOrder order = inOrder(chunk); order.verify(chunk).raiseFlagUnchecked(eq(MantleFlag.CLEANED), any()); order.verify(chunk).release(); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterOverlayTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterOverlayTest.java index cdedfb14d..355ee624b 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterOverlayTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterOverlayTest.java @@ -1,8 +1,6 @@ package art.arcane.iris.engine.mantle; import art.arcane.iris.core.link.Identifier; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.PlatformBlockState; @@ -17,7 +15,6 @@ import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; import static org.mockito.ArgumentMatchers.anyString; @@ -33,11 +30,9 @@ public class MantleWriterOverlayTest { private MantleWriter writer; private Matter matter; - private MantleChunk chunk; private MatterSlice blockSlice; private MatterSlice identifierSlice; private MatterSlice cavernSlice; - private MatterSlice hydrologySlice; @Before @SuppressWarnings("unchecked") @@ -52,28 +47,22 @@ public class MantleWriterOverlayTest { EngineMantle engineMantle = mock(EngineMantle.class); Mantle mantle = mock(Mantle.class); - chunk = mock(MantleChunk.class); + MantleChunk chunk = mock(MantleChunk.class); matter = mock(Matter.class); blockSlice = mock(MatterSlice.class); identifierSlice = mock(MatterSlice.class); cavernSlice = mock(MatterSlice.class); - hydrologySlice = mock(MatterSlice.class); when(mantle.getWorldHeight()).thenReturn(64); when(mantle.getChunk(0, 0)).thenReturn(chunk); when(chunk.use()).thenReturn(chunk); when(chunk.getOrCreate(0)).thenReturn(matter); - when(chunk.exists(0)).thenReturn(true); - when(chunk.get(0)).thenReturn(matter); when(matter.hasSlice(PlatformBlockState.class)).thenReturn(true); when(matter.hasSlice(Identifier.class)).thenReturn(true); - when(matter.hasSlice(MatterCavern.class)).thenReturn(true); when(matter.getSlice(PlatformBlockState.class)).thenReturn(blockSlice); when(matter.getSlice(Identifier.class)).thenReturn(identifierSlice); - when(matter.getSlice(MatterCavern.class)).thenReturn(cavernSlice); doReturn(blockSlice).when(matter).slice(PlatformBlockState.class); doReturn(cavernSlice).when(matter).slice(MatterCavern.class); - doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class); writer = new MantleWriter(engineMantle, mantle, 0, 0, 0, false); } @@ -116,41 +105,4 @@ public class MantleWriterOverlayTest { verify(identifierSlice).set(X, Y, Z, null); verify(blockSlice).set(X, Y, Z, replacement); } - - @Test - public void protectedHydrologyRejectsLaterBlockAndCavernWrites() { - PlatformBlockState replacement = mock(PlatformBlockState.class); - MatterCavern cavern = new MatterCavern(true, "", (byte) 3); - when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true); - when(hydrologySlice.get(X, Y, Z)) - .thenReturn(RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD)); - - writer.setData(X, Y, Z, replacement); - assertFalse(writer.carveDataIfAbsent(X, Y, Z, cavern)); - writer.setForcedCarve(X, Y, Z, cavern); - - verify(blockSlice, never()).set(X, Y, Z, replacement); - verify(blockSlice, never()).set(X, Y, Z, null); - verify(identifierSlice, never()).set(X, Y, Z, null); - verify(cavernSlice, never()).set(X, Y, Z, cavern); - } - - @Test - public void hydrologyOverridesBaselineCarvedQueriesWithoutChangingIt() { - MatterCavern baseline = new MatterCavern(true, "", (byte) 0); - when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true); - when(cavernSlice.get(X, Y, Z)).thenReturn(baseline); - when(hydrologySlice.get(X, Y, Z)) - .thenReturn(RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD)); - when(hydrologySlice.get(X, Y + 1, Z)) - .thenReturn(RiverCaveHydrology.of(RiverCaveAction.DRY_AIR)); - - assertFalse(writer.isCarved(X, Y, Z)); - assertTrue(writer.isCarved(X, Y + 1, Z)); - - byte[] column = writer.getCarvedColumn(X, Z, Y + 2); - assertEquals(0, column[Y]); - assertEquals(1, column[Y + 1]); - assertEquals(0, baseline.getLiquid()); - } } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorCarvePassRadiusTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorCarvePassRadiusTest.java index e7482314e..5d7168b28 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorCarvePassRadiusTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorCarvePassRadiusTest.java @@ -74,7 +74,7 @@ public class MatterGeneratorCarvePassRadiusTest { RecordingComponent carving = new RecordingComponent(ReservedFlag.CARVED, 0, 1); RecordingComponent conditional = new RecordingComponent( - ReservedFlag.RIVER_HYDROLOGY, + ReservedFlag.JIGSAW, 1, 0, 160, @@ -111,7 +111,7 @@ public class MatterGeneratorCarvePassRadiusTest { RecordingComponent carving = new RecordingComponent(ReservedFlag.CARVED, 0, 0); RecordingComponent conditional = new RecordingComponent( - ReservedFlag.RIVER_HYDROLOGY, + ReservedFlag.JIGSAW, 1, 0, 0, diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransactionTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransactionTest.java index 5b9ba3af1..6766dbe8d 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransactionTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/CaveObjectPlacementTransactionTest.java @@ -21,8 +21,6 @@ package art.arcane.iris.engine.mantle.components; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IObjectPlacer; import art.arcane.iris.engine.object.TileData; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.spi.PlatformBlockState; import org.junit.Test; @@ -111,21 +109,6 @@ public class CaveObjectPlacementTransactionTest { assertEquals(61, MantleObjectComponent.caveAnchorScanUpperBound(128, 80, 20)); } - @Test - public void protectedHydrologyRejectsTheWholePlacement() { - IObjectPlacer delegate = createPlacer(128, 80, 20, 90); - when(delegate.getData(4, 30, 7, RiverCaveHydrology.class)) - .thenReturn(RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE)); - CaveObjectPlacementTransaction transaction = new CaveObjectPlacementTransaction(delegate, 20, 10); - - transaction.set(4, 30, 7, mock(PlatformBlockState.class)); - transaction.setData(5, 30, 7, "object@1"); - - assertEquals(CaveObjectPlacementTransaction.CommitResult.REJECTED_HYDROLOGY, transaction.commit()); - verify(delegate, never()).set(anyInt(), anyInt(), anyInt(), any()); - verify(delegate, never()).setData(anyInt(), anyInt(), anyInt(), any()); - } - private IObjectPlacer createPlacer(int worldHeight, int surfaceHeight, int caveFloor, int caveCeiling) { Engine engine = mock(Engine.class); when(engine.getHeight()).thenReturn(worldHeight); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java index 86ea66868..877e0ee51 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java @@ -868,11 +868,10 @@ public class IrisCaveCarver3DNearParityTest { Engine engine = createEngine(80, 70); int[] surfaceHeights = filledHeights(70); surfaceHeights[0] = 60; - long[] boundaries = new long[256]; - Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary( - SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE)); - boundaries[0] = SurfaceFluidBoundaryPlan.boundary(60, 64); - boundaries[16] = SurfaceFluidBoundaryPlan.boundary(61, 64); + int[] boundaryStartY = new int[256]; + Arrays.fill(boundaryStartY, SurfaceFluidBoundaryPlan.NO_BOUNDARY); + boundaryStartY[0] = 60; + boundaryStartY[16] = 61; WriterCapture capture = createWriterCapture(80); CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan(); @@ -885,7 +884,7 @@ public class IrisCaveCarver3DNearParityTest { 0D, null, surfaceHeights, - boundaries, + boundaryStartY, null, supportPlan ); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java index 938be8a38..3d8205de4 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleObjectComponentCaveAnchorTest.java @@ -1,8 +1,6 @@ package art.arcane.iris.engine.mantle.components; import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.volmlib.util.matter.MatterCavern; import org.junit.Test; @@ -48,18 +46,6 @@ public class MantleObjectComponentCaveAnchorTest { )); } - @Test - public void protectedRiverHydrologyCannotBecomeAnObjectAnchor() { - MatterCavern water = new MatterCavern(true, "", (byte) 1); - - assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid( - true, water, RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), 20, 8)); - assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid( - true, water, RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), 20, 8)); - assertFalse(MantleObjectComponent.acceptsCaveAnchorFluid( - true, null, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), 20, 8)); - } - private static IrisBiome biome(String loadKey) { IrisBiome biome = new IrisBiome(); biome.setLoadKey(loadKey); diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleRiverCaveVoxelViewTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleRiverCaveVoxelViewTest.java deleted file mode 100644 index fdd1f4af3..000000000 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleRiverCaveVoxelViewTest.java +++ /dev/null @@ -1,182 +0,0 @@ -package art.arcane.iris.engine.mantle.components; - -import art.arcane.iris.engine.river.cave.CavePosition; -import art.arcane.iris.engine.river.cave.CaveVoxel; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.volmlib.util.collection.KMap; -import art.arcane.volmlib.util.mantle.runtime.Mantle; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; -import art.arcane.volmlib.util.mantle.runtime.TectonicPlate; -import art.arcane.volmlib.util.matter.Matter; -import art.arcane.volmlib.util.matter.MatterSlice; -import art.arcane.volmlib.util.matter.MatterCavern; -import art.arcane.iris.spi.PlatformBlockState; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; - -public class MantleRiverCaveVoxelViewTest { - @Test - @SuppressWarnings("unchecked") - public void absentCellsAboveLocalTerrainAreOpenAir() { - Mantle mantle = mock(Mantle.class); - when(mantle.getLoadedRegions()).thenReturn(new KMap>()); - MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView( - mantle, - 128, - (x, z) -> x < 0 ? 20 : 60, - (x, z) -> null, - RiverCaveFluidKind.RIVER, - (chunkX, chunkZ) -> { - } - ); - CavePosition cliffAir = new CavePosition(-1, 21, 0); - CavePosition terrain = new CavePosition(-1, 20, 0); - - assertEquals(CaveVoxel.CAVE_AIR, view.voxelAt(cliffAir)); - assertTrue(view.isOpenToSurface(cliffAir)); - assertEquals(CaveVoxel.SOLID, view.voxelAt(terrain)); - assertFalse(view.isOpenToSurface(terrain)); - } - - @Test - @SuppressWarnings("unchecked") - public void carvingInputLoadsOnlyOncePerReadChunk() { - Mantle mantle = mock(Mantle.class); - when(mantle.getLoadedRegions()).thenReturn(new KMap>()); - List loaded = new ArrayList<>(); - MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView( - mantle, - 128, - (x, z) -> 60, - (x, z) -> null, - RiverCaveFluidKind.RIVER, - (chunkX, chunkZ) -> loaded.add(chunkX + "," + chunkZ) - ); - - view.voxelAt(new CavePosition(0, 20, 0)); - view.voxelAt(new CavePosition(15, 21, 15)); - view.riverHydrologyAt(new CavePosition(1, 22, 1)); - view.voxelAt(new CavePosition(16, 20, 0)); - - assertEquals(List.of("0,0", "1,0"), loaded); - } - - @Test - @SuppressWarnings("unchecked") - public void publishedRiverActionsDoNotReplaceTheUnderlyingPlanningBaseline() { - Mantle mantle = mock(Mantle.class); - TectonicPlate plate = mock(TectonicPlate.class); - MantleChunk chunk = mock(MantleChunk.class); - Matter matter = mock(Matter.class); - MatterSlice hydrologySlice = mock(MatterSlice.class); - KMap> regions = new KMap<>(); - regions.put(Mantle.key(0, 0), plate); - CavePosition position = new CavePosition(0, 20, 0); - when(mantle.getLoadedRegions()).thenReturn(regions); - when(plate.get(0, 0)).thenReturn(chunk); - when(chunk.exists(1)).thenReturn(true); - when(chunk.get(1)).thenReturn(matter); - when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true); - doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class); - when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE)); - MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView( - mantle, - 128, - (x, z) -> 60, - (x, z) -> null, - RiverCaveFluidKind.RIVER, - (chunkX, chunkZ) -> { - } - ); - - assertEquals(CaveVoxel.SOLID, view.voxelAt(position)); - assertEquals(RiverCaveAction.WET_SOURCE, view.riverHydrologyAt(position).action()); - } - - @Test - @SuppressWarnings("unchecked") - public void configuredLavaRiverTreatsPublishedLavaAsCompatibleFluid() { - Mantle mantle = mock(Mantle.class); - TectonicPlate plate = mock(TectonicPlate.class); - MantleChunk chunk = mock(MantleChunk.class); - Matter matter = mock(Matter.class); - MatterSlice cavernSlice = mock(MatterSlice.class); - PlatformBlockState lava = mock(PlatformBlockState.class); - KMap> regions = new KMap<>(); - regions.put(Mantle.key(0, 0), plate); - when(mantle.getLoadedRegions()).thenReturn(regions); - when(plate.get(0, 0)).thenReturn(chunk); - when(chunk.exists(1)).thenReturn(true); - when(chunk.get(1)).thenReturn(matter); - when(matter.hasSlice(MatterCavern.class)).thenReturn(true); - doReturn(cavernSlice).when(matter).getSlice(MatterCavern.class); - when(cavernSlice.get(0, 4, 0)).thenReturn(new MatterCavern(true, "", (byte) 2)); - when(lava.materialKey()).thenReturn("minecraft:lava"); - MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView( - mantle, - 128, - (x, z) -> 60, - (x, z) -> lava, - RiverCaveFluidKind.RIVER, - (chunkX, chunkZ) -> { - } - ); - - assertEquals(CaveVoxel.COMPATIBLE_FLUID, view.voxelAt(new CavePosition(0, 20, 0))); - } - - @Test - @SuppressWarnings("unchecked") - public void planningRejectsHydrologyOwnedByTheOtherFluidKind() { - Mantle mantle = mock(Mantle.class); - TectonicPlate plate = mock(TectonicPlate.class); - MantleChunk chunk = mock(MantleChunk.class); - Matter matter = mock(Matter.class); - MatterSlice hydrologySlice = mock(MatterSlice.class); - KMap> regions = new KMap<>(); - regions.put(Mantle.key(0, 0), plate); - CavePosition position = new CavePosition(0, 20, 0); - when(mantle.getLoadedRegions()).thenReturn(regions); - when(plate.get(0, 0)).thenReturn(chunk); - when(chunk.exists(1)).thenReturn(true); - when(chunk.get(1)).thenReturn(matter); - when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true); - doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class); - when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of( - RiverCaveAction.WET_SOURCE, - RiverCaveFluidKind.DEEP_POOL - )); - MantleRiverCaveVoxelView riverView = new MantleRiverCaveVoxelView( - mantle, - 128, - (x, z) -> 60, - (x, z) -> null, - RiverCaveFluidKind.RIVER, - (chunkX, chunkZ) -> { - } - ); - MantleRiverCaveVoxelView deepPoolView = new MantleRiverCaveVoxelView( - mantle, - 128, - (x, z) -> 60, - (x, z) -> null, - RiverCaveFluidKind.DEEP_POOL, - (chunkX, chunkZ) -> { - } - ); - - assertEquals(CaveVoxel.INCOMPATIBLE_FLUID, riverView.voxelAt(position)); - assertEquals(CaveVoxel.SOLID, deepPoolView.voxelAt(position)); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleRiverHydrologyComponentTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleRiverHydrologyComponentTest.java deleted file mode 100644 index 4846d71a4..000000000 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleRiverHydrologyComponentTest.java +++ /dev/null @@ -1,1025 +0,0 @@ -package art.arcane.iris.engine.mantle.components; - -import art.arcane.iris.engine.IrisComplex; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.mantle.EngineMantle; -import art.arcane.iris.engine.object.IrisGeneratorStyle; -import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.engine.object.IrisRiverCaveFallback; -import art.arcane.iris.engine.object.IrisRiverCaveMode; -import art.arcane.iris.engine.object.IrisRiverCaves; -import art.arcane.iris.engine.object.IrisRiverDeepPools; -import art.arcane.iris.engine.object.IrisRiverExistingFluidPolicy; -import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.iris.engine.mantle.ComponentFlag; -import art.arcane.iris.engine.river.RiverAnchor; -import art.arcane.iris.engine.river.RiverEdgeId; -import art.arcane.iris.engine.river.RiverNodeId; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverSection; -import art.arcane.iris.engine.river.RiverTopologyComplexity; -import art.arcane.iris.engine.river.cave.CavePosition; -import art.arcane.iris.engine.river.cave.CaveVoxel; -import art.arcane.iris.engine.river.cave.CaveVoxelPrecondition; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveContainmentPlanner; -import art.arcane.iris.engine.river.cave.RiverCaveFluidPolicy; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.engine.river.cave.RiverCaveMode; -import art.arcane.iris.engine.river.cave.RiverCavePlan; -import art.arcane.iris.engine.river.cave.RiverCavePlannerSettings; -import art.arcane.iris.engine.river.cave.RiverCaveRejection; -import art.arcane.iris.engine.river.cave.RiverCaveSource; -import art.arcane.iris.engine.river.runtime.IrisRiverSurfaceSample; -import art.arcane.iris.engine.river.runtime.IrisRiverTunnelSample; -import art.arcane.iris.engine.river.runtime.IrisRiverRuntime; -import art.arcane.iris.spi.IrisPlatform; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.spi.PlatformRegistries; -import art.arcane.iris.util.project.context.ChunkContext; -import org.junit.Test; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - -import art.arcane.volmlib.util.mantle.flag.ReservedFlag; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyDouble; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class MantleRiverHydrologyComponentTest { - private final RiverCaveContainmentPlanner planner = new RiverCaveContainmentPlanner(); - - @Test - public void componentRunsBetweenCarvingAndPlacementWithStableFlagOrdinal() { - ComponentFlag flag = MantleRiverHydrologyComponent.class.getAnnotation(ComponentFlag.class); - - assertEquals(1, MantleRiverHydrologyComponent.PRIORITY); - assertEquals(ReservedFlag.RIVER_HYDROLOGY, flag.value()); - assertEquals(17, ReservedFlag.FLOATING_OBJECT.ordinal()); - assertEquals(18, ReservedFlag.RIVER_HYDROLOGY.ordinal()); - } - - @Test - public void enablementSeparatesSealedBoresFromOptionalCaveConnections() { - IrisDimension dimension = new IrisDimension(); - - assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - dimension.getRivers().setEnabled(true); - assertTrue(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - assertFalse(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension)); - dimension.getRivers().getCaves().setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT); - assertTrue(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - assertTrue(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension)); - dimension.getRivers().getCaves().setMaximumPerReach(0); - assertTrue(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - assertFalse(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension)); - dimension.getRivers().getCaves().setMode(IrisRiverCaveMode.SEALED); - dimension.getRivers().getCaves().getDeepPools().setEnabled(true); - assertTrue(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension)); - dimension.getRivers().getCaves().getDeepPools().setMaximumPerReach(0); - assertFalse(MantleRiverHydrologyComponent.isCaveConnectionsEnabledFor(dimension)); - dimension.getRivers().getCaves().getDeepPools().setEnabled(false).setMaximumPerReach(1); - dimension.getRivers().getCaves().setMaximumPerReach(1); - dimension.setCarvingEnabled(false); - assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - dimension.setCarvingEnabled(true); - dimension.setUseMantle(false); - assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - dimension.setUseMantle(true); - dimension.getDisabledComponents().add(ReservedFlag.CARVED); - assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - dimension.getDisabledComponents().clear(); - dimension.getDisabledComponents().add(ReservedFlag.RIVER_HYDROLOGY); - assertFalse(MantleRiverHydrologyComponent.isEnabledFor(dimension)); - } - - @Test - public void adaptiveInputRadiusRetainsOnlyRequiredHydrologyHalos() { - bindMockPlatform(); - try { - IrisDimension dimension = new IrisDimension(); - dimension.setCarvingEnabled(true); - dimension.getRivers().setEnabled(true); - IrisRiverCaves caves = dimension.getRivers().getCaves(); - caves.setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT).setMaximumPerReach(1); - IrisRiverDeepPools deepPools = caves.getDeepPools(); - deepPools.setEnabled(true).setMaximumPerReach(1); - - Engine engine = mock(Engine.class); - EngineMantle engineMantle = mock(EngineMantle.class); - IrisComplex complex = mock(IrisComplex.class); - ChunkContext context = mock(ChunkContext.class); - IrisRiverRuntime runtime = mock(IrisRiverRuntime.class); - RiverAnchor anchor = mock(RiverAnchor.class); - when(engineMantle.getEngine()).thenReturn(engine); - when(engine.getDimension()).thenReturn(dimension); - when(engine.getComplex()).thenReturn(complex); - when(context.getComplex()).thenReturn(complex); - when(complex.getRiverRuntime()).thenReturn(runtime); - when(runtime.caveSettings()).thenReturn(caves); - when(runtime.maximumChannelWidth()).thenReturn(12D); - when(runtime.maximumTunnelWidthMultiplier()).thenReturn(1D); - when(runtime.tunnelMouthBlend()).thenReturn(0D); - when(runtime.candidateAnchors( - anyInt(), - anyInt(), - anyInt(), - anyInt(), - anyDouble(), - anyLong() - )).thenReturn(List.of(anchor)); - - MantleRiverHydrologyComponent component = new MantleRiverHydrologyComponent(engineMantle); - assertEquals(0, component.getInputRadius(0, 0, 0, context)); - - when(runtime.hasRiverFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(true); - assertEquals( - RiverTopologyComplexity.tunnelHalo(12D, 1D, 0D), - component.getInputRadius(0, 0, 0, context) - ); - - when(runtime.hasRiverFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(false); - when(runtime.acceptsCaveAnchor(anchor)).thenReturn(true); - assertEquals( - MantleRiverHydrologyComponent.planningHalo(caves), - component.getInputRadius(0, 0, 0, context) - ); - - when(runtime.acceptsCaveAnchor(anchor)).thenReturn(false); - when(runtime.acceptsDeepPoolAnchor(anchor)).thenReturn(true); - assertEquals( - MantleRiverHydrologyComponent.deepPoolPlanningHalo(deepPools), - component.getInputRadius(0, 0, 0, context) - ); - } finally { - IrisPlatforms.unbind(); - } - } - - @Test - public void buriedChannelPlansWetCoreDryRoofAndSolidGuardShell() { - TestVoxelView view = new TestVoxelView(); - IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample( - riverSample(RiverRouteState.WET, RiverSection.CHANNEL), - 10, - 12, - 14 - ); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - view, - 0, - 0, - 1, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> riverSample( - RiverRouteState.WET, - RiverSection.CHANNEL - ), - (x, z) -> x == 0 && z == 0 ? tunnel : null, - (x, z) -> IrisRiverSurfaceSample.none(70D, 63D) - ); - - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 11, 0))); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0))); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 13, 0))); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 14, 0))); - assertEquals(RiverCaveAction.SEAL_GUARD, plan.actions().get(new CavePosition(1, 12, 0))); - assertTrue(MantleRiverHydrologyComponent.preconditionsHold(view, plan.preconditions())); - } - - @Test - public void buriedChannelCutsIntoCaveWhileGuardingItsWaterline() { - TestVoxelView view = new TestVoxelView(); - view.set(new CavePosition(1, 12, 0), CaveVoxel.CAVE_AIR); - view.set(new CavePosition(1, 14, 0), CaveVoxel.CAVE_AIR); - IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample( - riverSample(RiverRouteState.WET, RiverSection.CHANNEL), - 10, - 12, - 14 - ); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - view, - 0, - 0, - 1, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> riverSample( - RiverRouteState.WET, - RiverSection.CHANNEL - ), - (x, z) -> x == 0 && z == 0 ? tunnel : null, - (x, z) -> IrisRiverSurfaceSample.none(70D, 63D) - ); - - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0))); - assertEquals(RiverCaveAction.SEAL_GUARD, plan.actions().get(new CavePosition(1, 12, 0))); - assertFalse(plan.actions().containsKey(new CavePosition(1, 14, 0))); - assertEquals( - new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false), - plan.preconditions().get(new CavePosition(1, 14, 0)) - ); - assertTrue(MantleRiverHydrologyComponent.preconditionsHold(view, plan.preconditions())); - } - - @Test - public void buriedChannelRejectsSurfaceOpenCaveAir() { - TestVoxelView view = new TestVoxelView(); - CavePosition contact = new CavePosition(1, 12, 0); - view.set(contact, CaveVoxel.CAVE_AIR); - view.open(contact); - IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample( - riverSample(RiverRouteState.WET, RiverSection.CHANNEL), - 10, - 12, - 14 - ); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - view, - 0, - 0, - 1, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> riverSample( - RiverRouteState.WET, - RiverSection.CHANNEL - ), - (x, z) -> x == 0 && z == 0 ? tunnel : null, - (x, z) -> IrisRiverSurfaceSample.none(70D, 63D) - ); - - assertTrue(plan.actions().isEmpty()); - } - - @Test - public void buriedChannelStillRejectsAnUncontainedFluidCave() { - TestVoxelView view = new TestVoxelView(); - view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID); - IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample( - riverSample(RiverRouteState.WET, RiverSection.CHANNEL), - 10, - 12, - 14 - ); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - view, - 0, - 0, - 1, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> riverSample( - RiverRouteState.WET, - RiverSection.CHANNEL - ), - (x, z) -> x == 0 && z == 0 ? tunnel : null, - (x, z) -> IrisRiverSurfaceSample.none(70D, 63D) - ); - - assertTrue(plan.actions().isEmpty()); - assertTrue(plan.preconditions().isEmpty()); - } - - @Test - public void buriedChannelMouthMayFlareIntoTheWetSurfaceBank() { - TestVoxelView view = new TestVoxelView(); - view.set(new CavePosition(1, 11, 0), CaveVoxel.COMPATIBLE_FLUID); - view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID); - view.set(new CavePosition(1, 13, 0), CaveVoxel.CAVE_AIR); - view.set(new CavePosition(1, 14, 0), CaveVoxel.CAVE_AIR); - IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample( - riverSample(RiverRouteState.WET, RiverSection.CHANNEL), - 10, - 12, - 14 - ); - IrisRiverSurfaceSample mouth = new IrisRiverSurfaceSample( - riverSample(RiverRouteState.WET, RiverSection.BANK), - 14D, - 10D, - 12D, - false, - true - ); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - view, - 0, - 0, - 1, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> riverSample( - RiverRouteState.WET, - RiverSection.CHANNEL - ), - (x, z) -> x == 0 && z == 0 ? tunnel : null, - (x, z) -> x == 1 && z == 0 ? mouth : IrisRiverSurfaceSample.none(70D, 63D) - ); - - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0))); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 14, 0))); - assertFalse(plan.preconditions().containsKey(new CavePosition(1, 12, 0))); - } - - @Test - public void buriedChannelContinuesAcrossPreviouslyPublishedNeighborCells() { - TestVoxelView view = new TestVoxelView(); - view.publish(new CavePosition(-1, 11, 0), RiverCaveAction.WET_SOURCE); - view.publish(new CavePosition(-1, 12, 0), RiverCaveAction.WET_SOURCE); - view.publish(new CavePosition(-1, 13, 0), RiverCaveAction.DRY_AIR); - view.publish(new CavePosition(-1, 14, 0), RiverCaveAction.DRY_AIR); - IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample( - riverSample(RiverRouteState.WET, RiverSection.CHANNEL), - 10, - 12, - 14 - ); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - view, - 0, - 0, - 1, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> riverSample( - RiverRouteState.WET, - RiverSection.CHANNEL - ), - (x, z) -> (x == -1 || x == 0) && z == 0 ? tunnel : null, - (x, z) -> IrisRiverSurfaceSample.none(70D, 63D) - ); - - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0))); - assertTrue(MantleRiverHydrologyComponent.preconditionsHold(view, plan.preconditions())); - } - - @Test - public void sinkholeTerminalForcesGeneratedGrottoEvenInClosedMode() { - assertEquals( - RiverCaveMode.CLOSED_COMPONENT, - MantleRiverHydrologyComponent.sourceMode( - IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT, - false - ) - ); - assertEquals( - RiverCaveMode.GENERATED_GROTTO, - MantleRiverHydrologyComponent.sourceMode( - IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT, - true - ) - ); - } - - @Test - public void wetClosedCavePlansSourcesWithoutChangingBaseline() { - TestVoxelView view = new TestVoxelView(); - CavePosition target = new CavePosition(8, 42, 8); - view.set(target, CaveVoxel.CAVE_AIR); - Map baseline = view.snapshot(); - IrisRiverCaves caves = caves(); - RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.plannerSettings(caves, 91L, null); - RiverCaveSource source = new RiverCaveSource( - 1L, - new CavePosition(8, 48, 8), - target, - 46, - RiverCaveMode.CLOSED_COMPONENT - ); - - RiverCavePlan plan = planner.plan(view, source, settings); - - assertTrue(plan.accepted()); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(target)); - assertEquals(baseline, view.snapshot()); - } - - @Test - public void onlyWetChannelBedsAreEligible() { - IrisRiverSurfaceSample wet = surface(RiverRouteState.WET, RiverSection.CHANNEL, true); - IrisRiverSurfaceSample dry = surface(RiverRouteState.DRY, RiverSection.DRY_CHANNEL, false); - IrisRiverSurfaceSample bank = surface(RiverRouteState.WET, RiverSection.BANK, true); - IrisRiverSurfaceSample absent = IrisRiverSurfaceSample.none(64D, 63D); - - assertTrue(MantleRiverHydrologyComponent.isWetChannelBed(wet)); - assertFalse(MantleRiverHydrologyComponent.isWetChannelBed(dry)); - assertFalse(MantleRiverHydrologyComponent.isWetChannelBed(bank)); - assertFalse(MantleRiverHydrologyComponent.isWetChannelBed(absent)); - } - - @Test - public void openLavaAndOversizeCandidatesRejectWhileSolidFallbackCanPlan() { - IrisRiverCaves caves = caves().setMaxFloodVolume(32); - RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.plannerSettings(caves, 92L, null); - RiverCaveSource closed = new RiverCaveSource( - 2L, - new CavePosition(0, 50, 0), - new CavePosition(0, 44, 0), - 48, - RiverCaveMode.CLOSED_COMPONENT - ); - TestVoxelView open = new TestVoxelView(); - open.set(closed.target(), CaveVoxel.CAVE_AIR); - open.open(closed.target()); - TestVoxelView lava = new TestVoxelView(); - lava.set(closed.target(), CaveVoxel.LAVA); - TestVoxelView oversize = new TestVoxelView(); - for (int y = 43; y <= 44; y++) { - for (int x = -2; x <= 2; x++) { - for (int z = -2; z <= 2; z++) { - oversize.set(new CavePosition(x, y, z), CaveVoxel.CAVE_AIR); - } - } - } - - assertEquals(RiverCaveRejection.OPEN_SURFACE, planner.plan(open, closed, settings).rejection()); - assertEquals(RiverCaveRejection.LAVA_CONTACT, planner.plan(lava, closed, settings).rejection()); - assertEquals(RiverCaveRejection.VOLUME_LIMIT, planner.plan(oversize, closed, settings).rejection()); - - IrisRiverCaves fallbackCaves = caves() - .setMaxFloodVolume(4096) - .setFallback(IrisRiverCaveFallback.GENERATE_GROTTO); - RiverCavePlannerSettings fallbackSettings = MantleRiverHydrologyComponent.plannerSettings( - fallbackCaves, - 92L, - null - ); - RiverCaveSource fallback = new RiverCaveSource( - 2L, - closed.entry(), - new CavePosition(3, 46, 0), - closed.waterHeadY(), - RiverCaveMode.GENERATED_GROTTO - ); - assertEquals( - RiverCaveRejection.NONE, - planner.plan(new TestVoxelView(), fallback, fallbackSettings).rejection() - ); - } - - @Test - public void waterfallPoolPoursThroughAnOpenPitWithoutFloodingItsComponent() { - IrisRiverCaves caves = caves().setMode(IrisRiverCaveMode.WATERFALL_POOL); - RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.plannerSettings(caves, 94L, null); - CavePosition entry = new CavePosition(0, 50, 0); - CavePosition target = new CavePosition(0, 44, 0); - CavePosition connectedPit = new CavePosition(1, 44, 0); - TestVoxelView view = new TestVoxelView(); - view.set(target, CaveVoxel.CAVE_AIR); - view.set(connectedPit, CaveVoxel.CAVE_AIR); - view.open(target); - view.open(connectedPit); - RiverCaveSource source = new RiverCaveSource( - 3L, - entry, - target, - 48, - RiverCaveMode.WATERFALL_POOL - ); - - RiverCavePlan plan = planner.plan(view, source, settings); - - assertTrue(plan.accepted()); - assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(entry)); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(target)); - assertFalse(plan.actions().containsKey(connectedPit)); - } - - @Test - public void generatedTargetKeepsItsGrottoShellBelowTheRiverBed() { - IrisRiverCaves caves = caves() - .setDryHeadroom(4) - .setGrottoVerticalRadius(6) - .setMaxBoreDepth(64); - CavePosition entry = new CavePosition(0, 54, 0); - int waterHeadY = 48; - - CavePosition target = MantleRiverHydrologyComponent.findGeneratedTarget( - new TestVoxelView(), - caves, - entry, - waterHeadY, - 0, - 0 - ); - - assertEquals(new CavePosition(0, 46, 0), target); - assertTrue(target.y() + caves.getGrottoVerticalRadius() < entry.y()); - assertTrue(target.y() + caves.getGrottoVerticalRadius() >= waterHeadY + caves.getDryHeadroom()); - } - - @Test - public void fluidPolicyMappingPreservesReplaceSemantics() { - assertEquals( - RiverCaveFluidPolicy.REJECT_EXISTING, - MantleRiverHydrologyComponent.fluidPolicy(IrisRiverExistingFluidPolicy.REJECT) - ); - assertEquals( - RiverCaveFluidPolicy.ALLOW_COMPATIBLE, - MantleRiverHydrologyComponent.fluidPolicy(IrisRiverExistingFluidPolicy.ALLOW_SAME) - ); - assertEquals( - RiverCaveFluidPolicy.REPLACE_CONTAINED, - MantleRiverHydrologyComponent.fluidPolicy(IrisRiverExistingFluidPolicy.REPLACE) - ); - } - - @Test - public void authoredHeadThroatAndProofLimitsMapDirectly() { - IrisRiverCaves caves = caves() - .setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT) - .setWaterLevelOffset(-3) - .setThroatRadius(5) - .setDryHeadroom(6) - .setMaxFloodRadius(23) - .setMaxFloodDepth(17); - RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.plannerSettings(caves, 93L, null); - - assertEquals(61, MantleRiverHydrologyComponent.waterHeadY( - surface(RiverRouteState.WET, RiverSection.CHANNEL, true), - caves - )); - assertEquals(5, settings.throatRadius()); - assertEquals(6, settings.dryHeadroom()); - assertEquals(23, settings.maxClosedComponentHorizontalRadius()); - assertEquals(17, settings.maxClosedComponentDepth()); - assertEquals(24, MantleRiverHydrologyComponent.closedComponentPublicationRadius(caves)); - assertEquals(5, MantleRiverHydrologyComponent.generatedGrottoPublicationRadius(caves)); - assertEquals(24, MantleRiverHydrologyComponent.cavePublicationRadius(caves)); - assertEquals(72, MantleRiverHydrologyComponent.candidateHalo(caves)); - assertEquals(96, MantleRiverHydrologyComponent.planningHalo(caves)); - } - - @Test - public void generatedGrottosDoNotInheritClosedComponentProofRadius() { - IrisRiverCaves caves = caves() - .setMode(IrisRiverCaveMode.GENERATE_GROTTO) - .setGrottoHorizontalRadius(12) - .setThroatRadius(2) - .setMaxFloodRadius(48); - - assertEquals(18, MantleRiverHydrologyComponent.generatedGrottoPublicationRadius(caves)); - assertEquals(49, MantleRiverHydrologyComponent.closedComponentPublicationRadius(caves)); - assertEquals(18, MantleRiverHydrologyComponent.cavePublicationRadius(caves)); - assertEquals(54, MantleRiverHydrologyComponent.candidateHalo(caves)); - assertEquals(72, MantleRiverHydrologyComponent.planningHalo(caves)); - - caves.setFallback(IrisRiverCaveFallback.GENERATE_GROTTO); - - assertEquals(21, MantleRiverHydrologyComponent.generatedGrottoPublicationRadius(caves)); - assertEquals(84, MantleRiverHydrologyComponent.planningHalo(caves)); - } - - @Test - public void inputRadiusUsesOnlyCapabilitiesActiveForTheConfiguredMode() { - IrisRiverCaves caves = caves() - .setMode(IrisRiverCaveMode.GENERATE_GROTTO) - .setGrottoHorizontalRadius(12) - .setThroatRadius(2) - .setMaxFloodRadius(48); - - assertEquals(72, MantleRiverHydrologyComponent.inputRadius(caves, 6)); - - caves.setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT); - assertEquals(196, MantleRiverHydrologyComponent.inputRadius(caves, 6)); - - caves.setMode(IrisRiverCaveMode.SEALED); - assertEquals(6, MantleRiverHydrologyComponent.inputRadius(caves, 6)); - - caves.getDeepPools() - .setEnabled(true) - .setSearchRadius(20) - .setHorizontalRadius(24); - assertEquals(180, MantleRiverHydrologyComponent.inputRadius(caves, 6)); - - caves.getDeepPools().setEnabled(false); - caves.setMode(IrisRiverCaveMode.GENERATE_GROTTO).setMaximumPerReach(0); - assertEquals(6, MantleRiverHydrologyComponent.inputRadius(caves, 6)); - } - - @Test - public void deepPoolSourceSearchUsesAbsoluteConfiguredHeightAndCaveFloor() { - IrisRiverDeepPools deepPools = new IrisRiverDeepPools() - .setMinimumFluidY(-180) - .setMaximumFluidY(-130) - .setSearchRadius(0) - .setSearchAttempts(1) - .setVerticalRadius(8) - .setDryHeadroom(4); - TestVoxelView view = new TestVoxelView(); - view.set(new CavePosition(12, 101, 20), CaveVoxel.CAVE_AIR); - RiverAnchor anchor = new RiverAnchor( - new RiverEdgeId(new RiverNodeId(1L, 1L), new RiverNodeId(2L, 2L)), - 0, - 91L, - 768D, - 73L, - 12D, - 20D, - 0.5D, - RiverRouteState.WET, - 1, - 1 - ); - - RiverCaveSource source = MantleRiverHydrologyComponent.deepPoolSourceFor( - view, - deepPools, - anchor, - -256, - 42L - ); - - assertNotNull(source); - assertEquals(new CavePosition(12, 100, 20), source.entry()); - assertEquals(new CavePosition(12, 96, 20), source.target()); - assertEquals(100, source.waterHeadY()); - assertEquals(-156, source.waterHeadY() - 256); - assertEquals(RiverCaveMode.DEEP_POOL, source.mode()); - } - - @Test - public void deepPoolProofRadiusContainsNoisyDiagonalLobesAndShell() { - IrisRiverDeepPools deepPools = new IrisRiverDeepPools() - .setHorizontalRadius(24) - .setVerticalRadius(10) - .setDryHeadroom(5); - - RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.deepPoolPlannerSettings( - deepPools, - 42L, - null - ); - - assertEquals(35, settings.maxHorizontalRadius()); - assertEquals(16, settings.maxDepth()); - assertEquals(35, settings.maxClosedComponentHorizontalRadius()); - } - - @Test - public void managedScaleWarpedDeepPoolPassesAsOneContainedBlob() { - IrisRiverDeepPools deepPools = new IrisRiverDeepPools() - .setHorizontalRadius(24) - .setVerticalRadius(10) - .setDryHeadroom(5) - .setShapeVariation(0.6D) - .setWarpStrength(8D) - .setMaximumVolume(65536); - TestVoxelView view = new TestVoxelView(); - view.set(new CavePosition(0, 101, 0), CaveVoxel.CAVE_AIR); - RiverCaveSource source = new RiverCaveSource( - 92L, - new CavePosition(0, 100, 0), - new CavePosition(0, 95, 0), - 100, - RiverCaveMode.DEEP_POOL - ); - - RiverCavePlan plan = planner.plan( - view, - source, - MantleRiverHydrologyComponent.deepPoolPlannerSettings(deepPools, 42L, null) - ); - - assertTrue(plan.rejection().toString(), plan.accepted()); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 100, 0))); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 101, 0))); - assertTrue(plan.actions().size() > 10000); - } - - @Test - public void absentRiverFootprintSkipsEveryTunnelColumnSample() { - AtomicInteger footprintSamples = new AtomicInteger(); - AtomicInteger tunnelSamples = new AtomicInteger(); - AtomicInteger surfaceSamples = new AtomicInteger(); - - MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels( - new TestVoxelView(), - 3, - -4, - 6, - 2, - (minimumX, minimumZ, maximumX, maximumZ) -> { - footprintSamples.incrementAndGet(); - assertEquals(42D, minimumX, 0D); - assertEquals(-70D, minimumZ, 0D); - assertEquals(70D, maximumX, 0D); - assertEquals(-42D, maximumZ, 0D); - return RiverSample.none(); - }, - (x, z) -> { - tunnelSamples.incrementAndGet(); - return null; - }, - (x, z) -> { - surfaceSamples.incrementAndGet(); - return IrisRiverSurfaceSample.none(70D, 63D); - } - ); - - assertTrue(plan.actions().isEmpty()); - assertEquals(1, footprintSamples.get()); - assertEquals(0, tunnelSamples.get()); - assertEquals(0, surfaceSamples.get()); - } - - @Test - public void configuredShapeAndWarpChangeDeterministicGrottoFootprints() { - IrisRiverCaves flatCaves = caves() - .setGrottoHorizontalRadius(6) - .setGrottoVerticalRadius(5) - .setDryHeadroom(1) - .setMaxFloodVolume(10000) - .setGrottoShapeStyle(new IrisGeneratorStyle(NoiseStyle.FLAT)) - .setGrottoWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX)) - .setGrottoWarpStrength(0D); - IrisRiverCaves shapedCaves = caves() - .setGrottoHorizontalRadius(6) - .setGrottoVerticalRadius(5) - .setDryHeadroom(1) - .setMaxFloodVolume(10000) - .setGrottoShapeStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(9D)) - .setGrottoWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX)) - .setGrottoWarpStrength(0D); - IrisRiverCaves warpedCaves = caves() - .setGrottoHorizontalRadius(6) - .setGrottoVerticalRadius(5) - .setDryHeadroom(1) - .setMaxFloodVolume(10000) - .setGrottoShapeStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(9D)) - .setGrottoWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(7D)) - .setGrottoWarpStrength(3D); - RiverCaveSource source = new RiverCaveSource( - 8L, - new CavePosition(37, 58, -21), - new CavePosition(37, 52, -21), - 56, - RiverCaveMode.GENERATED_GROTTO - ); - TestVoxelView view = new TestVoxelView(); - - RiverCavePlan flat = planner.plan( - view, - source, - MantleRiverHydrologyComponent.plannerSettings(flatCaves, 555L, null) - ); - RiverCavePlannerSettings shapedSettings = MantleRiverHydrologyComponent.plannerSettings( - shapedCaves, - 555L, - null - ); - RiverCavePlan shaped = planner.plan(view, source, shapedSettings); - RiverCavePlan shapedRepeat = planner.plan(view, source, shapedSettings); - RiverCavePlan warped = planner.plan( - view, - source, - MantleRiverHydrologyComponent.plannerSettings(warpedCaves, 555L, null) - ); - - assertTrue(flat.accepted()); - assertTrue(shaped.accepted()); - assertTrue(warped.accepted()); - assertEquals(shaped, shapedRepeat); - assertNotEquals(mutations(flat), mutations(shaped)); - assertNotEquals(mutations(shaped), mutations(warped)); - } - - @Test - public void chunkOwnershipIsOrderInvariantAndNonOverlapping() { - Set actions = Set.of( - new CavePosition(15, 40, 0), - new CavePosition(16, 40, 0), - new CavePosition(31, 40, 0), - new CavePosition(32, 40, 0) - ); - Set forward = partition(actions, new int[]{0, 1, 2}); - Set reverse = partition(actions, new int[]{2, 1, 0}); - - assertEquals(actions, forward); - assertEquals(forward, reverse); - } - - @Test - public void perChunkCandidateWindowsMatchGlobalDirectOverlapArbitration() { - IrisRiverCaves caves = caves() - .setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT) - .setMaxFloodRadius(16) - .setMaxFloodVolume(1024); - RiverCavePlannerSettings settings = MantleRiverHydrologyComponent.plannerSettings(caves, 777L, null); - TestVoxelView view = new TestVoxelView(); - view.set(new CavePosition(14, 45, 0), CaveVoxel.CAVE_AIR); - view.set(new CavePosition(16, 45, 0), CaveVoxel.CAVE_AIR); - view.set(new CavePosition(18, 45, 0), CaveVoxel.CAVE_AIR); - view.set(new CavePosition(200, 45, 0), CaveVoxel.CAVE_AIR); - List sources = List.of( - closedSource(30L, 14, 50), - closedSource(20L, 16, 51), - closedSource(10L, 18, 52), - closedSource(1L, 200, 60) - ); - Map global = planner.planAll(view, sources, settings).actions(); - - for (int chunkX = 0; chunkX <= 1; chunkX++) { - List localSources = candidateWindow(sources, caves, chunkX); - Map local = planner.planAll(view, localSources, settings).actions(); - assertEquals(owned(global, chunkX), owned(local, chunkX)); - } - assertFalse(global.containsKey(new CavePosition(14, 45, 0))); - assertTrue(global.containsKey(new CavePosition(18, 45, 0))); - } - - @Test - public void transactionRejectsAnyChangedActionOrBoundaryGuard() { - TestVoxelView view = new TestVoxelView(); - CavePosition wet = new CavePosition(0, 20, 0); - CavePosition guard = new CavePosition(1, 20, 0); - Map preconditions = Map.of( - wet, new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false), - guard, new CaveVoxelPrecondition(CaveVoxel.SOLID, false) - ); - view.set(wet, CaveVoxel.CAVE_AIR); - - assertTrue(MantleRiverHydrologyComponent.preconditionsHold(view, preconditions)); - view.set(guard, CaveVoxel.CAVE_AIR); - assertFalse(MantleRiverHydrologyComponent.preconditionsHold(view, preconditions)); - } - - private IrisRiverCaves caves() { - return new IrisRiverCaves() - .setThroatRadius(1) - .setDryHeadroom(2) - .setGrottoHorizontalRadius(3) - .setGrottoVerticalRadius(4) - .setMaxBoreDepth(24) - .setMaxFloodRadius(16) - .setMaxFloodDepth(16) - .setMaxFloodVolume(512) - .setGrottoShapeStyle(new IrisGeneratorStyle(NoiseStyle.FLAT)) - .setGrottoWarpStyle(new IrisGeneratorStyle(NoiseStyle.FLAT)) - .setGrottoWarpStrength(0D); - } - - private IrisRiverSurfaceSample surface(RiverRouteState state, RiverSection section, boolean fluid) { - return new IrisRiverSurfaceSample(riverSample(state, section), 68D, 61D, 64D, false, fluid); - } - - private RiverSample riverSample(RiverRouteState state, RiverSection section) { - RiverEdgeId edge = new RiverEdgeId(new RiverNodeId(1L, 1L), new RiverNodeId(2L, 2L)); - return new RiverSample( - true, - state, - section, - 0D, - 0.5D, - 1D, - 2, - 1, - 8D, - 4D, - 3D, - false, - edge - ); - } - - private Set mutations(RiverCavePlan plan) { - Set positions = new HashSet<>(); - for (Map.Entry entry : plan.actions().entrySet()) { - if (entry.getValue() != RiverCaveAction.SEAL_GUARD) { - positions.add(entry.getKey()); - } - } - return positions; - } - - private Set partition(Set actions, int[] order) { - Set published = new HashSet<>(); - for (int chunkX : order) { - for (CavePosition position : actions) { - if (MantleRiverHydrologyComponent.owns(chunkX, 0, position)) { - assertTrue(published.add(position)); - } - } - } - return published; - } - - private RiverCaveSource closedSource(long id, int x, int head) { - return new RiverCaveSource( - id, - new CavePosition(x, 60, 0), - new CavePosition(x, 45, 0), - head, - RiverCaveMode.CLOSED_COMPONENT - ); - } - - private List candidateWindow( - List sources, - IrisRiverCaves caves, - int chunkX - ) { - int halo = MantleRiverHydrologyComponent.candidateHalo(caves); - int minimum = (chunkX << 4) - halo; - int maximum = ((chunkX + 1) << 4) + halo; - List local = new ArrayList<>(); - for (RiverCaveSource source : sources) { - if (source.entry().x() >= minimum && source.entry().x() < maximum) { - local.add(source); - } - } - return local; - } - - private Map owned( - Map actions, - int chunkX - ) { - Map owned = new HashMap<>(); - for (Map.Entry entry : actions.entrySet()) { - if (MantleRiverHydrologyComponent.owns(chunkX, 0, entry.getKey())) { - owned.put(entry.getKey(), entry.getValue()); - } - } - return Map.copyOf(owned); - } - - private static void bindMockPlatform() { - IrisPlatforms.unbind(); - PlatformBlockState block = mock(PlatformBlockState.class); - PlatformRegistries registries = mock(PlatformRegistries.class); - when(registries.block(anyString())).thenReturn(block); - IrisPlatform platform = mock(IrisPlatform.class); - when(platform.registries()).thenReturn(registries); - IrisPlatforms.bind(platform); - } - - private static final class TestVoxelView implements MantleRiverHydrologyComponent.TunnelVoxelView { - private final Map voxels = new HashMap<>(); - private final Map riverActions = new HashMap<>(); - private final Set open = new HashSet<>(); - - @Override - public boolean isInWorld(CavePosition position) { - return position.y() > 0 && position.y() < 128; - } - - @Override - public CaveVoxel voxelAt(CavePosition position) { - return voxels.getOrDefault(position, CaveVoxel.SOLID); - } - - @Override - public boolean isOpenToSurface(CavePosition position) { - return open.contains(position); - } - - @Override - public RiverCaveHydrology riverHydrologyAt(CavePosition position) { - return riverActions.get(position); - } - - private void set(CavePosition position, CaveVoxel voxel) { - voxels.put(position, voxel); - } - - private void open(CavePosition position) { - open.add(position); - } - - private void publish(CavePosition position, RiverCaveAction action) { - riverActions.put(position, RiverCaveHydrology.of(action, RiverCaveFluidKind.RIVER)); - voxels.put( - position, - action == RiverCaveAction.WET_SOURCE - ? CaveVoxel.COMPATIBLE_FLUID - : CaveVoxel.CAVE_AIR - ); - } - - private Map snapshot() { - return Map.copyOf(voxels); - } - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java index 6f243956d..ef1550777 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/SurfaceFluidBoundaryPlanTest.java @@ -25,8 +25,8 @@ public class SurfaceFluidBoundaryPlanTest { int boundaryY = fixture.boundary(8, 8); assertEquals(60, boundaryY); - assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, fixture.index(8, 8), 59)); - assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, fixture.index(8, 8), 60)); + assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, fixture.index(8, 8), 59, FLUID_HEIGHT)); + assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, fixture.index(8, 8), 60, FLUID_HEIGHT)); } @Test @@ -38,10 +38,10 @@ public class SurfaceFluidBoundaryPlanTest { int columnIndex = fixture.index(8, 8); assertEquals(61, fixture.boundary(8, 8)); - assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, columnIndex, 60)); - assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, columnIndex, 61)); - assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, columnIndex, 64)); - assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, columnIndex, 65)); + assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 60, FLUID_HEIGHT)); + assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 61, FLUID_HEIGHT)); + assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 64, FLUID_HEIGHT)); + assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaryStartY, columnIndex, 65, FLUID_HEIGHT)); } @Test @@ -94,28 +94,13 @@ public class SurfaceFluidBoundaryPlanTest { new int[CHUNK_SIZE * CHUNK_SIZE], new double[FIELD_SIZE * FIELD_SIZE], new boolean[FIELD_SIZE * FIELD_SIZE], - new double[FIELD_SIZE * FIELD_SIZE], FIELD_SIZE, 0, - new long[CHUNK_SIZE * CHUNK_SIZE] + FLUID_HEIGHT, + new int[CHUNK_SIZE * CHUNK_SIZE] )); } - @Test - public void terracedReservoirProtectsOnlyToItsLocalHead() { - Fixture fixture = new Fixture(); - fixture.setFieldSurface(7, 8, 60D); - fixture.setFieldFluidHeight(7, 8, 67D); - - fixture.resolve(); - - int columnIndex = fixture.index(8, 8); - assertEquals(61, fixture.boundary(8, 8)); - assertEquals(67, fixture.boundaryEnd(8, 8)); - assertTrue(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, columnIndex, 67)); - assertFalse(SurfaceFluidBoundaryPlan.protects(fixture.boundaries, columnIndex, 68)); - } - private void assertEdgeBoundary(int localX, int localZ, int neighborLocalX, int neighborLocalZ) { Fixture fixture = new Fixture(); fixture.setFieldSurface(neighborLocalX, neighborLocalZ, 60D); @@ -126,14 +111,12 @@ public class SurfaceFluidBoundaryPlanTest { private static final class Fixture { private final int[] chunkSurfaceHeights = new int[CHUNK_SIZE * CHUNK_SIZE]; private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE]; - private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE]; private final boolean[] fieldHasFluid = new boolean[FIELD_SIZE * FIELD_SIZE]; - private final long[] boundaries = new long[CHUNK_SIZE * CHUNK_SIZE]; + private final int[] boundaryStartY = new int[CHUNK_SIZE * CHUNK_SIZE]; private Fixture() { Arrays.fill(chunkSurfaceHeights, 70); Arrays.fill(fieldSurfaceHeights, 70D); - Arrays.fill(fieldFluidHeights, FLUID_HEIGHT); Arrays.fill(fieldHasFluid, true); } @@ -154,30 +137,20 @@ public class SurfaceFluidBoundaryPlanTest { fieldHasFluid[(fieldX * FIELD_SIZE) + fieldZ] = hasFluid; } - private void setFieldFluidHeight(int localX, int localZ, double fluidHeight) { - int fieldX = localX + PADDING; - int fieldZ = localZ + PADDING; - fieldFluidHeights[(fieldX * FIELD_SIZE) + fieldZ] = fluidHeight; - } - private void resolve() { SurfaceFluidBoundaryPlan.fill( chunkSurfaceHeights, fieldSurfaceHeights, fieldHasFluid, - fieldFluidHeights, FIELD_SIZE, PADDING, - boundaries + FLUID_HEIGHT, + boundaryStartY ); } private int boundary(int localX, int localZ) { - return SurfaceFluidBoundaryPlan.startY(boundaries[index(localX, localZ)]); - } - - private int boundaryEnd(int localX, int localZ) { - return SurfaceFluidBoundaryPlan.endY(boundaries[index(localX, localZ)]); + return boundaryStartY[index(localX, localZ)]; } private int index(int localX, int localZ) { diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java index 9c58fa3a5..01279a14a 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierBoundarySupportTest.java @@ -5,8 +5,6 @@ import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; @@ -109,52 +107,10 @@ public class IrisCarveModifierBoundarySupportTest { assertTrue(IrisCarveModifier.hasStableCaveFloorSupport(output, 0, 6, 0)); } - @Test - public void riverGuardOnlyAcceptsStableSolidBoundaryLayers() { - RiverCaveHydrology guard = RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD); - PlatformBlockState stone = state("minecraft:stone", true); - PlatformBlockState sand = state("minecraft:sand", true); - PlatformBlockState water = state("minecraft:water", false, true); - - assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, false)); - assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, true)); - assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, sand, false)); - assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, sand, true)); - assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, water, false)); - } - - @Test - public void riverBiomeInheritanceIsColumnCoherentAndHonorsLimits() { - long seed = 7845123L; - boolean cell = IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0.5D); - for (int x = 8; x < 12; x++) { - for (int z = 12; z < 16; z++) { - assertTrue(cell == IrisCarveModifier.selectsParentRiverBiome(seed, x, z, 0.5D)); - } - } - assertFalse(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0D)); - assertTrue(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 1D)); - - boolean inherited = false; - boolean overridden = false; - for (int x = -128; x <= 128; x += 4) { - boolean selected = IrisCarveModifier.selectsParentRiverBiome(seed, x, 0, 0.5D); - inherited |= selected; - overridden |= !selected; - } - assertTrue(inherited); - assertTrue(overridden); - } - private PlatformBlockState state(String key, boolean solid) { - return state(key, solid, false); - } - - private PlatformBlockState state(String key, boolean solid, boolean fluid) { PlatformBlockState state = mock(PlatformBlockState.class); doReturn(key).when(state).key(); doReturn(solid).when(state).isSolid(); - doReturn(fluid).when(state).isFluid(); return state; } } diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java index 06ea0083c..5111ffaa6 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierInferenceIsolationTest.java @@ -15,7 +15,6 @@ import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.stream.ProceduralStream; -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.MatterCavern; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.junit.AfterClass; @@ -70,11 +69,10 @@ public class IrisCarveModifierInferenceIsolationTest { Map customBiomes = new HashMap<>(); customBiomes.put("shared", biome); CarveWallBuffer walls = new CarveWallBuffer(1); - walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0), false); + walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0)); Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod( "paintBoundaryZone", Hunk.class, - MantleChunk.class, CarveWallBuffer.class, int.class, int.class, @@ -90,7 +88,6 @@ public class IrisCarveModifierInferenceIsolationTest { paintBoundaryZone.invoke( modifier, mock(Hunk.class), - mock(MantleChunk.class), walls, 0, 0, diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierRiverHydrologyTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierRiverHydrologyTest.java deleted file mode 100644 index a30b29b7b..000000000 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierRiverHydrologyTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package art.arcane.iris.engine.modifier; - -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.volmlib.util.matter.MatterCavern; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class IrisCarveModifierRiverHydrologyTest { - @Test - public void overlayCompositionDoesNotMutateBaselineCavern() { - MatterCavern baseline = new MatterCavern(true, "baseline", (byte) 2); - - assertSame(baseline, IrisCarveModifier.composeCavern(baseline, null)); - assertNull(IrisCarveModifier.composeCavern( - baseline, RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD))); - - MatterCavern wet = IrisCarveModifier.composeCavern( - baseline, new RiverCaveHydrology( - RiverCaveAction.WET_SOURCE, - "iris:flooded", - RiverCaveFluidKind.DEEP_POOL - )); - MatterCavern dry = IrisCarveModifier.composeCavern( - baseline, RiverCaveHydrology.of(RiverCaveAction.DRY_AIR)); - - assertEquals(1, wet.getLiquid()); - assertEquals("iris:flooded", wet.getCustomBiome()); - assertEquals(3, dry.getLiquid()); - assertEquals(2, baseline.getLiquid()); - assertEquals("baseline", baseline.getCustomBiome()); - } - - @Test - public void hydrologyActionsResolveDistinctFluidStates() { - PlatformBlockState current = mock(PlatformBlockState.class); - PlatformBlockState source = mock(PlatformBlockState.class); - PlatformBlockState falling = mock(PlatformBlockState.class); - PlatformBlockState air = mock(PlatformBlockState.class); - when(source.key()).thenReturn("minecraft:water[level=0]"); - when(source.withProperty("level", "8")).thenReturn(falling); - - assertSame(source, IrisCarveModifier.resolveHydrologyState( - RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), current, source, air)); - assertSame(falling, IrisCarveModifier.resolveHydrologyState( - RiverCaveHydrology.of(RiverCaveAction.FALLING_FLUID), current, source, air)); - assertSame(air, IrisCarveModifier.resolveHydrologyState( - RiverCaveHydrology.of(RiverCaveAction.DRY_AIR), current, source, air)); - } - - @Test - public void waterloggingFollowsOnlyExplicitResultingWater() { - PlatformBlockState waterlogged = mock(PlatformBlockState.class); - PlatformBlockState dry = mock(PlatformBlockState.class); - PlatformBlockState water = mock(PlatformBlockState.class); - PlatformBlockState lava = mock(PlatformBlockState.class); - when(waterlogged.key()).thenReturn("minecraft:seagrass[waterlogged=true]"); - when(waterlogged.withProperty("waterlogged", "false")).thenReturn(dry); - when(water.isWater()).thenReturn(true); - - assertSame(waterlogged, IrisCarveModifier.normalizeWaterlogging(waterlogged, water)); - assertSame(dry, IrisCarveModifier.normalizeWaterlogging(waterlogged, null)); - assertSame(dry, IrisCarveModifier.normalizeWaterlogging(waterlogged, lava)); - assertSame(dry, IrisCarveModifier.resolveHydrologyState( - RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD), waterlogged, water, null)); - } - - @Test - public void noOverlayLeavesBaselineWaterloggingUntouched() { - PlatformBlockState waterlogged = mock(PlatformBlockState.class); - PlatformBlockState water = mock(PlatformBlockState.class); - MatterCavern flooded = new MatterCavern(true, "", (byte) 1); - - assertSame(waterlogged, IrisCarveModifier.normalizeHydrologyWaterlogging( - waterlogged, - flooded, - null, - water - )); - verify(waterlogged, never()).withProperty("waterlogged", "false"); - } - - @Test - public void explicitOverlayUsesComposedFluidIntentForWaterlogging() { - PlatformBlockState waterlogged = mock(PlatformBlockState.class); - PlatformBlockState dry = mock(PlatformBlockState.class); - PlatformBlockState water = mock(PlatformBlockState.class); - MatterCavern flooded = new MatterCavern(true, "", (byte) 1); - when(waterlogged.key()).thenReturn("minecraft:seagrass[waterlogged=true]"); - when(waterlogged.withProperty("waterlogged", "false")).thenReturn(dry); - when(water.isWater()).thenReturn(true); - - assertSame(waterlogged, IrisCarveModifier.normalizeHydrologyWaterlogging( - waterlogged, - flooded, - RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE), - water - )); - assertSame(dry, IrisCarveModifier.normalizeHydrologyWaterlogging( - waterlogged, - flooded, - RiverCaveHydrology.of(RiverCaveAction.DRY_AIR), - water - )); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java index 3e38380c7..d03e4cea1 100644 --- a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveScratchTest.java @@ -22,21 +22,19 @@ public class IrisCarveScratchTest { int y = 20 + index; int z = (index * 3) & 15; MatterCavern cavern = new MatterCavern(true, "cave-" + index, (byte) 0); - buffer.put(x, y, z, cavern, (index & 1) == 0); + buffer.put(x, y, z, cavern); expected.put(key(x, y, z), cavern); } MatterCavern replacement = new MatterCavern(true, "replacement", (byte) 0); - buffer.put(5, 25, 15, replacement, true); + buffer.put(5, 25, 15, replacement); expected.put(key(5, 25, 15), replacement); assertSame(replacement, buffer.get(5, 25, 15)); - assertTrue(buffer.isRiverBoundary(5, 25, 15)); assertNull(buffer.get(5, 26, 15)); - assertFalse(buffer.isRiverBoundary(5, 26, 15)); Map actual = new HashMap<>(); - buffer.forEach((x, y, z, cavern, riverBoundary) -> actual.put(key(x, y, z), cavern)); + buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern)); assertEquals(expected.keySet(), actual.keySet()); for (Map.Entry entry : expected.entrySet()) { assertSame(entry.getValue(), actual.get(entry.getKey())); @@ -49,7 +47,7 @@ public class IrisCarveScratchTest { MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0); scratch.columnMasks[0].add(12); scratch.boundaryMasks[0].add(13); - scratch.walls.put(1, 12, 2, cavern, true); + scratch.walls.put(1, 12, 2, cavern); scratch.customBiomeCache.put("cave", null); scratch.customCaveBiomePresent = true; @@ -60,7 +58,7 @@ public class IrisCarveScratchTest { assertTrue(scratch.customBiomeCache.isEmpty()); assertFalse(scratch.customCaveBiomePresent); int[] wallCount = new int[1]; - scratch.walls.forEach((x, y, z, value, riverBoundary) -> wallCount[0]++); + scratch.walls.forEach((x, y, z, value) -> wallCount[0]++); assertEquals(0, wallCount[0]); } diff --git a/core/src/test/java/art/arcane/iris/engine/object/IObjectPlacerFluidHeightTest.java b/core/src/test/java/art/arcane/iris/engine/object/IObjectPlacerFluidHeightTest.java deleted file mode 100644 index c7248e4a5..000000000 --- a/core/src/test/java/art/arcane/iris/engine/object/IObjectPlacerFluidHeightTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.engine.IrisComplex; -import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.util.project.stream.ProceduralStream; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class IObjectPlacerFluidHeightTest { - @Test - public void missingEngineOrComplexFallsBackToThePlacersScalarHead() { - IObjectPlacer withoutEngine = mock(IObjectPlacer.class, CALLS_REAL_METHODS); - when(withoutEngine.getFluidHeight()).thenReturn(63); - when(withoutEngine.getEngine()).thenReturn(null); - - assertEquals(63, withoutEngine.getFluidHeight(12, -7)); - - IObjectPlacer withoutComplex = mock(IObjectPlacer.class, CALLS_REAL_METHODS); - Engine engine = mock(Engine.class); - when(withoutComplex.getFluidHeight()).thenReturn(63); - when(withoutComplex.getEngine()).thenReturn(engine); - when(engine.getComplex()).thenReturn(null); - - assertEquals(63, withoutComplex.getFluidHeight(12, -7)); - } - - @Test - public void engineLocalPlacersUseTheRoundedPerColumnRiverHead() { - IObjectPlacer placer = placer(127, 127, 130.6D); - - assertEquals(131, placer.getFluidHeight(12, -7)); - - verify(riverHead(placer)).get(12, -7); - } - - @Test - public void worldCoordinatePlacersShiftTheLocalRiverHeadExactlyOnce() { - IObjectPlacer placer = placer(63, 127, 130.6D); - - assertEquals(67, placer.getFluidHeight(12, -7)); - - verify(riverHead(placer)).get(12, -7); - } - - private static IObjectPlacer placer(int placerFluidHeight, int dimensionFluidHeight, double riverFluidHeight) { - IObjectPlacer placer = mock(IObjectPlacer.class, CALLS_REAL_METHODS); - Engine engine = mock(Engine.class); - IrisComplex complex = mock(IrisComplex.class); - IrisDimension dimension = mock(IrisDimension.class); - @SuppressWarnings("unchecked") - ProceduralStream riverHead = mock(ProceduralStream.class); - - when(placer.getFluidHeight()).thenReturn(placerFluidHeight); - when(placer.getEngine()).thenReturn(engine); - when(engine.getComplex()).thenReturn(complex); - when(engine.getDimension()).thenReturn(dimension); - when(dimension.getFluidHeight()).thenReturn(dimensionFluidHeight); - when(complex.getRiverWaterSurfaceStream()).thenReturn(riverHead); - when(riverHead.get(12, -7)).thenReturn(riverFluidHeight); - return placer; - } - - private static ProceduralStream riverHead(IObjectPlacer placer) { - return placer.getEngine().getComplex().getRiverWaterSurfaceStream(); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java index 0ec8d93c1..7c48a8290 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDepositTuningTest.java @@ -69,6 +69,28 @@ public class IrisDepositTuningTest { assertEquals(firstWorld, new IrisDepositGenerator.ClumpCacheKey(41L, 4, 8)); } + @Test + public void clumpSaltIsStableAcrossEquivalentConfigInstances() { + IrisData data = mock(IrisData.class); + IrisDepositGenerator first = generatorWithState(data, false, "minecraft:granite"); + IrisDepositGenerator second = generatorWithState(data, false, "minecraft:granite"); + + assertEquals(first.stableClumpSalt(data), second.stableClumpSalt(data)); + } + + @Test + public void clumpSaltIncludesAuthoredConfigAndPalette() { + IrisData data = mock(IrisData.class); + IrisDepositGenerator granite = generatorWithState(data, false, "minecraft:granite"); + IrisDepositGenerator andesite = generatorWithState(data, false, "minecraft:andesite"); + IrisDepositGenerator alteredShape = generatorWithState(data, false, "minecraft:granite"); + alteredShape.setShape(IrisDepositShape.VANILLA_SCATTERED); + + assertEquals(3112546198474861350L, granite.stableClumpSalt(data)); + assertNotEquals(granite.stableClumpSalt(data), andesite.stableClumpSalt(data)); + assertNotEquals(granite.stableClumpSalt(data), alteredShape.stableClumpSalt(data)); + } + @Test public void onlyOreDepositPalettesReceiveBiomeTuning() { IrisData data = mock(IrisData.class); @@ -143,10 +165,15 @@ public class IrisDepositTuningTest { } private IrisDepositGenerator generatorWithState(IrisData data, boolean ore) { + return generatorWithState(data, ore, ore ? "minecraft:iron_ore" : "minecraft:stone"); + } + + private IrisDepositGenerator generatorWithState(IrisData data, boolean ore, String key) { IrisBlockData block = mock(IrisBlockData.class); PlatformBlockState state = mock(PlatformBlockState.class); when(block.getBlockData(data)).thenReturn(state); when(state.isOre()).thenReturn(ore); + when(state.key()).thenReturn(key); IrisDepositGenerator generator = new IrisDepositGenerator(); generator.getPalette().add(block); return generator; diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisMaterialPaletteGeneratorCacheTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisMaterialPaletteGeneratorCacheTest.java new file mode 100644 index 000000000..a778b5541 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisMaterialPaletteGeneratorCacheTest.java @@ -0,0 +1,86 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class IrisMaterialPaletteGeneratorCacheTest { + private static final long GENERATOR_SALT = -23_498_896L; + + @Test + public void callerSeedsDoNotShareAFirstWinnerGenerator() { + GeneratorFixture fixture = new GeneratorFixture(); + + assertSame(fixture.firstGenerator, fixture.palette.getLayerGenerator(new RNG(11L), fixture.data)); + assertSame(fixture.secondGenerator, fixture.palette.getLayerGenerator(new RNG(29L), fixture.data)); + assertSame(fixture.firstGenerator, fixture.palette.getLayerGenerator(new RNG(11L), fixture.data)); + } + + @Test + public void reverseInitializationOrderKeepsSeedAssignments() { + GeneratorFixture fixture = new GeneratorFixture(); + + assertSame(fixture.secondGenerator, fixture.palette.getLayerGenerator(new RNG(29L), fixture.data)); + assertSame(fixture.firstGenerator, fixture.palette.getLayerGenerator(new RNG(11L), fixture.data)); + } + + @Test + public void concurrentInitializationKeepsSeedAssignments() throws Exception { + GeneratorFixture fixture = new GeneratorFixture(); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future first = executor.submit(() -> { + assertTrue(start.await(5L, TimeUnit.SECONDS)); + return fixture.palette.getLayerGenerator(new RNG(11L), fixture.data); + }); + Future second = executor.submit(() -> { + assertTrue(start.await(5L, TimeUnit.SECONDS)); + return fixture.palette.getLayerGenerator(new RNG(29L), fixture.data); + }); + start.countDown(); + + assertSame(fixture.firstGenerator, first.get(5L, TimeUnit.SECONDS)); + assertSame(fixture.secondGenerator, second.get(5L, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + } + } + + private static final class GeneratorFixture { + private final IrisData data = mock(IrisData.class); + private final Engine engine = mock(Engine.class); + private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); + private final CNG firstGenerator = mock(CNG.class); + private final CNG secondGenerator = mock(CNG.class); + private final IrisMaterialPalette palette = new IrisMaterialPalette().qclear().setStyle(style); + + private GeneratorFixture() { + when(data.getEngine()).thenReturn(engine); + Map generators = Map.of( + 11L + GENERATOR_SALT, firstGenerator, + 29L + GENERATOR_SALT, secondGenerator + ); + when(style.create(any(RNG.class), same(data), same(engine))).thenAnswer(invocation -> { + RNG rng = invocation.getArgument(0); + return generators.get(rng.getSeed()); + }); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java index f08eb5422..c1a6520a0 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisMathNoiseHotPathParityTest.java @@ -43,7 +43,7 @@ public class IrisMathNoiseHotPathParityTest { .setOpacity(0.91D) .setComposite(new KList().qadd(noiseGenerator)); - assertEquals(0.451949817597527D, generator.getHeight(63.0D, -27.0D, 445566L), 0D); + assertEquals(0.5671980255964687D, generator.getHeight(63.0D, -27.0D, 445566L), 0D); } @Test diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisNoiseGeneratorCacheTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisNoiseGeneratorCacheTest.java new file mode 100644 index 000000000..f0831d23b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisNoiseGeneratorCacheTest.java @@ -0,0 +1,93 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public final class IrisNoiseGeneratorCacheTest { + private static final long GENERATOR_SALT = 33_955_677L; + private static final long CONFIGURED_SEED = 7L; + private static final int OCTAVES = 3; + + @Test + public void callerSeedsDoNotShareAFirstWinnerGenerator() { + GeneratorFixture fixture = new GeneratorFixture(); + + assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data)); + assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data)); + assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data)); + } + + @Test + public void reverseInitializationOrderKeepsSeedAssignments() { + GeneratorFixture fixture = new GeneratorFixture(); + + assertSame(fixture.secondGenerator, fixture.generator.getGenerator(29L, fixture.data)); + assertSame(fixture.firstGenerator, fixture.generator.getGenerator(11L, fixture.data)); + } + + @Test + public void concurrentInitializationKeepsSeedAssignments() throws Exception { + GeneratorFixture fixture = new GeneratorFixture(); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future first = executor.submit(() -> { + assertTrue(start.await(5L, TimeUnit.SECONDS)); + return fixture.generator.getGenerator(11L, fixture.data); + }); + Future second = executor.submit(() -> { + assertTrue(start.await(5L, TimeUnit.SECONDS)); + return fixture.generator.getGenerator(29L, fixture.data); + }); + start.countDown(); + + assertSame(fixture.firstGenerator, first.get(5L, TimeUnit.SECONDS)); + assertSame(fixture.secondGenerator, second.get(5L, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + } + } + + private static final class GeneratorFixture { + private final IrisData data = mock(IrisData.class); + private final Engine engine = mock(Engine.class); + private final IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); + private final CNG firstGenerator = mock(CNG.class); + private final CNG secondGenerator = mock(CNG.class); + private final IrisNoiseGenerator generator = new IrisNoiseGenerator() + .setStyle(style) + .setSeed(CONFIGURED_SEED) + .setOctaves(OCTAVES); + + private GeneratorFixture() { + when(data.getEngine()).thenReturn(engine); + when(firstGenerator.oct(OCTAVES)).thenReturn(firstGenerator); + when(secondGenerator.oct(OCTAVES)).thenReturn(secondGenerator); + Map generators = Map.of( + 11L + GENERATOR_SALT - CONFIGURED_SEED, firstGenerator, + 29L + GENERATOR_SALT - CONFIGURED_SEED, secondGenerator + ); + when(style.createNoCache(any(RNG.class), same(data))).thenAnswer(invocation -> { + RNG rng = invocation.getArgument(0); + return generators.get(rng.getSeed()); + }); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisRiverConfigurationTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisRiverConfigurationTest.java deleted file mode 100644 index c6db9087d..000000000 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisRiverConfigurationTest.java +++ /dev/null @@ -1,317 +0,0 @@ -package art.arcane.iris.engine.object; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.core.loader.ResourceLoader; -import art.arcane.volmlib.util.collection.KList; -import com.google.gson.Gson; -import org.junit.Test; - -import java.util.Set; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class IrisRiverConfigurationTest { - @Test - public void defaultsKeepRiverGenerationDisabledAndContained() { - IrisDimension dimension = new IrisDimension(); - - assertNotNull(dimension.getRivers()); - assertFalse(dimension.getRivers().isEnabled()); - assertEquals(IrisRiverWaterMode.FIXED, dimension.getRivers().getWater().getMode()); - assertEquals(63, dimension.getRivers().getWater().getFluidHeight()); - assertEquals("water", dimension.getRivers().getWater().getFluidPalette().getPalette().getFirst().getBlock()); - assertFalse(dimension.getRivers().getTopology().isRequireOcean()); - assertEquals(512, dimension.getRivers().getTopology().getCellSize()); - assertEquals(16, dimension.getRivers().getTopology().getMaxRouteReaches()); - assertEquals(0, dimension.getRivers().getTopology().getMinimumSourcesPerTile()); - assertEquals(64, dimension.getRivers().getTopology().getRoutingBasinCells()); - assertEquals(8D, dimension.getRivers().getTopology().getRoutingPlateauHeight(), 0D); - assertEquals(0.05D, dimension.getRivers().getTopology().getSource().getChance(), 0D); - assertEquals(0.035D, dimension.getRivers().getTopology().getSource().getInfluence(), 0D); - assertEquals(10D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D); - assertEquals(4D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D); - assertEquals(10D, dimension.getRivers().getTerrain().getMaxDepth(), 0D); - assertTrue(dimension.getRivers().getTerrain().getWorms().isEmpty()); - assertEquals(IrisRiverCaveMode.SEALED, dimension.getRivers().getCaves().getMode()); - assertEquals(IrisRiverCaveFallback.SEALED, dimension.getRivers().getCaves().getFallback()); - assertEquals(IrisRiverExistingFluidPolicy.REJECT, - dimension.getRivers().getCaves().getExistingFluidPolicy()); - assertNotNull(dimension.getRivers().getCaves().getDeepPools()); - assertFalse(dimension.getRivers().getCaves().getDeepPools().isEnabled()); - assertEquals(-224, dimension.getRivers().getCaves().getDeepPools().getMinimumFluidY()); - assertEquals(-104, dimension.getRivers().getCaves().getDeepPools().getMaximumFluidY()); - assertEquals("lava", dimension.getRivers().getCaves().getDeepPools() - .getFluidPalette().getPalette().getFirst().getBlock()); - assertTrue(dimension.getRivers().getBiomes().getAllBiomeIds().isEmpty()); - assertNull(new IrisRegion().getRiverOverride()); - assertNull(new IrisBiome().getRiverOverride()); - } - - @Test - public void deserializesTypedDimensionRegionAndBiomeSettings() { - Gson gson = new Gson(); - IrisDimension dimension = gson.fromJson(""" - { - "rivers": { - "enabled": true, - "topology": { - "cellSize": 512, - "minimumSourcesPerTile": 2, - "routingBasinCells": 96, - "routingPlateauHeight": 12, - "requireOcean": false, - "source": {"chance": 0.27, "influence": 0.4} - }, - "terrain": { - "channelRadiusBonus": 3, - "maxChannelWidth": 9, - "maxBankWidth": 2.5, - "maxDepth": 8, - "maxIncision": 36, - "worms": [ - { - "seed": 73, - "weight": 2.5, - "wavelength": 1536, - "detailWavelength": 192, - "tortuosity": 0.65, - "detailTortuosity": 0.2, - "maxOffset": 420, - "segments": 56, - "widthMultiplier": 1.4, - "bankMultiplier": 1.2, - "depthMultiplier": 0.8, - "bodyWavelength": 704, - "bodyDetailWavelength": 18, - "bodyDetailInfluence": 0.82, - "widthVariation": 0.75, - "bankVariation": 0.65, - "depthVariation": 0.55, - "roofVariation": 0.45 - } - ], - "terminalMode": "SUPPRESS" - }, - "water": { - "mode": "TERRACED", - "fluidHeight": -48, - "fluidPalette": { - "palette": [{"block": "minecraft:lava"}] - }, - "poolLength": 80 - }, - "biomes": { - "channel": ["river/channel"], - "floodedCave": ["river/grotto"] - }, - "caves": { - "mode": "FLOOD_CLOSED_COMPONENT", - "maxFloodVolume": 2048, - "existingFluidPolicy": "ALLOW_SAME", - "deepPools": { - "enabled": true, - "reach": { - "chance": 0.4, - "influence": 0.1 - }, - "minimumSpacing": 896, - "maximumPerReach": 2, - "minimumFluidY": -220, - "maximumFluidY": -112, - "searchRadius": 24, - "searchAttempts": 18, - "horizontalRadius": 28, - "verticalRadius": 11, - "dryHeadroom": 5, - "shapeVariation": 0.7, - "warpStrength": 9, - "maximumVolume": 65536, - "fluidPalette": { - "palette": [{"block": "minecraft:lava"}] - } - } - } - } - } - """, IrisDimension.class); - IrisRegion region = gson.fromJson(""" - { - "riverOverride": { - "allowSources": false, - "routingPolicy": "AVOID", - "widthMultiplier": 0.75, - "bankBiomes": ["river/region-bank"] - } - } - """, IrisRegion.class); - IrisBiome biome = gson.fromJson(""" - { - "riverOverride": { - "routingPolicy": "BLOCK", - "caveEntryMultiplier": 0.2, - "floodedCaveBiomes": [] - } - } - """, IrisBiome.class); - - assertTrue(dimension.getRivers().isEnabled()); - assertEquals(512, dimension.getRivers().getTopology().getCellSize()); - assertEquals(2, dimension.getRivers().getTopology().getMinimumSourcesPerTile()); - assertEquals(96, dimension.getRivers().getTopology().getRoutingBasinCells()); - assertEquals(12D, dimension.getRivers().getTopology().getRoutingPlateauHeight(), 0D); - assertFalse(dimension.getRivers().getTopology().isRequireOcean()); - assertEquals(0.27D, dimension.getRivers().getTopology().getSource().getChance(), 0D); - assertEquals(9D, dimension.getRivers().getTerrain().getMaxChannelWidth(), 0D); - assertEquals(3D, dimension.getRivers().getTerrain().getChannelRadiusBonus(), 0D); - assertEquals(2.5D, dimension.getRivers().getTerrain().getMaxBankWidth(), 0D); - assertEquals(8D, dimension.getRivers().getTerrain().getMaxDepth(), 0D); - assertEquals(36, dimension.getRivers().getTerrain().getMaxIncision()); - IrisRiverWorm worm = dimension.getRivers().getTerrain().getWorms().get(0); - assertEquals(73L, worm.getSeed()); - assertEquals(2.5D, worm.getWeight(), 0D); - assertEquals(1536D, worm.getWavelength(), 0D); - assertEquals(192D, worm.getDetailWavelength(), 0D); - assertEquals(0.65D, worm.getTortuosity(), 0D); - assertEquals(0.2D, worm.getDetailTortuosity(), 0D); - assertEquals(420D, worm.getMaxOffset(), 0D); - assertEquals(56, worm.getSegments()); - assertEquals(1.4D, worm.getWidthMultiplier(), 0D); - assertEquals(1.2D, worm.getBankMultiplier(), 0D); - assertEquals(0.8D, worm.getDepthMultiplier(), 0D); - assertEquals(704D, worm.getBodyWavelength(), 0D); - assertEquals(18D, worm.getBodyDetailWavelength(), 0D); - assertEquals(0.82D, worm.getBodyDetailInfluence(), 0D); - assertEquals(0.75D, worm.getWidthVariation(), 0D); - assertEquals(0.65D, worm.getBankVariation(), 0D); - assertEquals(0.55D, worm.getDepthVariation(), 0D); - assertEquals(0.45D, worm.getRoofVariation(), 0D); - assertEquals(IrisRiverTerminalMode.SUPPRESS, - dimension.getRivers().getTerrain().getTerminalMode()); - assertEquals(IrisRiverWaterMode.TERRACED, dimension.getRivers().getWater().getMode()); - assertEquals(-48, dimension.getRivers().getWater().getFluidHeight()); - assertEquals("minecraft:lava", - dimension.getRivers().getWater().getFluidPalette().getPalette().getFirst().getBlock()); - assertEquals(Set.of("river/channel", "river/grotto"), - Set.copyOf(dimension.getRivers().getBiomes().getAllBiomeIds())); - assertEquals(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT, - dimension.getRivers().getCaves().getMode()); - assertEquals(IrisRiverExistingFluidPolicy.ALLOW_SAME, - dimension.getRivers().getCaves().getExistingFluidPolicy()); - IrisRiverDeepPools deepPools = dimension.getRivers().getCaves().getDeepPools(); - assertTrue(deepPools.isEnabled()); - assertEquals(0.4D, deepPools.getReach().getChance(), 0D); - assertEquals(0.1D, deepPools.getReach().getInfluence(), 0D); - assertEquals(896, deepPools.getMinimumSpacing()); - assertEquals(2, deepPools.getMaximumPerReach()); - assertEquals(-220, deepPools.getMinimumFluidY()); - assertEquals(-112, deepPools.getMaximumFluidY()); - assertEquals(24, deepPools.getSearchRadius()); - assertEquals(18, deepPools.getSearchAttempts()); - assertEquals(28, deepPools.getHorizontalRadius()); - assertEquals(11, deepPools.getVerticalRadius()); - assertEquals(5, deepPools.getDryHeadroom()); - assertEquals(0.7D, deepPools.getShapeVariation(), 0D); - assertEquals(9D, deepPools.getWarpStrength(), 0D); - assertEquals(65536, deepPools.getMaximumVolume()); - assertEquals("minecraft:lava", deepPools.getFluidPalette().getPalette().getFirst().getBlock()); - - assertEquals(Boolean.FALSE, region.getRiverOverride().getAllowSources()); - assertEquals(IrisRiverRoutingPolicy.AVOID, region.getRiverOverride().getRoutingPolicy()); - assertEquals(Double.valueOf(0.75D), region.getRiverOverride().getWidthMultiplier()); - assertNull(region.getRiverOverride().getChannelBiomes()); - assertEquals(new KList<>("river/region-bank"), region.getRiverOverride().getBankBiomes()); - - assertEquals(IrisRiverRoutingPolicy.BLOCK, biome.getRiverOverride().getRoutingPolicy()); - assertNull(biome.getRiverOverride().getChannelBiomes()); - assertNotNull(biome.getRiverOverride().getFloodedCaveBiomes()); - assertTrue(biome.getRiverOverride().getFloodedCaveBiomes().isEmpty()); - } - - @Test - @SuppressWarnings("unchecked") - public void resolvesRiverPoolsOnlyWhenRiversAreEnabled() { - IrisRiverBiomes dimensionBiomes = new IrisRiverBiomes() - .setChannel(new KList<>("dimension-channel")); - IrisRiverOverride regionOverride = new IrisRiverOverride() - .setBankBiomes(new KList<>("region-bank")); - IrisRiverOverride biomeOverride = new IrisRiverOverride() - .setMouthBiomes(new KList<>("biome-mouth")) - .setFloodedCaveBiomes(new KList<>("biome-grotto")); - IrisDimension dimension = new IrisDimension() - .setRegions(new KList<>("region")) - .setRivers(new IrisRiverNetwork() - .setEnabled(true) - .setTerrain(new IrisRiverTerrain() - .setWorms(new KList<>(new IrisRiverWorm()))) - .setBiomes(dimensionBiomes)); - IrisRegion region = new IrisRegion() - .setLandBiomes(new KList<>("natural")) - .setRiverOverride(regionOverride); - IrisBiome natural = biome("natural").setRiverOverride(biomeOverride); - - IrisData data = mock(IrisData.class); - ResourceLoader regionLoader = mock(ResourceLoader.class); - ResourceLoader biomeLoader = mock(ResourceLoader.class); - when(data.getRegionLoader()).thenReturn(regionLoader); - when(data.getBiomeLoader()).thenReturn(biomeLoader); - when(regionLoader.load("region")).thenReturn(region); - when(biomeLoader.load("natural")).thenReturn(natural); - when(biomeLoader.load("dimension-channel")).thenReturn(biome("dimension-channel")); - when(biomeLoader.load("region-bank")).thenReturn(biome("region-bank")); - when(biomeLoader.load("biome-mouth")).thenReturn(biome("biome-mouth")); - when(biomeLoader.load("biome-grotto")).thenReturn(biome("biome-grotto")); - - Set enabledKeys = keys(dimension.getReachableBiomes(() -> data)); - dimension.getRivers().setEnabled(false); - Set disabledKeys = keys(dimension.getReachableBiomes(() -> data)); - - assertEquals(Set.of("natural", "dimension-channel", "region-bank", "biome-mouth", "biome-grotto"), - enabledKeys); - assertEquals(Set.of("natural"), disabledKeys); - } - - @Test - @SuppressWarnings("unchecked") - public void separatesNaturalAndRiverOnlyRegionBiomes() { - IrisRegion region = new IrisRegion() - .setLandBiomes(new KList<>("natural-parent")) - .setRiverOverride(new IrisRiverOverride() - .setChannelBiomes(new KList<>("river-parent"))); - IrisBiome naturalParent = biome("natural-parent").setChildren(new KList<>("natural-child")); - IrisBiome naturalChild = biome("natural-child"); - IrisBiome riverParent = biome("river-parent").setChildren(new KList<>("river-child")); - IrisBiome riverChild = biome("river-child"); - - IrisData data = mock(IrisData.class); - ResourceLoader biomeLoader = mock(ResourceLoader.class); - when(data.getBiomeLoader()).thenReturn(biomeLoader); - when(biomeLoader.load("natural-parent")).thenReturn(naturalParent); - when(biomeLoader.load("natural-child")).thenReturn(naturalChild); - when(biomeLoader.load("river-parent")).thenReturn(riverParent); - when(biomeLoader.load("river-child")).thenReturn(riverChild); - - assertEquals(Set.of("natural-parent"), Set.copyOf(region.getNaturalBiomeIds())); - assertEquals(Set.of("natural-parent", "river-parent"), Set.copyOf(region.getAllBiomeIds())); - assertEquals(Set.of("natural-parent", "natural-child"), - keys(region.getNaturalBiomes(() -> data))); - assertEquals(Set.of("natural-parent", "natural-child", "river-parent", "river-child"), - keys(region.getAllBiomes(() -> data))); - } - - private static IrisBiome biome(String loadKey) { - IrisBiome biome = new IrisBiome(); - biome.setLoadKey(loadKey); - return biome; - } - - private static Set keys(KList biomes) { - return biomes.stream().map(IrisBiome::getLoadKey).collect(Collectors.toSet()); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/river/RiverNetworkTest.java b/core/src/test/java/art/arcane/iris/engine/river/RiverNetworkTest.java deleted file mode 100644 index 4d22b6209..000000000 --- a/core/src/test/java/art/arcane/iris/engine/river/RiverNetworkTest.java +++ /dev/null @@ -1,1944 +0,0 @@ -package art.arcane.iris.engine.river; - -import org.junit.Test; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -public class RiverNetworkTest { - @Test - public void mapsNegativeWorldCoordinatesWithFloorDivision() { - RiverNetwork network = new RiverNetwork(options(1L).build()); - RiverTerrainSampler terrain = slopedTerrain(false); - - assertEquals(new RiverNodeId(-1L, -1L), network.nodeAtWorld(-1, -1, terrain).id()); - assertEquals(new RiverNodeId(-1L, 0L), network.nodeAtWorld(-64, 0, terrain).id()); - assertEquals(new RiverNodeId(0L, 0L), network.nodeAtWorld(0, 0, terrain).id()); - assertEquals(-1, network.tileXForBlock(-1)); - assertEquals(0, network.tileXForBlock(0)); - } - - @Test - public void expandedSamplingAddsOnlyTheRequestedLateralRadius() { - RiverNode from = node(0L, 0L, 0D, 0D); - RiverNode to = node(1L, 0L, 100D, 0D); - RiverTile tile = new RiverTile( - 0, - 0, - 0, - -64, - 128, - 64, - List.of(reach(from, to, 4D, 1D)) - ); - - assertFalse(tile.sample(50D, 5D).present()); - RiverSample expanded = tile.sampleExpanded(50D, 5D, 2D); - assertTrue(expanded.present()); - assertEquals(RiverSection.BANK, expanded.section()); - assertEquals(5D, expanded.distance(), 0D); - assertThrows(IllegalArgumentException.class, () -> tile.sampleExpanded(50D, 5D, -1D)); - } - - @Test - public void graphHasReciprocalCardinalsAndOneStableDiagonalPerSquare() { - RiverNetwork network = new RiverNetwork(options(2L).build()); - RiverNodeId origin = new RiverNodeId(0L, 0L); - List neighbors = network.neighbors(origin); - - assertTrue(neighbors.contains(new RiverNodeId(-1L, 0L))); - assertTrue(neighbors.contains(new RiverNodeId(1L, 0L))); - assertTrue(neighbors.contains(new RiverNodeId(0L, -1L))); - assertTrue(neighbors.contains(new RiverNodeId(0L, 1L))); - assertTrue(neighbors.size() >= 4 && neighbors.size() <= 8); - for (RiverNodeId neighbor : neighbors) { - assertTrue(network.neighbors(neighbor).contains(origin)); - } - - boolean ascending = network.neighbors(new RiverNodeId(0L, 0L)).contains(new RiverNodeId(1L, 1L)); - boolean descending = network.neighbors(new RiverNodeId(0L, 1L)).contains(new RiverNodeId(1L, 0L)); - assertNotEquals(ascending, descending); - } - - @Test - public void downstreamAlwaysLowersStrictRankAndCannotCycle() { - RiverNetwork network = new RiverNetwork(options(3L).routingNoiseWeight(20.0).build()); - RiverTerrainSampler terrain = flatTerrain(false); - RiverNode current = network.nodeAtCell(12L, -7L, terrain); - Set visited = new HashSet<>(); - - for (int step = 0; step < 64; step++) { - assertTrue(visited.add(current.id())); - RiverNode next = network.downstream(current.id(), terrain); - if (next == null) { - break; - } - assertTrue(compareRank(next, current) < 0); - assertTrue(next.ocean() || next.hydraulicHeight() <= current.hydraulicHeight()); - current = next; - } - } - - @Test - public void routingPlateausCanCrossSmallNaturalRisesWithoutRaisingTheHydraulicHead() { - RiverNetwork network = new RiverNetwork(options(71L) - .siteJitter(0D) - .routingPlateauHeight(16D) - .routingNoiseWeight(0D) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double naturalHeight(int blockX, int blockZ) { - if (blockX >= 0 && blockX < 64 && blockZ >= 0 && blockZ < 64) { - return 68D; - } - return blockX >= 64 ? 70D : 80D; - } - - @Override - public double routingCost(int blockX, int blockZ) { - return blockX >= 64 ? 0D : 10D; - } - }; - - RiverNode source = network.nodeAtCell(0L, 0L, terrain); - RiverNode next = network.downstream(source.id(), terrain); - - assertNotNull(next); - assertTrue(next.naturalHeight() > source.naturalHeight()); - assertTrue(next.hydraulicHeight() <= source.hydraulicHeight()); - } - - @Test - public void longRoutesConvergeIntoDeterministicBranches() { - RiverNetwork network = new RiverNetwork(options(72L) - .tileCells(4) - .maxRouteReaches(16) - .routingPlateauHeight(8D) - .requireOcean(false) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double naturalHeight(int blockX, int blockZ) { - return 512D - blockX * 0.25D; - } - - @Override - public double routingCost(int blockX, int blockZ) { - return StrictMath.abs(blockZ) * 0.5D; - } - - @Override - public double flowNoise(double x, double z) { - return StrictMath.sin(x / 384D) + StrictMath.cos(z / 512D); - } - }; - - RiverRoute route = network.trace(new RiverNodeId(0L, 4L), terrain); - RiverTile tile = network.buildTile(0, 0, terrain); - HashMap incoming = new HashMap<>(); - for (RiverReach reach : tile.reaches()) { - incoming.merge(reach.to().id(), 1, Integer::sum); - } - - assertEquals(16, route.edges().size()); - assertTrue(incoming.values().stream().anyMatch(count -> count >= 2)); - assertEquals(digest(tile), digest(network.buildTile(0, 0, terrain))); - } - - @Test - public void recursiveBranchesApplyTheProfileChildSoftCapAtEveryNode() { - RiverWorm constrained = wormProfile( - "constrained", - 720L, - 6, - 1D, - 1D, - 1D, - 4, - 0D, - 1D, - 0D, - 0D, - List.of() - ); - RiverNetwork network = new RiverNetwork(options(720L) - .requireOcean(false) - .routingBasinCells(16) - .worms(List.of(constrained)) - .build()); - RiverTerrainSampler terrain = flatTerrain(false); - HashMap incoming = new HashMap<>(); - for (long cellX = -24L; cellX <= 24L; cellX++) { - for (long cellZ = -24L; cellZ <= 24L; cellZ++) { - RiverNode downstream = network.downstream(new RiverNodeId(cellX, cellZ), terrain); - if (downstream != null) { - incoming.merge(downstream.id(), 1, Integer::sum); - } - } - } - - assertFalse(incoming.isEmpty()); - assertTrue(incoming.values().stream().allMatch(children -> children <= 4)); - assertTrue(incoming.values().stream().anyMatch(children -> children >= 3)); - } - - @Test - public void candidateRankingAndTracingAreIndependentOfWormGeometry() { - RiverNetwork straightNetwork = new RiverNetwork(options(75L) - .requireOcean(false) - .worms(List.of(worm(1L, 0D, 0D, 0D, 1))) - .build()); - RiverNetwork windingNetwork = new RiverNetwork(options(75L) - .requireOcean(false) - .worms(List.of(worm(2L, 0.8D, 0.2D, 32D, 32))) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double reachRoutingCost(RiverRoutingContext context) { - return context.midpointX() + context.midpointZ(); - } - }; - RiverNodeId source = new RiverNodeId(0L, 0L); - - assertFalse(straightNetwork.downstreamCandidates(source, terrain).isEmpty()); - assertEquals( - straightNetwork.downstreamCandidates(source, terrain), - windingNetwork.downstreamCandidates(source, terrain) - ); - assertEquals(straightNetwork.trace(source, terrain), windingNetwork.trace(source, terrain)); - } - - @Test - public void routingFieldAlignmentSelectsTheMostParallelDownstreamReach() { - RiverNetwork reference = new RiverNetwork(options(74L) - .siteJitter(0D) - .flowAlignmentWeight(0D) - .build()); - RiverNodeId sourceId = new RiverNodeId(0L, 0L); - RiverTerrainSampler flat = flatTerrain(false); - RiverNode source = reference.nodeAtCell(sourceId.cellX(), sourceId.cellZ(), flat); - List candidates = reference.downstreamCandidates(sourceId, flat); - assertTrue(candidates.size() > 1); - RiverNode target = null; - double targetGap = 0D; - for (RiverNode proposed : candidates) { - double proposedX = proposed.x() - source.x(); - double proposedZ = proposed.z() - source.z(); - double proposedLength = StrictMath.hypot(proposedX, proposedZ); - double firstAlignment = -1D; - double secondAlignment = -1D; - RiverNode first = null; - for (RiverNode candidate : candidates) { - double candidateX = candidate.x() - source.x(); - double candidateZ = candidate.z() - source.z(); - double candidateLength = StrictMath.hypot(candidateX, candidateZ); - double alignment = StrictMath.abs( - proposedX * candidateX + proposedZ * candidateZ - ) / (proposedLength * candidateLength); - if (alignment > firstAlignment) { - secondAlignment = firstAlignment; - firstAlignment = alignment; - first = candidate; - } else if (alignment > secondAlignment) { - secondAlignment = alignment; - } - } - double gap = firstAlignment - secondAlignment; - if (gap > targetGap) { - targetGap = gap; - target = first; - } - } - assertNotNull(target); - assertTrue(targetGap > 0.000001D); - double tangentX = target.x() - source.x(); - double tangentZ = target.z() - source.z(); - RiverTerrainSampler guidedTerrain = new TestTerrain(false) { - @Override - public double flowNoise(double x, double z) { - return x * tangentZ - z * tangentX; - } - }; - RiverNetwork guided = new RiverNetwork(options(74L) - .siteJitter(0D) - .flowAlignmentWeight(10_000D) - .build()); - - RiverNode downstream = guided.downstream(sourceId, guidedTerrain); - - assertNotNull(downstream); - assertEquals(target.id(), downstream.id()); - } - - @Test - public void basinPotentialCarriesLongRoutesAcrossLocalTerrainRises() { - RiverNetwork network = new RiverNetwork(options(73L) - .siteJitter(0D) - .maxRouteReaches(16) - .routingPlateauHeight(8D) - .terrainHeightWeight(0D) - .requireOcean(false) - .build()); - RiverTerrainSampler flat = flatTerrain(false); - RiverRoute baseline = network.trace(new RiverNodeId(0L, 0L), flat); - HashSet raisedNodes = new HashSet<>(); - RiverNodeId baselineNode = new RiverNodeId(0L, 0L); - for (int edgeIndex = 0; edgeIndex < baseline.edges().size(); edgeIndex++) { - RiverEdgeId edge = baseline.edges().get(edgeIndex); - baselineNode = edge.first().equals(baselineNode) ? edge.second() : edge.first(); - if ((edgeIndex & 1) == 0) { - raisedNodes.add(baselineNode); - } - } - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double naturalHeight(int blockX, int blockZ) { - RiverNodeId id = new RiverNodeId(Math.floorDiv(blockX, 64), Math.floorDiv(blockZ, 64)); - return raisedNodes.contains(id) ? 104D : 64D; - } - }; - - RiverRoute route = network.trace(new RiverNodeId(0L, 0L), terrain); - RiverNode current = network.nodeAtCell(0L, 0L, terrain); - boolean crossedNaturalRise = false; - for (RiverEdgeId edge : route.edges()) { - RiverNode next = network.downstream(current.id(), terrain); - assertNotNull(next); - assertEquals(edge, RiverEdgeId.of(current.id(), next.id())); - crossedNaturalRise |= next.naturalHeight() > current.naturalHeight(); - assertTrue(next.rank() < current.rank()); - assertTrue(next.hydraulicHeight() <= current.hydraulicHeight()); - current = next; - } - - assertEquals(16, route.edges().size()); - assertTrue(crossedNaturalRise); - } - - @Test - public void rejectedBestReachReroutesToAnotherStrictlyDownhillCandidate() { - RiverNetwork network = new RiverNetwork(options(31L).downstreamCandidateLimit(4).build()); - RiverNodeId source = new RiverNodeId(0L, 0L); - RiverTerrainSampler terrain = slopedTerrain(false); - List candidates = network.downstreamCandidates(source, terrain); - assertTrue(candidates.size() > 1); - RiverEdgeId rejected = RiverEdgeId.of(source, candidates.getFirst().id()); - - RiverTerrainSampler reroutingTerrain = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - return !context.edgeId().equals(rejected); - } - }; - RiverNode rerouted = network.downstream(source, reroutingTerrain); - assertNotNull(rerouted); - assertNotEquals(candidates.getFirst().id(), rerouted.id()); - assertTrue(rerouted.rank() < network.nodeAtCell(0L, 0L, reroutingTerrain).rank()); - } - - @Test - public void failedContinuationStopsAtTheFirstPhysicallyValidReach() { - RiverNetwork network = new RiverNetwork(options(32L).reachChance(0D).build()); - int[] feasibilityChecks = new int[1]; - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - feasibilityChecks[0]++; - return true; - } - }; - - assertEquals(null, network.downstream(new RiverNodeId(0L, 0L), terrain)); - assertEquals(1, feasibilityChecks[0]); - } - - @Test - public void classifiesCompleteOceanRoutesAsWetAndIncompleteRoutesAsDryOrSuppressed() { - RiverNodeId source = new RiverNodeId(0L, 0L); - RiverNetwork wetNetwork = new RiverNetwork(options(4L).maxRouteReaches(16).build()); - RiverTerrainSampler oceanNeighbor = new TestTerrain(false) { - @Override - public boolean isOcean(int blockX, int blockZ) { - return Math.floorDiv(blockX, 64) != 0 || Math.floorDiv(blockZ, 64) != 0; - } - }; - RiverRoute wet = wetNetwork.trace(source, oceanNeighbor); - assertEquals(RiverRouteState.WET, wet.state()); - assertFalse(wet.edges().isEmpty()); - - RiverNetwork dryNetwork = new RiverNetwork(options(4L).maxRouteReaches(6).build()); - RiverTerrainSampler physicalTerminal = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - return context.from().id().equals(source); - } - }; - RiverRoute dry = dryNetwork.trace(source, physicalTerminal); - assertEquals(RiverRouteState.DRY, dry.state()); - assertFalse(dry.edges().isEmpty()); - - RiverNetwork suppressedNetwork = new RiverNetwork( - options(4L).maxRouteReaches(6).dryChannelChance(0.0).build() - ); - RiverRoute suppressed = suppressedNetwork.trace(source, physicalTerminal); - assertEquals(RiverRouteState.SUPPRESSED, suppressed.state()); - assertTrue(suppressed.edges().isEmpty()); - } - - @Test - public void oceanOptionalRoutesDoNotTurnTheTraceHorizonIntoATerminal() { - RiverNetwork network = new RiverNetwork( - options(41L).maxRouteReaches(3).requireOcean(false).build() - ); - RiverRoute route = network.trace(new RiverNodeId(0L, 0L), slopedTerrain(false)); - assertEquals(RiverRouteState.WET, route.state()); - assertFalse(route.oceanConnected()); - assertFalse(route.terminal()); - assertFalse(route.edges().isEmpty()); - } - - @Test - public void sampledTerminalPoliciesOverrideTheNetworkFallback() { - RiverNodeId source = new RiverNodeId(0L, 0L); - RiverNetwork requiredOcean = new RiverNetwork(options(42L).maxRouteReaches(3).requireOcean(true).build()); - RiverTerrainSampler wetTerminal = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - return context.from().id().equals(source); - } - - @Override - public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) { - return RiverTerminalPolicy.WET; - } - }; - RiverTerrainSampler suppressedTerminal = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - return context.from().id().equals(source); - } - - @Override - public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) { - return RiverTerminalPolicy.SUPPRESS; - } - }; - - assertEquals(RiverRouteState.WET, requiredOcean.trace(source, wetTerminal).state()); - assertEquals(RiverRouteState.SUPPRESSED, requiredOcean.trace(source, suppressedTerminal).state()); - - RiverNetwork optionalOcean = new RiverNetwork(options(43L).maxRouteReaches(3).requireOcean(false).build()); - RiverTerrainSampler dryTerminal = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - return context.from().id().equals(source); - } - - @Override - public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) { - return RiverTerminalPolicy.DRY; - } - }; - assertEquals(RiverRouteState.DRY, optionalOcean.trace(source, dryTerminal).state()); - } - - @Test - public void sourceAndReachPoliciesGateWholeGraphEvents() { - RiverNetwork sourceNetwork = new RiverNetwork(options(5L).build()); - RiverRoute sourceSuppressed = sourceNetwork.trace(new RiverNodeId(0L, 0L), new TestTerrain(false) { - @Override - public double sourceChanceMultiplier(int blockX, int blockZ) { - return 0.0; - } - }); - assertEquals(RiverRouteState.SUPPRESSED, sourceSuppressed.state()); - assertTrue(sourceSuppressed.edges().isEmpty()); - - RiverNetwork reachNetwork = new RiverNetwork(options(5L).reachChance(0.0).build()); - RiverRoute reachSuppressed = reachNetwork.trace(new RiverNodeId(0L, 0L), slopedTerrain(false)); - assertEquals(RiverRouteState.SUPPRESSED, reachSuppressed.state()); - assertTrue(reachSuppressed.edges().isEmpty()); - - RiverTerrainSampler blocked = new TestTerrain(false) { - @Override - public boolean allowsRiver(int blockX, int blockZ) { - return blockX < 0; - } - }; - RiverNode blockedNode = sourceNetwork.nodeAtCell(0L, 0L, blocked); - assertFalse(blockedNode.riverAllowed()); - assertEquals(null, sourceNetwork.downstream(blockedNode.id(), blocked)); - } - - @Test - public void impossibleSourceRollsSkipExpensiveTerrainSettings() { - int[] sampledSettings = new int[1]; - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double maximumSourceChanceMultiplier() { - return 0D; - } - - @Override - public double sourceChanceMultiplier(int blockX, int blockZ) { - sampledSettings[0]++; - return 1D; - } - - @Override - public double naturalHeight(int blockX, int blockZ) { - throw new AssertionError("A source rejected by its upper bound must not sample terrain"); - } - }; - RiverNetwork network = new RiverNetwork(options(53L) - .sourceChance(0.05D) - .minimumSourcesPerTile(0) - .build()); - - RiverRoute route = network.trace(new RiverNodeId(0L, 0L), terrain); - - assertEquals(RiverRouteState.SUPPRESSED, route.state()); - assertTrue(route.edges().isEmpty()); - assertEquals(0, sampledSettings[0]); - } - - @Test - public void minimumSourceFloorKeepsEligibleRoutingTilesActive() { - RiverNetwork network = new RiverNetwork(options(52L) - .maxRouteReaches(4) - .minimumSourcesPerTile(1) - .sourceChance(0.000001D) - .requireOcean(false) - .build()); - RiverTerrainSampler terrain = slopedTerrain(false); - - for (int tileX = -2; tileX <= 2; tileX++) { - for (int tileZ = -2; tileZ <= 2; tileZ++) { - assertFalse(network.buildTile(tileX, tileZ, terrain).reaches().isEmpty()); - } - } - - RiverNetwork disabled = new RiverNetwork(options(52L) - .maxRouteReaches(4) - .minimumSourcesPerTile(1) - .sourceChance(0D) - .requireOcean(false) - .build()); - assertTrue(disabled.buildTile(0, 0, terrain).reaches().isEmpty()); - } - - @Test - public void minimumSourceFloorDoesNotResolveEveryRandomCandidate() { - int[] sampledSettings = new int[1]; - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double maximumSourceChanceMultiplier() { - return 1D; - } - - @Override - public double sourceChanceMultiplier(int blockX, int blockZ) { - sampledSettings[0]++; - return 1D; - } - }; - RiverNetwork network = new RiverNetwork(options(54L) - .maxRouteReaches(1) - .minimumSourcesPerTile(1) - .sourceChance(0.000000001D) - .requireOcean(false) - .build()); - - RiverTile tile = network.buildTile(0, 0, terrain); - - assertFalse(tile.reaches().isEmpty()); - assertTrue(sampledSettings[0] <= 25); - } - - @Test - public void minimumSourceFloorRejectsNonFiniteLocalChanceMultipliers() { - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double sourceChanceMultiplier(int blockX, int blockZ) { - return Double.NaN; - } - }; - RiverNetwork network = new RiverNetwork(options(55L) - .maxRouteReaches(4) - .minimumSourcesPerTile(1) - .sourceChance(1D) - .requireOcean(false) - .build()); - - assertTrue(network.buildTile(0, 0, terrain).reaches().isEmpty()); - } - - @Test - public void tileBuildIsDeterministicUnderParallelEvaluation() throws Exception { - RiverNetwork network = new RiverNetwork(options(6L).build()); - RiverTerrainSampler terrain = slopedTerrain(false); - long expected = digest(network.buildTile(-1, 0, terrain)); - ExecutorService executor = Executors.newFixedThreadPool(4); - try { - ArrayList> tasks = new ArrayList<>(); - for (int task = 0; task < 12; task++) { - tasks.add(() -> digest(network.buildTile(-1, 0, terrain))); - } - List> results = executor.invokeAll(tasks); - for (Future result : results) { - assertEquals(expected, result.get().longValue()); - } - } finally { - executor.shutdownNow(); - } - } - - @Test - public void tileBuildPreservesPinnedOutputDigests() { - RiverTerrainSampler terrain = slopedTerrain(false); - List actual = List.of( - digest(new RiverNetwork(options(6L).build()).buildTile(-1, 0, terrain)), - digest(new RiverNetwork(options(6L).build()).buildTile(0, 0, terrain)), - digest(new RiverNetwork(options(6L).build()).buildTile(1, 1, terrain)), - digest(new RiverNetwork(options(61L).build()).buildTile(-2, -1, terrain)) - ); - - assertEquals(List.of( - 973031325888677478L, - 8932177974453767311L, - -5189009084208004632L, - 6374264141259432071L - ), actual); - } - - @Test - public void adjacentTilesShareIdenticalPinnedReachGeometry() { - RiverNetwork network = new RiverNetwork(options(7L).build()); - RiverTerrainSampler terrain = slopedTerrain(false); - RiverTile first = network.buildTile(0, 0, terrain); - RiverTile second = network.buildTile(1, 0, terrain); - HashMap secondById = new HashMap<>(); - for (RiverReach reach : second.reaches()) { - secondById.put(reach.id(), reach); - } - int sharedCount = 0; - for (RiverReach candidate : first.reaches()) { - RiverReach matching = secondById.get(candidate.id()); - if (matching == null) { - continue; - } - sharedCount++; - assertReachEquals(candidate, matching); - assertEquals(candidate.from().x(), candidate.polyline().x(0), 0.0); - assertEquals(candidate.from().z(), candidate.polyline().z(0), 0.0); - int last = candidate.polyline().size() - 1; - assertEquals(candidate.to().x(), candidate.polyline().x(last), 0.0); - assertEquals(candidate.to().z(), candidate.polyline().z(last), 0.0); - } - assertTrue(sharedCount > 0); - } - - @Test - public void geometryEnvelopeKeepsWideReachesIdenticalAcrossTileBoundaries() { - RiverNetwork network = new RiverNetwork(options(71L) - .maxRouteReaches(1) - .siteJitter(0D) - .maximumReachRadius(256D) - .channelWidth(512D) - .maxChannelWidth(512D) - .bankWidth(0D) - .worms(List.of(wormProfile( - "straight", - 1L, - 1, - 1D, - 1D, - 1D, - 8, - 1D, - 1D, - 0D, - 0D, - List.of() - ))) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public double sourceChanceMultiplier(int blockX, int blockZ) { - return blockX == -96 ? 1D : 0D; - } - }; - RiverTile west = network.buildTile(0, 0, terrain); - RiverTile east = network.buildTile(1, 0, terrain); - - boolean present = false; - for (int z = 0; z < 128; z++) { - RiverSample westSample = west.sample(128D, z); - RiverSample eastSample = east.sample(128D, z); - assertEquals(westSample, eastSample); - present |= westSample.present(); - } - assertTrue(present); - } - - @Test - public void mergedReachesIncreaseFlowAndOrderAndCanBeSampled() { - RiverNetwork network = new RiverNetwork(options(8L).sourceChance(1.0).build()); - RiverTile tile = network.buildTile(0, 0, slopedTerrain(false)); - boolean merged = false; - boolean sampled = false; - - for (RiverReach reach : tile.reaches()) { - assertEquals(1 + (31 - Integer.numberOfLeadingZeros(reach.flow())), reach.order()); - if (reach.flow() > 1) { - merged = true; - } - int middle = reach.polyline().size() / 2; - RiverSample sample = tile.sample(reach.polyline().x(middle), reach.polyline().z(middle)); - if (sample.present()) { - sampled = true; - assertTrue(sample.carveWeight() > 0.0); - assertTrue(sample.alongReach() >= 0.0 && sample.alongReach() <= 1.0); - } - } - - assertTrue(merged); - assertTrue(sampled); - assertThrows(UnsupportedOperationException.class, () -> tile.reaches().add(tile.reaches().getFirst())); - } - - @Test - public void candidateAnchorsAreStableUniqueAndOwnedByOneTile() { - RiverNetwork network = new RiverNetwork(options(9L).build()); - RiverTile tile = network.buildTile(-1, -1, slopedTerrain(false)); - List first = tile.candidateAnchors(24.0, 77L); - List second = tile.candidateAnchors(24.0, 77L); - Set identities = new HashSet<>(); - - assertEquals(first, second); - for (RiverAnchor anchor : first) { - assertTrue(identities.add(anchor.stableId())); - assertTrue(anchor.x() >= tile.minimumX() && anchor.x() < tile.maximumX()); - assertTrue(anchor.z() >= tile.minimumZ() && anchor.z() < tile.maximumZ()); - assertTrue(anchor.alongReach() >= 0.0 && anchor.alongReach() <= 1.0); - } - assertThrows(UnsupportedOperationException.class, () -> first.add(first.getFirst())); - } - - @Test - public void spatialIndexReducesColumnCandidatesAndBoundsAnchorQueries() { - RiverNetwork network = new RiverNetwork(options(51L).tileCells(8).build()); - RiverTile tile = network.buildTile(0, 0, slopedTerrain(false)); - int minimumCandidates = Integer.MAX_VALUE; - for (int x = tile.minimumX() + 32; x < tile.maximumX(); x += 64) { - for (int z = tile.minimumZ() + 32; z < tile.maximumZ(); z += 64) { - minimumCandidates = StrictMath.min(minimumCandidates, tile.sampleCandidateCount(x, z)); - } - } - assertTrue(tile.reaches().size() > 8); - assertTrue(minimumCandidates < tile.reaches().size() / 2); - - double queryMaximumX = tile.minimumX() + 64.0; - double queryMaximumZ = tile.minimumZ() + 64.0; - List anchors = tile.candidateAnchors( - tile.minimumX(), - tile.minimumZ(), - queryMaximumX, - queryMaximumZ, - 12.0, - 12L - ); - for (RiverAnchor anchor : anchors) { - assertTrue(anchor.x() >= tile.minimumX() && anchor.x() < queryMaximumX); - assertTrue(anchor.z() >= tile.minimumZ() && anchor.z() < queryMaximumZ); - } - } - - @Test - public void sampleReportsNormalizedClosestPositionAndTerminalMetadata() { - RiverNode from = new RiverNode( - new RiverNodeId(0L, 0L), - 0.0, - 0.0, - 20.0, - 20.0, - 20.0, - 20.0, - false, - true - ); - RiverNode to = new RiverNode( - new RiverNodeId(1L, 0L), - 100.0, - 0.0, - 10.0, - 10.0, - 10.0, - 10.0, - false, - true - ); - RiverReach reach = new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.DRY, - 1, - 1, - 10.0, - 5.0, - 3.0, - RiverBodyProfile.constant(10.0, 5.0, 3.0), - false, - true, - new RiverPolyline(new double[]{0.0, 100.0}, new double[]{0.0, 0.0}) - ); - RiverTile tile = new RiverTile(0, 0, 0, -32, 128, 32, List.of(reach)); - - RiverSample sample = tile.sample(75.0, 0.0); - assertTrue(sample.present()); - assertEquals(0.75, sample.alongReach(), 0.0000001); - assertEquals(RiverSection.DRY_CHANNEL, sample.section()); - assertTrue(sample.terminal()); - } - - @Test - public void uncoveredNarrowReachDoesNotMaskCoveringWideReach() { - RiverNode narrowFrom = node(0L, 0L, 0D, 0D); - RiverNode narrowTo = node(1L, 0L, 100D, 0D); - RiverNode wideFrom = node(0L, 1L, 0D, 4D); - RiverNode wideTo = node(1L, 1L, 100D, 4D); - RiverReach narrow = reach(narrowFrom, narrowTo, 2D, 0D); - RiverReach wide = reach(wideFrom, wideTo, 8D, 0D); - RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(narrow, wide)); - - RiverSample sample = tile.sample(50D, 1.5D); - - assertTrue(sample.present()); - assertEquals(wide.id(), sample.reachId()); - } - - @Test - public void footprintSamplingFindsChannelThatPointSamplingMisses() { - RiverNode from = node(0L, 0L, 0D, 0D); - RiverNode to = node(1L, 0L, 100D, 0D); - RiverReach reach = reach(from, to, 2D, 0D); - RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach)); - - assertFalse(tile.sample(50D, 2.5D).present()); - RiverSample sample = tile.sampleFootprint(48D, 0.5D, 52D, 4.5D); - - assertTrue(sample.present()); - assertEquals(RiverSection.CHANNEL, sample.section()); - assertEquals(0.5D, sample.distance(), 0D); - } - - @Test - public void footprintSamplingFindsBankThatPointSamplingMisses() { - RiverNode from = node(0L, 0L, 0D, 0D); - RiverNode to = node(1L, 0L, 100D, 0D); - RiverReach reach = reach(from, to, 2D, 3D); - RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach)); - - assertFalse(tile.sample(50D, 5D).present()); - RiverSample sample = tile.sampleFootprint(48D, 2D, 52D, 8D); - - assertTrue(sample.present()); - assertEquals(RiverSection.BANK, sample.section()); - assertEquals(2D, sample.distance(), 0D); - } - - @Test - public void footprintSamplingRejectsBoundingBoxOnlyCornerOverlap() { - RiverNode from = node(0L, 0L, 0D, 0D); - RiverNode to = node(1L, 0L, 100D, 0D); - RiverReach reach = reach(from, to, 1D, 2D); - RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach)); - - RiverSample sample = tile.sampleFootprint(101.8D, 1.8D, 104D, 4D); - - assertFalse(sample.present()); - } - - @Test - public void terrainSamplerDimensionsAreClampedAfterOrderScaling() { - RiverNetwork network = new RiverNetwork(options(61L) - .maxChannelWidth(10D) - .maxBankWidth(3D) - .maxDepth(6D) - .worms(List.of(worm(1L, 0D, 0D, 0D, 1))) - .build()); - RiverTerrainSampler styled = new TestTerrain(false) { - @Override - public double channelWidth(RiverRoutingContext context, double fallback) { - return 20.0; - } - - @Override - public double bankWidth(RiverRoutingContext context, double fallback) { - return 9.0; - } - - @Override - public double depth(RiverRoutingContext context, double fallback) { - return 7.0; - } - }; - RiverTile tile = network.buildTile(0, 0, styled); - RiverReach reach = tile.reaches().getFirst(); - double deltaX = reach.to().x() - reach.from().x(); - double deltaZ = reach.to().z() - reach.from().z(); - - assertEquals(10D, reach.width(), 0D); - assertEquals(3D, reach.bankWidth(), 0D); - assertEquals(6D, reach.depth(), 0D); - for (int point = 0; point < reach.polyline().size(); point++) { - double pointDeltaX = reach.polyline().x(point) - reach.from().x(); - double pointDeltaZ = reach.polyline().z(point) - reach.from().z(); - assertEquals(0.0, pointDeltaX * deltaZ - pointDeltaZ * deltaX, 0.0000001); - } - } - - @Test - public void bodyAnatomyVariesInsideOneReachAndRemainsSpatiallyQueryable() { - RiverNode from = node(0L, 0L, 0D, 0D); - RiverNode to = node(1L, 0L, 100D, 0D); - RiverBodyProfile profile = new RiverBodyProfile( - new double[]{0D, 0.5D, 1D}, - new double[]{1D, 7D, 1D}, - new double[]{1D, 9D, 1D}, - new double[]{2D, 8D, 2D}, - new double[]{1D, 0.35D, 1D} - ); - RiverReach reach = new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.WET, - 1, - 1, - 7D, - 9D, - 8D, - profile, - false, - false, - new RiverPolyline(new double[]{0D, 100D}, new double[]{0D, 0D}) - ); - RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach)); - - RiverSample narrow = tile.sample(10D, 7D); - RiverSample swollen = tile.sample(50D, 7D); - - assertFalse(narrow.present()); - assertTrue(swollen.present()); - assertEquals(7D, swollen.width(), 0D); - assertEquals(9D, swollen.bankWidth(), 0D); - assertEquals(8D, swollen.depth(), 0D); - assertEquals(0.35D, reach.roofScaleAt(swollen.alongReach()), 0.0000001D); - assertEquals(RiverSection.BANK, swollen.section()); - } - - @Test - public void perlinWormBodyAnatomyVariesAllDimensionsDeterministically() { - RiverWorm anatomy = wormProfile( - "anatomy", - 917L, - 24, - 1D, - 1D, - 1D, - 64D, - 32D, - 0.875D, - 0.875D, - 0.875D, - 0.875D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - ); - RiverNetwork network = new RiverNetwork(options(917L) - .requireOcean(false) - .maxChannelWidth(64D) - .maxBankWidth(64D) - .maxDepth(64D) - .maximumReachRadius(96D) - .worms(List.of(anatomy)) - .build()); - RiverTerrainSampler terrain = flatTerrain(false); - - RiverTile first = network.buildTile(0, 0, terrain); - RiverTile second = network.buildTile(0, 0, terrain); - - assertFalse(first.reaches().isEmpty()); - assertEquals(digest(first), digest(second)); - double minimumWidth = Double.POSITIVE_INFINITY; - double maximumWidth = 0D; - double minimumBank = Double.POSITIVE_INFINITY; - double maximumBank = 0D; - double minimumDepth = Double.POSITIVE_INFINITY; - double maximumDepth = 0D; - double minimumRoof = Double.POSITIVE_INFINITY; - double maximumRoof = 0D; - for (RiverReach reach : first.reaches()) { - for (int index = 0; index < reach.bodyProfile().size(); index++) { - minimumWidth = StrictMath.min(minimumWidth, reach.bodyProfile().widthAtIndex(index)); - maximumWidth = StrictMath.max(maximumWidth, reach.bodyProfile().widthAtIndex(index)); - minimumBank = StrictMath.min(minimumBank, reach.bodyProfile().bankWidthAtIndex(index)); - maximumBank = StrictMath.max(maximumBank, reach.bodyProfile().bankWidthAtIndex(index)); - minimumDepth = StrictMath.min(minimumDepth, reach.bodyProfile().depthAtIndex(index)); - maximumDepth = StrictMath.max(maximumDepth, reach.bodyProfile().depthAtIndex(index)); - minimumRoof = StrictMath.min(minimumRoof, reach.bodyProfile().roofScaleAtIndex(index)); - maximumRoof = StrictMath.max(maximumRoof, reach.bodyProfile().roofScaleAtIndex(index)); - } - } - assertTrue(maximumWidth - minimumWidth > 2D); - assertTrue(maximumBank - minimumBank > 1.5D); - assertTrue(maximumDepth - minimumDepth > 0.75D); - assertTrue(maximumRoof - minimumRoof > 0.1D); - } - - @Test - public void volatileBodyProfilesResolveSubTenBlockThicknessChanges() { - RiverWorm volatileBody = new RiverWorm( - "volatile-body", - 4815L, - 1D, - 1024D, - 128D, - 0.65D, - 0.2D, - 180D, - 48, - 1D, - 1D, - 1D, - 24D, - 8D, - 0.95D, - 0.875D, - 0.75D, - 0.75D, - 0.75D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - ); - RiverNetwork network = new RiverNetwork(options(4815L) - .cellSize(1700) - .tileCells(1) - .siteJitter(0D) - .requireOcean(false) - .channelWidth(14D) - .maxChannelWidth(38D) - .maximumReachRadius(64D) - .worms(List.of(volatileBody)) - .build()); - - RiverReach reach = network.buildTile(0, 0, flatTerrain(false)).reaches().getFirst(); - RiverBodyProfile profile = reach.bodyProfile(); - double maximumStationSpacing = 0D; - for (int index = 1; index < profile.size(); index++) { - maximumStationSpacing = StrictMath.max( - maximumStationSpacing, - reach.polyline().length() * (profile.position(index) - profile.position(index - 1)) - ); - } - boolean volatileChangeFound = false; - for (double distance = 0D; distance + 10D <= reach.polyline().length(); distance += 2D) { - double start = distance / reach.polyline().length(); - double end = (distance + 10D) / reach.polyline().length(); - if (StrictMath.abs(profile.width(start) - profile.width(end)) >= 2D) { - volatileChangeFound = true; - break; - } - } - - assertTrue(maximumStationSpacing <= 10D); - assertTrue(volatileChangeFound); - } - - @Test - public void channelRadiusBonusAddsThreeBlocksPerSideAfterShaping() { - RiverWorm constantBody = wormProfile( - "radius-bonus", - 6501L, - 8, - 1D, - 1D, - 1D, - 64D, - 16D, - 0D, - 0D, - 0D, - 0D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - ); - RiverNetwork baseline = new RiverNetwork(options(6501L) - .siteJitter(0D) - .requireOcean(false) - .channelWidth(10D) - .maxChannelWidth(38D) - .maximumReachRadius(64D) - .worms(List.of(constantBody)) - .build()); - RiverNetwork expanded = new RiverNetwork(options(6501L) - .siteJitter(0D) - .requireOcean(false) - .channelWidth(10D) - .channelRadiusBonus(3D) - .maxChannelWidth(38D) - .maximumReachRadius(64D) - .worms(List.of(constantBody)) - .build()); - - RiverReach baselineReach = baseline.buildTile(0, 0, flatTerrain(false)).reaches().getFirst(); - RiverReach expandedReach = expanded.buildTile(0, 0, flatTerrain(false)).reaches().getFirst(); - - assertEquals(baselineReach.id(), expandedReach.id()); - assertEquals(6D, expandedReach.bodyProfile().width(0.5D) - - baselineReach.bodyProfile().width(0.5D), 0.0000001D); - } - - @Test - public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() { - RiverNode from = node(0L, 0L, 0D, 0D); - RiverNode to = node(1L, 0L, 0D, 10D); - RiverBodyProfile profile = new RiverBodyProfile( - new double[]{0D, 0.48D, 0.53D, 1D}, - new double[]{1D, 1D, 18D, 18D}, - new double[]{0D, 0D, 0D, 0D}, - new double[]{3D, 3D, 3D, 3D}, - new double[]{1D, 1D, 1D, 1D} - ); - RiverReach reach = new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.WET, - 1, - 1, - 18D, - 0D, - 3D, - profile, - false, - false, - new RiverPolyline( - new double[]{0D, 100D, 100D, 0D}, - new double[]{0D, 0D, 10D, 10D} - ) - ); - RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach)); - - RiverSample point = tile.sample(50D, 2D); - RiverSample footprint = tile.sampleFootprint(49D, 1.5D, 51D, 2.5D); - - assertTrue(point.present()); - assertTrue(point.alongReach() > 0.53D); - assertEquals(18D, point.width(), 0.000001D); - assertTrue(footprint.present()); - assertTrue(footprint.alongReach() > 0.53D); - } - - @Test - public void reachFeasibilityReceivesTheFinalPinnedWormPolyline() { - RiverNetwork network = new RiverNetwork(options(62L) - .worms(List.of(worm(62L, 0.8D, 0.2D, 20D, 32))) - .build()); - boolean[] observedWorm = new boolean[1]; - RiverTerrainSampler terrain = new TestTerrain(false) { - @Override - public boolean allowsReach(RiverRoutingContext context) { - RiverPolyline polyline = context.polyline(); - double deltaX = context.to().x() - context.from().x(); - double deltaZ = context.to().z() - context.from().z(); - for (int point = 1; point < polyline.size() - 1; point++) { - double pointX = polyline.x(point) - context.from().x(); - double pointZ = polyline.z(point) - context.from().z(); - if (StrictMath.abs(pointX * deltaZ - pointZ * deltaX) > 0.000001D) { - observedWorm[0] = true; - return false; - } - } - return true; - } - }; - - RiverNode downstream = network.downstream(new RiverNodeId(0L, 0L), terrain); - - assertTrue(observedWorm[0]); - assertEquals(null, downstream); - } - - @Test - public void wormSeedChangesDeterministicGeometryWithoutBreakingItsEnvelope() { - double maximumOffset = 20D; - RiverNetwork firstNetwork = new RiverNetwork(options(63L) - .siteJitter(0D) - .requireOcean(false) - .worms(List.of(worm(101L, 0.8D, 0.2D, maximumOffset, 32))) - .build()); - RiverNetwork secondNetwork = new RiverNetwork(options(63L) - .siteJitter(0D) - .requireOcean(false) - .worms(List.of(worm(202L, 0.8D, 0.2D, maximumOffset, 32))) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false); - - RiverReach firstReach = firstNetwork.buildTile(0, 0, terrain).reaches().getFirst(); - RiverReach repeatedReach = firstNetwork.buildTile(0, 0, terrain).reaches().getFirst(); - RiverReach secondReach = secondNetwork.buildTile(0, 0, terrain).reaches().getFirst(); - - assertReachEquals(firstReach, repeatedReach); - assertEquals(firstReach.id(), secondReach.id()); - assertTrue(polylinesDiffer(firstReach.polyline(), secondReach.polyline())); - assertWormBounds(firstReach, maximumOffset); - assertWormBounds(secondReach, maximumOffset); - } - - @Test - public void weightedWormProfilesSelectDistinctConfigurableVariants() { - RiverWorm gentle = new RiverWorm( - "gentle", 301L, 1D, 1024D, 256D, 0.2D, 0.05D, 10D, 8, 0.5D, 0.5D, 0.5D, - 512D, 128D, 0.3D, 0D, 0D, 0D, 0D, - 4, 0.35D, 1D, 0D, 0D, List.of() - ); - RiverWorm winding = new RiverWorm( - "winding", 302L, 1D, 512D, 128D, 0.55D, 0.15D, 20D, 16, 1D, 1D, 1D, - 512D, 128D, 0.3D, 0D, 0D, 0D, 0D, - 4, 0.35D, 1D, 0D, 0D, List.of() - ); - RiverWorm restless = new RiverWorm( - "restless", 303L, 1D, 192D, 48D, 0.9D, 0.35D, 30D, 32, 2D, 2D, 2D, - 512D, 128D, 0.3D, 0D, 0D, 0D, 0D, - 4, 0.35D, 1D, 0D, 0D, List.of() - ); - RiverNetwork network = new RiverNetwork(options(64L) - .siteJitter(0D) - .routingBasinCells(8) - .requireOcean(false) - .maxChannelWidth(32D) - .maxBankWidth(32D) - .maxDepth(32D) - .orderWidthFactor(0D) - .orderDepthFactor(0D) - .maximumReachRadius(32D) - .worms(List.of(gentle, winding, restless)) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false); - - HashMap uniqueReaches = new HashMap<>(); - for (int tileX = -2; tileX <= 2; tileX++) { - for (int tileZ = -2; tileZ <= 2; tileZ++) { - for (RiverReach reach : network.buildTile(tileX, tileZ, terrain).reaches()) { - uniqueReaches.putIfAbsent(reach.id(), reach); - } - } - } - Set selectedPointCounts = new HashSet<>(); - HashMap maximumDisplacementRatios = new HashMap<>(); - for (RiverReach reach : uniqueReaches.values()) { - int pointCount = reach.polyline().size(); - selectedPointCounts.add(pointCount); - RiverWorm selected = switch (pointCount) { - case 9 -> gentle; - case 17 -> winding; - case 33 -> restless; - default -> throw new AssertionError("Unexpected worm point count " + pointCount); - }; - assertEquals(8D * selected.widthMultiplier(), reach.width(), 0.0000001D); - assertEquals(6D * selected.bankMultiplier(), reach.bankWidth(), 0.0000001D); - assertEquals(3D * selected.depthMultiplier(), reach.depth(), 0.0000001D); - assertWormBounds(reach, selected.maxOffset()); - maximumDisplacementRatios.merge( - pointCount, - maximumWormDisplacement(reach) / selected.maxOffset(), - StrictMath::max - ); - } - assertEquals(Set.of(9, 17, 33), selectedPointCounts); - assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(9) > 0.03D); - assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(17) > 0.18D); - assertTrue(maximumDisplacementRatios.toString(), maximumDisplacementRatios.get(33) > 0.4D); - } - - @Test - public void forcedChildHierarchyControlsReachGeometryAndDimensionsDeterministically() { - RiverWorm child = wormProfile( - "child", - 402L, - 24, - 2D, - 1.5D, - 0.5D, - 8, - 1D, - 1D, - 0D, - 0D, - List.of() - ); - RiverWorm root = wormProfile( - "root", - 401L, - 8, - 0.5D, - 0.5D, - 0.5D, - 8, - 1D, - 1D, - 1D, - 0D, - List.of(child) - ); - RiverNetwork network = new RiverNetwork(options(68L) - .siteJitter(0D) - .requireOcean(false) - .maxChannelWidth(32D) - .maxBankWidth(32D) - .maxDepth(32D) - .orderWidthFactor(0D) - .orderDepthFactor(0D) - .maximumReachRadius(32D) - .worms(List.of(root)) - .build()); - RiverTerrainSampler terrain = new TestTerrain(false); - - RiverTile first = network.buildTile(0, 0, terrain); - RiverTile second = network.buildTile(0, 0, terrain); - - assertFalse(first.reaches().isEmpty()); - assertEquals(digest(first), digest(second)); - for (RiverReach reach : first.reaches()) { - assertEquals(child.segments() + 1, reach.polyline().size()); - assertEquals(8D * child.widthMultiplier(), reach.width(), 0.0000001D); - assertEquals(6D * child.bankMultiplier(), reach.bankWidth(), 0.0000001D); - assertEquals(3D * child.depthMultiplier(), reach.depth(), 0.0000001D); - assertWormBounds(reach, child.maxOffset()); - } - } - - @Test - public void wormHierarchyRejectsInvalidIdsDepthAndCounts() { - assertThrows(IllegalArgumentException.class, () -> wormProfile( - "Invalid", - 1L, - 8, - 1D, - 1D, - 1D, - 512D, - 128D, - 0D, - 0D, - 0D, - 0D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - )); - - RiverWorm duplicateChild = wormProfile( - "duplicate", 2L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 0D, List.of() - ); - RiverWorm duplicateRoot = wormProfile( - "duplicate", 3L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 1D, 0D, List.of(duplicateChild) - ); - assertThrows( - IllegalArgumentException.class, - () -> options(10L).worms(List.of(duplicateRoot)).build() - ); - - RiverWorm tooDeep = wormProfile( - "depth-5", 5L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 0D, List.of() - ); - for (int depth = 4; depth >= 1; depth--) { - tooDeep = wormProfile( - "depth-" + depth, - depth, - 8, - 1D, - 1D, - 1D, - 4, - 0.35D, - 1D, - 1D, - 0D, - List.of(tooDeep) - ); - } - RiverWorm depthRoot = tooDeep; - assertThrows( - IllegalArgumentException.class, - () -> options(10L).worms(List.of(depthRoot)).build() - ); - - ArrayList roots = new ArrayList<>(); - for (int root = 0; root < 17; root++) { - roots.add(wormProfile( - "root-" + root, root, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 0D, List.of() - )); - } - assertThrows(IllegalArgumentException.class, () -> options(10L).worms(roots).build()); - - ArrayList branches = new ArrayList<>(); - for (int branch = 0; branch < 8; branch++) { - ArrayList leaves = new ArrayList<>(); - for (int leaf = 0; leaf < 16; leaf++) { - leaves.add(wormProfile( - "leaf-" + branch + "-" + leaf, - branch * 16L + leaf, - 8, - 1D, - 1D, - 1D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - )); - } - branches.add(wormProfile( - "branch-" + branch, - branch, - 8, - 1D, - 1D, - 1D, - 4, - 0.35D, - 1D, - 1D, - 0D, - leaves - )); - } - RiverWorm oversized = wormProfile( - "oversized", 900L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 1D, 0D, branches - ); - assertThrows( - IllegalArgumentException.class, - () -> options(10L).worms(List.of(oversized)).build() - ); - } - - @Test - public void wormProfileRejectsInvalidBranchControls() { - assertThrows(IllegalArgumentException.class, () -> wormProfile( - "cap", 1L, 8, 1D, 1D, 1D, 0, 0.35D, 1D, 0D, 0D, List.of() - )); - assertThrows(IllegalArgumentException.class, () -> wormProfile( - "decay", 1L, 8, 1D, 1D, 1D, 4, 1.1D, 1D, 0D, 0D, List.of() - )); - assertThrows(IllegalArgumentException.class, () -> wormProfile( - "confluence", 1L, 8, 1D, 1D, 1D, 4, 0.35D, 8.1D, 0D, 0D, List.of() - )); - assertThrows(IllegalArgumentException.class, () -> wormProfile( - "child-chance", 1L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 1.1D, 0D, List.of() - )); - assertThrows(IllegalArgumentException.class, () -> wormProfile( - "sibling-chance", 1L, 8, 1D, 1D, 1D, 4, 0.35D, 1D, 0D, 1.1D, List.of() - )); - } - - @Test - public void drainageDomainWarpChangesRoutesDeterministically() { - RiverTerrainSampler terrain = flatTerrain(false); - RiverNetwork straight = new RiverNetwork(options(65L) - .requireOcean(false) - .routingDeviationStrengthCells(0D) - .build()); - RiverNetwork deviated = new RiverNetwork(options(65L) - .requireOcean(false) - .routingDeviationScaleCells(8) - .routingDeviationStrengthCells(4D) - .build()); - boolean changed = false; - for (long cellX = -8L; cellX <= 8L; cellX++) { - for (long cellZ = -8L; cellZ <= 8L; cellZ++) { - RiverNode straightDownstream = straight.downstream(new RiverNodeId(cellX, cellZ), terrain); - RiverNode deviatedDownstream = deviated.downstream(new RiverNodeId(cellX, cellZ), terrain); - if (!Objects.equals( - straightDownstream == null ? null : straightDownstream.id(), - deviatedDownstream == null ? null : deviatedDownstream.id() - )) { - changed = true; - } - RiverNode repeated = deviated.downstream(new RiverNodeId(cellX, cellZ), terrain); - assertEquals( - deviatedDownstream == null ? null : deviatedDownstream.id(), - repeated == null ? null : repeated.id() - ); - } - } - assertTrue(changed); - } - - @Test - public void confluenceAttractionChangesDownstreamChoicesDeterministically() { - RiverTerrainSampler terrain = flatTerrain(false); - RiverNetwork dispersed = new RiverNetwork(options(67L) - .requireOcean(false) - .confluenceWeight(0D) - .build()); - RiverNetwork branching = new RiverNetwork(options(67L) - .requireOcean(false) - .confluenceWeight(512D) - .build()); - boolean changed = false; - for (long cellX = -8L; cellX <= 8L; cellX++) { - for (long cellZ = -8L; cellZ <= 8L; cellZ++) { - RiverNodeId id = new RiverNodeId(cellX, cellZ); - RiverNode dispersedDownstream = dispersed.downstream(id, terrain); - RiverNode branchingDownstream = branching.downstream(id, terrain); - if (!Objects.equals( - dispersedDownstream == null ? null : dispersedDownstream.id(), - branchingDownstream == null ? null : branchingDownstream.id() - )) { - changed = true; - } - RiverNode repeated = branching.downstream(id, terrain); - assertEquals( - branchingDownstream == null ? null : branchingDownstream.id(), - repeated == null ? null : repeated.id() - ); - } - } - assertTrue(changed); - } - - @Test - public void optionsRejectNonFiniteOrUnsafeValues() { - assertThrows(IllegalArgumentException.class, () -> options(10L).sourceChance(Double.NaN).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L).cellSize(0).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L).routingBasinCells(7).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L).routingPlateauHeight(0D).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L).hydraulicBaseHeight(Double.NaN).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L).confluenceWeight(-1D).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L).channelWidth(-1.0).build()); - assertThrows(IllegalArgumentException.class, () -> options(10L) - .tileCells(1) - .minimumSourcesPerTile(2) - .build()); - } - - @Test - public void derivedComplexityAllowsDefaultsAndRejectsExpensiveTopologies() { - double defaultRadius = 10D * 0.5D + 4D; - RiverTopologyComplexity.Estimate defaults = RiverTopologyComplexity.estimate( - 768, - 4, - 0.35D, - 4, - defaultRadius, - 72D, - 8 - ); - assertTrue(defaults.violations().toString(), defaults.safe()); - assertEquals(2L, defaults.geometryPaddingCells()); - assertEquals(16L, defaults.sourceWindowAxis()); - assertEquals(256L, defaults.sourceWindowCells()); - assertEquals(1_024L, defaults.maximumRouteScanSteps()); - - double expensiveRadius = defaultRadius; - RiverTopologyComplexity.Estimate expensive = RiverTopologyComplexity.estimate( - 768, - 4, - 0.35D, - 48, - expensiveRadius, - 72D, - 8 - ); - assertFalse(expensive.safe()); - assertTrue(expensive.violations().toString(), expensive.maximumRouteScanSteps() - > RiverTopologyComplexity.MAXIMUM_ROUTE_SCAN_STEPS); - - double pathologicalRadius = 2048D * 0.5D + 2048D; - RiverTopologyComplexity.Estimate pathological = RiverTopologyComplexity.estimate( - 64, - 64, - 0.49D, - 256, - pathologicalRadius, - 1024D, - 64 - ); - assertFalse(pathological.safe()); - assertTrue(pathological.sourceWindowCells() > 400_000L); - IllegalArgumentException failure = assertThrows( - IllegalArgumentException.class, - () -> options(10L) - .cellSize(64) - .tileCells(64) - .siteJitter(0.49D) - .maxRouteReaches(256) - .maximumReachRadius(pathologicalRadius) - .worms(List.of(worm(1L, 1D, 1D, 1024D, 64))) - .build() - ); - assertTrue(failure.getMessage(), failure.getMessage().contains("source window")); - } - - private static RiverNetworkOptions.Builder options(long seed) { - return RiverNetworkOptions.builder(seed) - .cellSize(64) - .tileCells(2) - .siteJitter(0.25) - .maxRouteReaches(12) - .sourceChance(1.0) - .reachChance(1.0) - .dryChannelChance(1.0) - .requireOcean(true) - .terrainHeightWeight(1.0) - .routingNoiseWeight(0.0) - .oceanAttraction(256.0) - .channelWidth(8.0) - .bankWidth(6.0) - .depth(3.0) - .worms(List.of(worm(1L, 0.5D, 0.15D, 12D, 6))); - } - - private static RiverWorm worm( - long seed, - double tortuosity, - double detailTortuosity, - double maximumOffset, - int segments - ) { - return new RiverWorm( - "worm-" + Long.toUnsignedString(seed), - seed, - 1D, - 1024D, - 256D, - tortuosity, - detailTortuosity, - maximumOffset, - segments, - 1D, - 1D, - 1D, - 512D, - 128D, - 0.3D, - 0D, - 0D, - 0D, - 0D, - 4, - 0.35D, - 1D, - 0D, - 0D, - List.of() - ); - } - - private static RiverWorm wormProfile( - String id, - long seed, - int segments, - double widthMultiplier, - double bankMultiplier, - double depthMultiplier, - int branchCap, - double branchDecay, - double confluenceMultiplier, - double childChance, - double branchChildChance, - List children - ) { - return wormProfile( - id, - seed, - segments, - widthMultiplier, - bankMultiplier, - depthMultiplier, - 512D, - 128D, - 0D, - 0D, - 0D, - 0D, - branchCap, - branchDecay, - confluenceMultiplier, - childChance, - branchChildChance, - children - ); - } - - private static RiverWorm wormProfile( - String id, - long seed, - int segments, - double widthMultiplier, - double bankMultiplier, - double depthMultiplier, - double bodyWavelength, - double bodyDetailWavelength, - double widthVariation, - double bankVariation, - double depthVariation, - double roofVariation, - int branchCap, - double branchDecay, - double confluenceMultiplier, - double childChance, - double branchChildChance, - List children - ) { - return new RiverWorm( - id, - seed, - 1D, - 256D, - 64D, - 0.7D, - 0.2D, - 32D, - segments, - widthMultiplier, - bankMultiplier, - depthMultiplier, - bodyWavelength, - bodyDetailWavelength, - 0.3D, - widthVariation, - bankVariation, - depthVariation, - roofVariation, - branchCap, - branchDecay, - confluenceMultiplier, - childChance, - branchChildChance, - children - ); - } - - private static RiverNode node(long cellX, long cellZ, double x, double z) { - return new RiverNode( - new RiverNodeId(cellX, cellZ), - x, - z, - 64D, - 64D, - 64D, - 64D, - false, - true - ); - } - - private static RiverReach reach(RiverNode from, RiverNode to, double width, double bankWidth) { - return new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.WET, - 1, - 1, - width, - bankWidth, - 3D, - RiverBodyProfile.constant(width, bankWidth, 3D), - false, - false, - new RiverPolyline( - new double[]{from.x(), to.x()}, - new double[]{from.z(), to.z()} - ) - ); - } - - private static RiverTerrainSampler slopedTerrain(boolean ocean) { - return new TestTerrain(ocean); - } - - private static RiverTerrainSampler flatTerrain(boolean ocean) { - return new RiverTerrainSampler() { - @Override - public double naturalHeight(int blockX, int blockZ) { - return 64.0; - } - - @Override - public boolean isOcean(int blockX, int blockZ) { - return ocean && blockX >= 224; - } - }; - } - - private static int compareRank(RiverNode first, RiverNode second) { - if (first.ocean() != second.ocean()) { - return first.ocean() ? -1 : 1; - } - int comparison = Double.compare(first.rank(), second.rank()); - if (comparison != 0) { - return comparison; - } - int hydraulicComparison = Double.compare(first.hydraulicHeight(), second.hydraulicHeight()); - return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id()); - } - - private static void assertWormBounds(RiverReach reach, double maximumOffset) { - assertTrue(maximumWormDisplacement(reach) <= maximumOffset + 0.0000001D); - RiverPolyline polyline = reach.polyline(); - assertEquals(reach.from().x(), polyline.x(0), 0D); - assertEquals(reach.from().z(), polyline.z(0), 0D); - int last = polyline.size() - 1; - assertEquals(reach.to().x(), polyline.x(last), 0D); - assertEquals(reach.to().z(), polyline.z(last), 0D); - } - - private static double maximumWormDisplacement(RiverReach reach) { - RiverPolyline polyline = reach.polyline(); - double deltaX = reach.to().x() - reach.from().x(); - double deltaZ = reach.to().z() - reach.from().z(); - double maximumDisplacement = 0D; - for (int point = 0; point < polyline.size(); point++) { - double t = (double) point / (polyline.size() - 1); - double straightX = reach.from().x() + deltaX * t; - double straightZ = reach.from().z() + deltaZ * t; - double displacement = StrictMath.hypot( - polyline.x(point) - straightX, - polyline.z(point) - straightZ - ); - maximumDisplacement = StrictMath.max(maximumDisplacement, displacement); - } - return maximumDisplacement; - } - - private static boolean polylinesDiffer(RiverPolyline first, RiverPolyline second) { - if (first.size() != second.size()) { - return true; - } - for (int point = 0; point < first.size(); point++) { - if (Double.doubleToLongBits(first.x(point)) != Double.doubleToLongBits(second.x(point)) - || Double.doubleToLongBits(first.z(point)) != Double.doubleToLongBits(second.z(point))) { - return true; - } - } - return false; - } - - private static long digest(RiverTile tile) { - long hash = 0xCBF29CE484222325L; - for (RiverReach reach : tile.reaches()) { - hash = RiverNetwork.mix(hash ^ reach.id().stableId()); - hash = digestNode(hash, reach.from()); - hash = digestNode(hash, reach.to()); - hash = RiverNetwork.mix(hash ^ reach.flow()); - hash = RiverNetwork.mix(hash ^ reach.order()); - hash = RiverNetwork.mix(hash ^ reach.state().ordinal()); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.width())); - for (int index = 0; index < reach.bodyProfile().size(); index++) { - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().position(index))); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().widthAtIndex(index))); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().bankWidthAtIndex(index))); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().depthAtIndex(index))); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bodyProfile().roofScaleAtIndex(index))); - } - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bankWidth())); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.depth())); - hash = RiverNetwork.mix(hash ^ (reach.mouth() ? 1L : 0L)); - hash = RiverNetwork.mix(hash ^ (reach.terminal() ? 1L : 0L)); - for (int point = 0; point < reach.polyline().size(); point++) { - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.polyline().x(point))); - hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.polyline().z(point))); - } - } - return hash; - } - - private static long digestNode(long hash, RiverNode node) { - long result = RiverNetwork.mix(hash ^ node.id().stableId()); - result = RiverNetwork.mix(result ^ Double.doubleToLongBits(node.x())); - result = RiverNetwork.mix(result ^ Double.doubleToLongBits(node.z())); - result = RiverNetwork.mix(result ^ Double.doubleToLongBits(node.naturalHeight())); - result = RiverNetwork.mix(result ^ Double.doubleToLongBits(node.hydraulicHeight())); - result = RiverNetwork.mix(result ^ Double.doubleToLongBits(node.rank())); - result = RiverNetwork.mix(result ^ Double.doubleToLongBits(node.routingScore())); - result = RiverNetwork.mix(result ^ (node.ocean() ? 1L : 0L)); - return RiverNetwork.mix(result ^ (node.riverAllowed() ? 1L : 0L)); - } - - private static void assertReachEquals(RiverReach first, RiverReach second) { - assertEquals(first.id(), second.id()); - assertEquals(first.state(), second.state()); - assertEquals(first.flow(), second.flow()); - assertEquals(first.order(), second.order()); - assertEquals(first.width(), second.width(), 0.0); - assertEquals(first.bodyProfile(), second.bodyProfile()); - assertEquals(first.bankWidth(), second.bankWidth(), 0.0); - assertEquals(first.depth(), second.depth(), 0.0); - assertEquals(first.mouth(), second.mouth()); - assertEquals(first.terminal(), second.terminal()); - assertEquals(first.from(), second.from()); - assertEquals(first.to(), second.to()); - assertEquals(first.polyline().size(), second.polyline().size()); - for (int point = 0; point < first.polyline().size(); point++) { - assertEquals(first.polyline().x(point), second.polyline().x(point), 0.0); - assertEquals(first.polyline().z(point), second.polyline().z(point), 0.0); - } - } - - private static class TestTerrain implements RiverTerrainSampler { - private final boolean ocean; - - private TestTerrain(boolean ocean) { - this.ocean = ocean; - } - - @Override - public double naturalHeight(int blockX, int blockZ) { - return 256.0 - blockX; - } - - @Override - public boolean isOcean(int blockX, int blockZ) { - return ocean && blockX >= 224; - } - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/river/RiverTileCacheTest.java b/core/src/test/java/art/arcane/iris/engine/river/RiverTileCacheTest.java deleted file mode 100644 index af1452c8b..000000000 --- a/core/src/test/java/art/arcane/iris/engine/river/RiverTileCacheTest.java +++ /dev/null @@ -1,306 +0,0 @@ -package art.arcane.iris.engine.river; - -import org.junit.Test; - -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -public class RiverTileCacheTest { - @Test - public void concurrentRequestsBuildOneTileOnce() throws Exception { - int requestCount = 16; - AtomicInteger builds = new AtomicInteger(); - CountDownLatch ready = new CountDownLatch(requestCount); - CountDownLatch start = new CountDownLatch(1); - CountDownLatch builderEntered = new CountDownLatch(1); - CountDownLatch releaseBuilder = new CountDownLatch(1); - RiverTileCache cache = new RiverTileCache(8, (tileX, tileZ) -> { - builds.incrementAndGet(); - builderEntered.countDown(); - assertTrue(releaseBuilder.await(5, TimeUnit.SECONDS)); - return emptyTile(tileX, tileZ); - }); - ExecutorService executor = Executors.newFixedThreadPool(requestCount); - try { - @SuppressWarnings("unchecked") - Future[] futures = new Future[requestCount]; - for (int request = 0; request < requestCount; request++) { - futures[request] = executor.submit(() -> { - ready.countDown(); - assertTrue(start.await(5, TimeUnit.SECONDS)); - return cache.get(-7, -11); - }); - } - assertTrue(ready.await(5, TimeUnit.SECONDS)); - start.countDown(); - assertTrue(builderEntered.await(5, TimeUnit.SECONDS)); - releaseBuilder.countDown(); - - RiverTile first = futures[0].get(5, TimeUnit.SECONDS); - for (Future future : futures) { - assertSame(first, future.get(5, TimeUnit.SECONDS)); - } - assertEquals(-7, first.tileX()); - assertEquals(-11, first.tileZ()); - assertEquals(1, builds.get()); - assertEquals(1, cache.completedSize()); - } finally { - releaseBuilder.countDown(); - executor.shutdownNow(); - cache.close(); - } - } - - @Test - public void differentKeysBuildConcurrentlyOutsideCacheLock() throws Exception { - CountDownLatch buildersEntered = new CountDownLatch(2); - CountDownLatch releaseBuilders = new CountDownLatch(1); - RiverTileCache cache = new RiverTileCache(4, (tileX, tileZ) -> { - buildersEntered.countDown(); - assertTrue(releaseBuilders.await(5, TimeUnit.SECONDS)); - return emptyTile(tileX, tileZ); - }); - ExecutorService executor = Executors.newFixedThreadPool(2); - try { - Future first = executor.submit(() -> cache.get(1, 1)); - Future second = executor.submit(() -> cache.get(2, 2)); - assertTrue(buildersEntered.await(5, TimeUnit.SECONDS)); - releaseBuilders.countDown(); - assertEquals(1, first.get(5, TimeUnit.SECONDS).tileX()); - assertEquals(2, second.get(5, TimeUnit.SECONDS).tileX()); - } finally { - releaseBuilders.countDown(); - executor.shutdownNow(); - cache.close(); - } - } - - @Test - public void completedEntriesUseBoundedAccessOrderEviction() { - AtomicInteger builds = new AtomicInteger(); - RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> { - builds.incrementAndGet(); - return emptyTile(tileX, tileZ); - }); - try { - RiverTile zero = cache.get(0, 0); - RiverTile one = cache.get(1, 0); - assertSame(zero, cache.get(0, 0)); - cache.get(2, 0); - assertEquals(2, cache.completedSize()); - assertSame(zero, cache.get(0, 0)); - - RiverTile rebuiltOne = cache.get(1, 0); - assertFalse(one == rebuiltOne); - assertEquals(4, builds.get()); - assertEquals(2, cache.completedSize()); - } finally { - cache.close(); - } - } - - @Test - public void failedBuildsAreRemovedAndRetryable() { - AtomicInteger attempts = new AtomicInteger(); - RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> { - if (attempts.incrementAndGet() == 1) { - throw new IllegalArgumentException("expected failure"); - } - return emptyTile(tileX, tileZ); - }); - try { - IllegalArgumentException failure = assertThrows( - IllegalArgumentException.class, - () -> cache.get(-3, 4) - ); - assertEquals("expected failure", failure.getMessage()); - assertEquals(0, cache.completedSize()); - - RiverTile retried = cache.get(-3, 4); - assertEquals(-3, retried.tileX()); - assertEquals(2, attempts.get()); - assertEquals(1, cache.completedSize()); - } finally { - cache.close(); - } - } - - @Test - public void mismatchedBuilderResultIsRemovedAndRetryable() { - AtomicInteger attempts = new AtomicInteger(); - RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> attempts.incrementAndGet() == 1 - ? emptyTile(tileX + 1, tileZ) - : emptyTile(tileX, tileZ)); - try { - assertThrows(IllegalStateException.class, () -> cache.get(5, 6)); - RiverTile retried = cache.get(5, 6); - assertEquals(5, retried.tileX()); - assertEquals(2, attempts.get()); - } finally { - cache.close(); - } - } - - @Test - public void clearAndCloseReleaseEntriesAndEnforceLifecycle() { - AtomicInteger builds = new AtomicInteger(); - RiverTileCache cache = new RiverTileCache(4, (tileX, tileZ) -> { - builds.incrementAndGet(); - return emptyTile(tileX, tileZ); - }); - cache.get(0, 0); - cache.get(1, 0); - assertEquals(2, cache.completedSize()); - - cache.clear(); - assertEquals(0, cache.completedSize()); - cache.get(0, 0); - assertEquals(3, builds.get()); - - cache.close(); - cache.close(); - assertTrue(cache.isClosed()); - assertEquals(0, cache.completedSize()); - assertThrows(IllegalStateException.class, () -> cache.get(0, 0)); - assertThrows(IllegalStateException.class, cache::clear); - } - - @Test - public void closeInvalidatesInflightBuildWithoutRetainingItsResult() throws Exception { - CountDownLatch builderEntered = new CountDownLatch(1); - CountDownLatch releaseBuilder = new CountDownLatch(1); - RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> { - builderEntered.countDown(); - assertTrue(releaseBuilder.await(5, TimeUnit.SECONDS)); - return emptyTile(tileX, tileZ); - }); - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - Future result = executor.submit(() -> cache.get(9, -9)); - assertTrue(builderEntered.await(5, TimeUnit.SECONDS)); - cache.close(); - releaseBuilder.countDown(); - - ExecutionException failure = assertThrows( - ExecutionException.class, - () -> result.get(5, TimeUnit.SECONDS) - ); - assertTrue(failure.getCause() instanceof IllegalStateException); - assertEquals(0, cache.completedSize()); - } finally { - releaseBuilder.countDown(); - executor.shutdownNow(); - cache.close(); - } - } - - @Test - public void clearInvalidatesInflightBuildAndFreshRequestPublishesNewEntry() throws Exception { - AtomicInteger attempts = new AtomicInteger(); - CountDownLatch firstBuilderEntered = new CountDownLatch(1); - CountDownLatch releaseFirstBuilder = new CountDownLatch(1); - RiverTileCache cache = new RiverTileCache(2, (tileX, tileZ) -> { - if (attempts.incrementAndGet() == 1) { - firstBuilderEntered.countDown(); - assertTrue(releaseFirstBuilder.await(5, TimeUnit.SECONDS)); - } - return emptyTile(tileX, tileZ); - }); - ExecutorService executor = Executors.newSingleThreadExecutor(); - try { - Future invalidated = executor.submit(() -> cache.get(4, -2)); - assertTrue(firstBuilderEntered.await(5, TimeUnit.SECONDS)); - cache.clear(); - - RiverTile replacement = cache.get(4, -2); - releaseFirstBuilder.countDown(); - - ExecutionException failure = assertThrows( - ExecutionException.class, - () -> invalidated.get(5, TimeUnit.SECONDS) - ); - assertTrue(failure.getCause() instanceof IllegalStateException); - assertEquals(4, replacement.tileX()); - assertEquals(-2, replacement.tileZ()); - assertEquals(2, attempts.get()); - assertEquals(1, cache.completedSize()); - assertSame(replacement, cache.get(4, -2)); - } finally { - releaseFirstBuilder.countDown(); - executor.shutdownNow(); - cache.close(); - } - } - - @Test - public void tileProvidesConstantTimeReachIdentityLookup() { - RiverReach reach = reach(); - RiverTile tile = new RiverTile(0, 0, 0, 0, 64, 64, List.of(reach)); - - assertSame(reach, tile.reach(reach.id())); - assertNull(tile.reach(RiverEdgeId.of(new RiverNodeId(2L, 0L), new RiverNodeId(3L, 0L)))); - assertThrows( - IllegalArgumentException.class, - () -> new RiverTile(0, 0, 0, 0, 64, 64, List.of(reach, reach)) - ); - } - - private static RiverTile emptyTile(int tileX, int tileZ) { - int minimumX = tileX * 64; - int minimumZ = tileZ * 64; - return new RiverTile(tileX, tileZ, minimumX, minimumZ, minimumX + 64, minimumZ + 64, List.of()); - } - - private static RiverReach reach() { - RiverNode from = new RiverNode( - new RiverNodeId(0L, 0L), - 8.0, - 8.0, - 20.0, - 20.0, - 20.0, - 20.0, - false, - true - ); - RiverNode to = new RiverNode( - new RiverNodeId(1L, 0L), - 56.0, - 8.0, - 10.0, - 10.0, - 10.0, - 10.0, - false, - true - ); - return new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.WET, - 1, - 1, - 8.0, - 4.0, - 3.0, - RiverBodyProfile.constant(8.0, 4.0, 3.0), - false, - false, - new RiverPolyline(new double[]{8.0, 56.0}, new double[]{8.0, 8.0}) - ); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/river/cave/RiverCaveContainmentPlannerTest.java b/core/src/test/java/art/arcane/iris/engine/river/cave/RiverCaveContainmentPlannerTest.java deleted file mode 100644 index de2b39d7b..000000000 --- a/core/src/test/java/art/arcane/iris/engine/river/cave/RiverCaveContainmentPlannerTest.java +++ /dev/null @@ -1,757 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import org.junit.Test; - -import java.util.ArrayDeque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -public class RiverCaveContainmentPlannerTest { - private static final List DIRECTIONS = List.of( - new CavePosition(1, 0, 0), - new CavePosition(-1, 0, 0), - new CavePosition(0, 1, 0), - new CavePosition(0, -1, 0), - new CavePosition(0, 0, 1), - new CavePosition(0, 0, -1) - ); - - private final RiverCaveContainmentPlanner planner = new RiverCaveContainmentPlanner(); - - @Test - public void closedComponentProducesConnectedThroatPoolAndGuards() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(1, 6, 0)); - RiverCaveSource source = source(14L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)); - - RiverCavePlan plan = planner.plan(view, source, settings()); - - assertTrue(plan.accepted()); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 12, 0))); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 11, 0))); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0))); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(1, 6, 0))); - assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD)); - assertMutationConnected(plan, source.entry(), position(1, 6, 0)); - assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet()); - assertEquals(CaveVoxel.CAVE_AIR, plan.baselinePreconditions().get(position(1, 6, 0)).voxel()); - assertEquals(CaveVoxel.SOLID, plan.baselinePreconditions().get(position(0, 12, 0)).voxel()); - } - - @Test - public void compatibleFluidFollowsExplicitPolicy() { - TestVoxelView view = new TestVoxelView(); - CavePosition target = position(0, 7, 0); - view.set(CaveVoxel.COMPATIBLE_FLUID, target); - RiverCaveSource source = source(1L, 10, RiverCaveMode.CLOSED_COMPONENT, target); - - RiverCavePlan allowed = planner.plan(view, source, settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE)); - RiverCavePlan rejected = planner.plan(view, source, settings(RiverCaveFluidPolicy.REJECT_EXISTING)); - - assertTrue(allowed.accepted()); - assertEquals(RiverCaveAction.WET_SOURCE, allowed.actions().get(target)); - assertEquals(RiverCaveRejection.EXISTING_FLUID, rejected.rejection()); - assertTrue(rejected.actions().isEmpty()); - } - - @Test - public void replacementPolicyIsDistinctAndStillRejectsLava() { - CavePosition target = position(0, 7, 0); - TestVoxelView compatibleView = new TestVoxelView(); - compatibleView.set(CaveVoxel.COMPATIBLE_FLUID, target); - TestVoxelView incompatibleView = new TestVoxelView(); - incompatibleView.set(CaveVoxel.INCOMPATIBLE_FLUID, target); - TestVoxelView lavaView = new TestVoxelView(); - lavaView.set(CaveVoxel.LAVA, target); - RiverCaveSource source = source(101L, 10, RiverCaveMode.CLOSED_COMPONENT, target); - - RiverCavePlan rejectedCompatible = planner.plan( - compatibleView, - source, - settings(RiverCaveFluidPolicy.REJECT_EXISTING) - ); - RiverCavePlan allowedCompatible = planner.plan( - compatibleView, - source, - settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE) - ); - RiverCavePlan rejectedIncompatible = planner.plan( - incompatibleView, - source, - settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE) - ); - RiverCavePlan replacedIncompatible = planner.plan( - incompatibleView, - source, - settings(RiverCaveFluidPolicy.REPLACE_CONTAINED) - ); - RiverCavePlan rejectedLava = planner.plan( - lavaView, - source, - settings(RiverCaveFluidPolicy.REPLACE_CONTAINED) - ); - - assertEquals(RiverCaveRejection.EXISTING_FLUID, rejectedCompatible.rejection()); - assertTrue(allowedCompatible.accepted()); - assertEquals(RiverCaveRejection.INCOMPATIBLE_FLUID, rejectedIncompatible.rejection()); - assertTrue(replacedIncompatible.accepted()); - assertEquals(RiverCaveAction.WET_SOURCE, replacedIncompatible.actions().get(target)); - assertEquals(RiverCaveRejection.LAVA_CONTACT, rejectedLava.rejection()); - } - - @Test - public void throatRadiusExpandsTheConnectedBoreFootprint() { - TestVoxelView thinView = new TestVoxelView(); - TestVoxelView thickView = new TestVoxelView(); - CavePosition target = position(0, 7, 0); - thinView.set(CaveVoxel.CAVE_AIR, target); - thickView.set(CaveVoxel.CAVE_AIR, target); - RiverCaveSource source = source(102L, 10, RiverCaveMode.CLOSED_COMPONENT, target); - RiverCavePlannerSettings thinSettings = detailedSettings( - 1, - 0, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - RiverCavePlannerSettings thickSettings = detailedSettings( - 3, - 0, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - - RiverCavePlan thin = planner.plan(thinView, source, thinSettings); - RiverCavePlan thick = planner.plan(thickView, source, thickSettings); - - assertTrue(thin.accepted()); - assertTrue(thick.accepted()); - assertFalse(thin.actions().containsKey(position(2, 10, 0))); - assertEquals(RiverCaveAction.WET_SOURCE, thick.actions().get(position(2, 10, 0))); - assertTrue(mutationCount(thick) > mutationCount(thin)); - } - - @Test - public void generatedGrottoRetainsConfiguredDryHeadroom() { - TestVoxelView view = new TestVoxelView(); - RiverCaveSource source = source(103L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0)); - RiverCavePlannerSettings acceptedSettings = detailedSettings( - 1, - 2, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - RiverCavePlannerSettings rejectedSettings = detailedSettings( - 1, - 5, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - - RiverCavePlan accepted = planner.plan(view, source, acceptedSettings); - RiverCavePlan rejected = planner.plan(view, source, rejectedSettings); - - assertTrue(accepted.accepted()); - assertEquals(RiverCaveAction.FALLING_FLUID, accepted.actions().get(position(0, 11, 0))); - assertEquals(RiverCaveAction.FALLING_FLUID, accepted.actions().get(position(0, 12, 0))); - assertEquals(RiverCaveRejection.DRY_HEADROOM_LIMIT, rejected.rejection()); - } - - @Test - public void configuredGrottoPredicateChangesFootprintDeterministically() { - TestVoxelView view = new TestVoxelView(); - RiverCaveSource source = source(104L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0)); - RiverCavePlannerSettings ellipsoid = detailedSettings( - 1, - 0, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - RiverCaveGrottoShape compactShape = (candidate, settings, dx, dy, dz) -> { - double horizontal = settings.grottoHorizontalRadius(); - double vertical = settings.grottoVerticalRadius(); - double normalized = (dx * dx / (horizontal * horizontal)) - + (dy * dy / (vertical * vertical)) - + (dz * dz / (horizontal * horizontal)); - return normalized <= 0.5D; - }; - RiverCavePlannerSettings compact = detailedSettings( - 1, - 0, - compactShape, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - - RiverCavePlan full = planner.plan(view, source, ellipsoid); - RiverCavePlan firstCompact = planner.plan(view, source, compact); - RiverCavePlan secondCompact = planner.plan(view, source, compact); - - assertTrue(full.accepted()); - assertTrue(firstCompact.accepted()); - assertEquals(firstCompact, secondCompact); - assertTrue(mutationCount(full) > mutationCount(firstCompact)); - } - - @Test - public void radiusLimitRejectsWholeComponentWithoutClipping() { - TestVoxelView view = new TestVoxelView(); - for (int x = 0; x <= 3; x++) { - view.set(CaveVoxel.CAVE_AIR, position(x, 7, 0)); - } - RiverCavePlannerSettings settings = new RiverCavePlannerSettings( - 2, - 20, - 64, - 32, - 1, - 1, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - - RiverCavePlan plan = planner.plan( - view, - source(2L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)), - settings - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.RADIUS_LIMIT); - } - - @Test - public void depthLimitRejectsWholeComponentWithoutClipping() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 8, 0), position(0, 7, 0)); - RiverCavePlannerSettings settings = new RiverCavePlannerSettings( - 8, - 4, - 64, - 32, - 1, - 1, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - - RiverCavePlan plan = planner.plan( - view, - source(3L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 8, 0)), - settings - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.DEPTH_LIMIT); - } - - @Test - public void volumeLimitRejectsWholeComponentWithoutClipping() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(2, 7, 0)); - RiverCavePlannerSettings settings = new RiverCavePlannerSettings( - 8, - 20, - 2, - 32, - 1, - 1, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - ); - - RiverCavePlan plan = planner.plan( - view, - source(4L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)), - settings - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.VOLUME_LIMIT); - } - - @Test - public void worldBoundaryRejectsWholeComponent() { - TestVoxelView view = new TestVoxelView(-16, 0, 0, 32, -16, 16); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0)); - - RiverCavePlan plan = planner.plan( - view, - source(5L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)), - settings() - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.WORLD_BOUNDARY); - } - - @Test - public void openSurfaceRejectsWholeComponent() { - TestVoxelView view = new TestVoxelView(); - CavePosition target = position(0, 7, 0); - view.set(CaveVoxel.CAVE_AIR, target); - view.openToSurface(target); - - RiverCavePlan plan = planner.plan( - view, - source(6L, 10, RiverCaveMode.CLOSED_COMPONENT, target), - settings() - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.OPEN_SURFACE); - } - - @Test - public void lavaAndIncompatibleFluidContactsRejectWholeComponent() { - TestVoxelView lavaView = new TestVoxelView(); - lavaView.set(CaveVoxel.CAVE_AIR, position(0, 7, 0)); - lavaView.set(CaveVoxel.LAVA, position(1, 7, 0)); - TestVoxelView fluidView = new TestVoxelView(); - fluidView.set(CaveVoxel.CAVE_AIR, position(0, 7, 0)); - fluidView.set(CaveVoxel.INCOMPATIBLE_FLUID, position(1, 7, 0)); - RiverCaveSource source = source(7L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)); - - RiverCavePlan lavaPlan = planner.plan(lavaView, source, settings()); - RiverCavePlan fluidPlan = planner.plan(fluidView, source, settings()); - - assertRejectedWithoutPublication(lavaPlan, RiverCaveRejection.LAVA_CONTACT); - assertRejectedWithoutPublication(fluidPlan, RiverCaveRejection.INCOMPATIBLE_FLUID); - } - - @Test - public void fallingHazardAbovePoolRejectsWholeComponent() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0)); - view.set(CaveVoxel.LAVA, position(1, 11, 0)); - - RiverCavePlan plan = planner.plan( - view, - source(15L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)), - settings() - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.LAVA_CONTACT); - } - - @Test - public void generatedGrottoHasBoundedWetDryAndGuardActions() { - TestVoxelView view = new TestVoxelView(); - RiverCaveSource source = source(8L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0)); - - RiverCavePlan plan = planner.plan(view, source, settings()); - - assertTrue(plan.accepted()); - assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 12, 0))); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 8, 0))); - assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD)); - assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet()); - assertFalse(plan.actions().containsKey(position(0, 13, 0))); - assertMutationConnected(plan, source.entry(), source.target()); - } - - @Test - public void generatedGrottoAcceptsItsCompleteSurfaceMouth() { - TestVoxelView view = new TestVoxelView(); - CavePosition[] wetMouth = { - position(-1, 12, 0), - position(1, 12, 0), - position(0, 12, -1), - position(0, 12, 1) - }; - CavePosition[] dryMouth = { - position(-1, 13, 0), - position(1, 13, 0), - position(0, 13, -1), - position(0, 13, 1) - }; - view.set(CaveVoxel.COMPATIBLE_FLUID, wetMouth); - view.set(CaveVoxel.CAVE_AIR, dryMouth); - for (CavePosition position : wetMouth) { - view.openToSurface(position); - } - for (CavePosition position : dryMouth) { - view.openToSurface(position); - } - RiverCaveSource source = source(108L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0)); - - RiverCavePlan plan = planner.plan(view, source, detailedSettings( - 2, - 2, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.ALLOW_COMPATIBLE - )); - - assertTrue(plan.rejection().toString(), plan.accepted()); - for (CavePosition position : wetMouth) { - assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position)); - } - } - - @Test - public void generatedGrottoRejectsAnOpenShell() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(3, 8, 0)); - - RiverCavePlan plan = planner.plan( - view, - source(9L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0)), - settings() - ); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.GROTTO_SHELL_OPEN); - } - - @Test - public void deepPoolOpensOnlyAboveItsContainedFluidHead() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 11, 0), position(0, 13, 0)); - RiverCaveSource source = new RiverCaveSource( - 109L, - position(0, 10, 0), - position(0, 8, 0), - 10, - RiverCaveMode.DEEP_POOL - ); - - RiverCavePlan plan = planner.plan(view, source, detailedSettings( - 1, - 2, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.REJECT_EXISTING - )); - - assertTrue(plan.rejection().toString(), plan.accepted()); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0))); - assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 11, 0))); - assertFalse(plan.actions().containsKey(position(0, 13, 0))); - } - - @Test - public void deepPoolRejectsCaveLeakAtOrBelowItsFluidHead() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(5, 8, 0)); - RiverCaveSource source = new RiverCaveSource( - 110L, - position(0, 10, 0), - position(0, 8, 0), - 10, - RiverCaveMode.DEEP_POOL - ); - - RiverCavePlan plan = planner.plan(view, source, detailedSettings( - 1, - 2, - RiverCaveGrottoShape.ELLIPSOID, - RiverCaveFluidPolicy.REJECT_EXISTING - )); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.GROTTO_SHELL_OPEN); - } - - @Test - public void combinedModeUsesClosedCaveOrGeneratedGrottoFromBaseline() { - TestVoxelView caveView = new TestVoxelView(); - CavePosition target = position(0, 8, 0); - caveView.set(CaveVoxel.CAVE_AIR, target); - TestVoxelView solidView = new TestVoxelView(); - RiverCaveSource source = source(10L, 10, RiverCaveMode.GROTTO_OR_CLOSED_COMPONENT, target); - - RiverCavePlan cavePlan = planner.plan(caveView, source, settings()); - RiverCavePlan grottoPlan = planner.plan(solidView, source, settings()); - - assertTrue(cavePlan.accepted()); - assertTrue(grottoPlan.accepted()); - assertEquals(CaveVoxel.CAVE_AIR, cavePlan.baselinePreconditions().get(target).voxel()); - assertEquals(CaveVoxel.SOLID, grottoPlan.baselinePreconditions().get(target).voxel()); - } - - @Test - public void waterfallKeepsColumnDistinctFromSourcePoolAndDryAir() { - TestVoxelView view = new TestVoxelView(); - RiverCaveSource source = source(11L, 10, RiverCaveMode.WATERFALL_POOL, position(0, 8, 0)); - - RiverCavePlan plan = planner.plan(view, source, settings()); - - assertTrue(plan.accepted()); - assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 12, 0))); - assertEquals(RiverCaveAction.FALLING_FLUID, plan.actions().get(position(0, 11, 0))); - assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 10, 0))); - assertFalse(plan.actions().containsValue(RiverCaveAction.DRY_AIR)); - } - - @Test - public void waterfallRejectsAnUnsealedFallingColumn() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 8, 0), position(1, 11, 0)); - RiverCaveSource source = source(13L, 10, RiverCaveMode.WATERFALL_POOL, position(0, 8, 0)); - - RiverCavePlan plan = planner.plan(view, source, settings()); - - assertRejectedWithoutPublication(plan, RiverCaveRejection.WATERFALL_SHAFT_OPEN); - } - - @Test - public void overlapArbitrationIsOrderIndependentAndNamesWinner() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(2, 7, 0)); - RiverCaveSource lowerHead = new RiverCaveSource( - 1L, - position(2, 12, 0), - position(2, 7, 0), - 10, - RiverCaveMode.CLOSED_COMPONENT - ); - RiverCaveSource higherHead = new RiverCaveSource( - 20L, - position(0, 12, 0), - position(0, 7, 0), - 11, - RiverCaveMode.CLOSED_COMPONENT - ); - - RiverCavePlanningResult forward = planner.planAll(view, List.of(lowerHead, higherHead), settings()); - RiverCavePlanningResult reverse = planner.planAll(view, List.of(higherHead, lowerHead), settings()); - - assertEquals(forward, reverse); - assertEquals(higherHead, forward.plans().get(0).source()); - assertTrue(forward.plans().get(0).accepted()); - assertEquals(RiverCaveRejection.OVERLAPPING_SOURCE, forward.plans().get(1).rejection()); - assertEquals(20L, forward.plans().get(1).arbitrationWinnerSourceId().getAsLong()); - assertEquals(forward.actions().keySet(), forward.baselinePreconditions().keySet()); - } - - @Test - public void equalHeadOverlapUsesLowestStableSourceId() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0), position(1, 7, 0), position(2, 7, 0)); - RiverCaveSource largerId = new RiverCaveSource( - 20L, - position(0, 12, 0), - position(0, 7, 0), - 10, - RiverCaveMode.CLOSED_COMPONENT - ); - RiverCaveSource smallerId = new RiverCaveSource( - 1L, - position(2, 12, 0), - position(2, 7, 0), - 10, - RiverCaveMode.CLOSED_COMPONENT - ); - - RiverCavePlanningResult result = planner.planAll(view, List.of(largerId, smallerId), settings()); - - assertEquals(smallerId, result.plans().get(0).source()); - assertTrue(result.plans().get(0).accepted()); - assertEquals(1L, result.plans().get(1).arbitrationWinnerSourceId().getAsLong()); - } - - @Test - public void arbitrationUsesDirectHigherPriorityPlansEvenWhenTheyAreRejected() { - TestVoxelView view = new TestVoxelView(); - view.set( - CaveVoxel.CAVE_AIR, - position(0, 7, 0), - position(2, 7, 0), - position(4, 7, 0) - ); - RiverCaveSource lowest = new RiverCaveSource( - 30L, - position(0, 12, 0), - position(0, 7, 0), - 10, - RiverCaveMode.CLOSED_COMPONENT - ); - RiverCaveSource middle = new RiverCaveSource( - 20L, - position(2, 12, 0), - position(2, 7, 0), - 11, - RiverCaveMode.CLOSED_COMPONENT - ); - RiverCaveSource highest = new RiverCaveSource( - 10L, - position(4, 12, 0), - position(4, 7, 0), - 12, - RiverCaveMode.CLOSED_COMPONENT - ); - - RiverCavePlanningResult forward = planner.planAll( - view, - List.of(lowest, middle, highest), - settings() - ); - RiverCavePlanningResult reverse = planner.planAll( - view, - List.of(highest, middle, lowest), - settings() - ); - - assertEquals(forward, reverse); - assertTrue(forward.plans().get(0).accepted()); - assertEquals(RiverCaveRejection.OVERLAPPING_SOURCE, forward.plans().get(1).rejection()); - assertEquals(10L, forward.plans().get(1).arbitrationWinnerSourceId().getAsLong()); - assertEquals(RiverCaveRejection.OVERLAPPING_SOURCE, forward.plans().get(2).rejection()); - assertEquals(20L, forward.plans().get(2).arbitrationWinnerSourceId().getAsLong()); - assertFalse(forward.actions().containsKey(position(0, 7, 0))); - assertTrue(forward.actions().containsKey(position(4, 7, 0))); - } - - @Test - public void planOutputsAreImmutable() { - TestVoxelView view = new TestVoxelView(); - view.set(CaveVoxel.CAVE_AIR, position(0, 7, 0)); - RiverCaveSource source = source(12L, 10, RiverCaveMode.CLOSED_COMPONENT, position(0, 7, 0)); - RiverCavePlanningResult result = planner.planAll(view, List.of(source), settings()); - RiverCavePlan plan = result.plans().get(0); - - assertThrows( - UnsupportedOperationException.class, - () -> plan.actions().put(position(9, 9, 9), RiverCaveAction.WET_SOURCE) - ); - assertThrows( - UnsupportedOperationException.class, - () -> plan.baselinePreconditions().put( - position(9, 9, 9), - new CaveVoxelPrecondition(CaveVoxel.SOLID, false) - ) - ); - assertThrows(UnsupportedOperationException.class, () -> result.plans().add(plan)); - assertThrows( - UnsupportedOperationException.class, - () -> result.actions().put(position(9, 9, 9), RiverCaveAction.WET_SOURCE) - ); - } - - private void assertMutationConnected(RiverCavePlan plan, CavePosition start, CavePosition expected) { - Set mutations = new HashSet<>(); - for (Map.Entry entry : plan.actions().entrySet()) { - if (entry.getValue() != RiverCaveAction.SEAL_GUARD) { - mutations.add(entry.getKey()); - } - } - Queue queue = new ArrayDeque<>(); - Set visited = new HashSet<>(); - queue.add(start); - visited.add(start); - while (!queue.isEmpty()) { - CavePosition position = queue.remove(); - for (CavePosition direction : DIRECTIONS) { - CavePosition neighbor = position.offset(direction.x(), direction.y(), direction.z()); - if (mutations.contains(neighbor) && visited.add(neighbor)) { - queue.add(neighbor); - } - } - } - assertTrue(visited.contains(expected)); - assertEquals(mutations, visited); - } - - private int mutationCount(RiverCavePlan plan) { - int count = 0; - for (RiverCaveAction action : plan.actions().values()) { - if (action != RiverCaveAction.SEAL_GUARD) { - count++; - } - } - return count; - } - - private void assertRejectedWithoutPublication(RiverCavePlan plan, RiverCaveRejection rejection) { - assertFalse(plan.accepted()); - assertEquals(rejection, plan.rejection()); - assertTrue(plan.actions().isEmpty()); - assertTrue(plan.baselinePreconditions().isEmpty()); - } - - private RiverCaveSource source(long sourceId, int waterHeadY, RiverCaveMode mode, CavePosition target) { - return new RiverCaveSource(sourceId, position(0, 12, 0), target, waterHeadY, mode); - } - - private RiverCavePlannerSettings settings() { - return settings(RiverCaveFluidPolicy.ALLOW_COMPATIBLE); - } - - private RiverCavePlannerSettings settings(RiverCaveFluidPolicy policy) { - return new RiverCavePlannerSettings(12, 20, 64, 32, 2, 2, policy); - } - - private RiverCavePlannerSettings detailedSettings( - int throatRadius, - int dryHeadroom, - RiverCaveGrottoShape shape, - RiverCaveFluidPolicy policy - ) { - return new RiverCavePlannerSettings( - 12, - 20, - 2048, - 32, - throatRadius, - 4, - 4, - dryHeadroom, - policy, - shape - ); - } - - private CavePosition position(int x, int y, int z) { - return new CavePosition(x, y, z); - } - - private static final class TestVoxelView implements CaveVoxelView { - private final int minX; - private final int maxX; - private final int minY; - private final int maxY; - private final int minZ; - private final int maxZ; - private final Map voxels = new HashMap<>(); - private final Set surfaceOpenings = new HashSet<>(); - - private TestVoxelView() { - this(-32, 32, 0, 32, -32, 32); - } - - private TestVoxelView(int minX, int maxX, int minY, int maxY, int minZ, int maxZ) { - this.minX = minX; - this.maxX = maxX; - this.minY = minY; - this.maxY = maxY; - this.minZ = minZ; - this.maxZ = maxZ; - } - - @Override - public boolean isInWorld(CavePosition position) { - return position.x() >= minX - && position.x() <= maxX - && position.y() >= minY - && position.y() <= maxY - && position.z() >= minZ - && position.z() <= maxZ; - } - - @Override - public CaveVoxel voxelAt(CavePosition position) { - return voxels.getOrDefault(position, CaveVoxel.SOLID); - } - - @Override - public boolean isOpenToSurface(CavePosition position) { - return surfaceOpenings.contains(position); - } - - private void set(CaveVoxel voxel, CavePosition... positions) { - for (CavePosition position : positions) { - voxels.put(position, voxel); - } - } - - private void openToSurface(CavePosition position) { - surfaceOpenings.add(position); - } - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/river/cave/RiverCaveHydrologyStorageTest.java b/core/src/test/java/art/arcane/iris/engine/river/cave/RiverCaveHydrologyStorageTest.java deleted file mode 100644 index 217d0fb4a..000000000 --- a/core/src/test/java/art/arcane/iris/engine/river/cave/RiverCaveHydrologyStorageTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package art.arcane.iris.engine.river.cave; - -import art.arcane.volmlib.util.mantle.runtime.MantleChunk; -import art.arcane.volmlib.util.matter.Matter; -import art.arcane.volmlib.util.matter.MatterSlice; -import org.junit.Test; - -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class RiverCaveHydrologyStorageTest { - @Test - @SuppressWarnings("unchecked") - public void presentReadDoesNotMaterializeASectionOrSlice() { - MantleChunk chunk = mock(MantleChunk.class); - Matter matter = mock(Matter.class); - MatterSlice slice = mock(MatterSlice.class); - RiverCaveHydrology expected = RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE); - when(chunk.exists(2)).thenReturn(true); - when(chunk.get(2)).thenReturn(matter); - when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true); - when(matter.getSlice(RiverCaveHydrology.class)).thenReturn(slice); - when(slice.get(3, 1, 5)).thenReturn(expected); - - assertSame(expected, RiverCaveHydrologyStorage.getIfPresent(chunk, 3, 33, 5)); - verify(chunk, never()).getOrCreate(2); - verify(matter, never()).slice(RiverCaveHydrology.class); - } - - @Test - @SuppressWarnings("unchecked") - public void absentSliceReturnsWithoutCreatingIt() { - MantleChunk chunk = mock(MantleChunk.class); - Matter matter = mock(Matter.class); - when(chunk.exists(2)).thenReturn(true); - when(chunk.get(2)).thenReturn(matter); - - assertNull(RiverCaveHydrologyStorage.getIfPresent(chunk, 3, 33, 5)); - verify(matter, never()).slice(RiverCaveHydrology.class); - verify(matter, never()).getSlice(RiverCaveHydrology.class); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/river/runtime/EffectiveRiverSettingsTest.java b/core/src/test/java/art/arcane/iris/engine/river/runtime/EffectiveRiverSettingsTest.java deleted file mode 100644 index 4ba2cc289..000000000 --- a/core/src/test/java/art/arcane/iris/engine/river/runtime/EffectiveRiverSettingsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverBiomes; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.object.IrisRiverOverride; -import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; -import art.arcane.iris.engine.object.IrisRiverTerminalMode; -import art.arcane.volmlib.util.collection.KList; -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; - -public class EffectiveRiverSettingsTest { - @Test - public void appliesRegionThenBiomeOverridesWithoutChangingGraphSettings() { - IrisRiverNetwork network = new IrisRiverNetwork() - .setBiomes(new IrisRiverBiomes() - .setChannel(new KList<>("dimension-channel")) - .setBank(new KList<>("dimension-bank"))); - IrisRegion region = new IrisRegion().setRiverOverride(new IrisRiverOverride() - .setAllowSources(false) - .setRoutingPolicy(IrisRiverRoutingPolicy.AVOID) - .setWidthMultiplier(1.5D) - .setChannelBiomes(new KList<>("region-channel"))); - IrisBiome biome = new IrisBiome().setRiverOverride(new IrisRiverOverride() - .setRoutingPolicy(IrisRiverRoutingPolicy.BLOCK) - .setWidthMultiplier(0.5D) - .setChannelBiomes(new KList<>()) - .setTerminalMode(IrisRiverTerminalMode.SUPPRESS)); - - EffectiveRiverSettings settings = EffectiveRiverSettings.resolve(network, region, biome); - - assertFalse(settings.allowSources()); - assertEquals(IrisRiverRoutingPolicy.BLOCK, settings.routingPolicy()); - assertEquals(0.5D, settings.widthMultiplier(), 0D); - assertTrue(settings.channelBiomes().isEmpty()); - assertEquals(List.of("dimension-bank"), settings.bankBiomes()); - assertEquals(IrisRiverTerminalMode.SUPPRESS, settings.terminalMode()); - assertTrue(settings.terminalModeOverridden()); - } - - @Test - public void inheritsDimensionDefaultsWhenOverridesAreAbsent() { - IrisRiverNetwork network = new IrisRiverNetwork() - .setBiomes(new IrisRiverBiomes().setDry(new KList<>("dry"))); - - EffectiveRiverSettings settings = EffectiveRiverSettings.resolve(network, null, null); - - assertTrue(settings.allowSources()); - assertEquals(IrisRiverRoutingPolicy.ALLOW, settings.routingPolicy()); - assertEquals(1D, settings.routingCostMultiplier(), 0D); - assertEquals(List.of("dry"), settings.dryBiomes()); - assertEquals(IrisRiverTerminalMode.DRY_CHANNEL, settings.terminalMode()); - assertFalse(settings.terminalModeOverridden()); - } -} diff --git a/core/src/test/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntimeTest.java b/core/src/test/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntimeTest.java deleted file mode 100644 index d1b7c8d96..000000000 --- a/core/src/test/java/art/arcane/iris/engine/river/runtime/IrisRiverRuntimeTest.java +++ /dev/null @@ -1,1178 +0,0 @@ -package art.arcane.iris.engine.river.runtime; - -import art.arcane.iris.core.loader.IrisData; -import art.arcane.iris.engine.object.InferredType; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisGeneratorStyle; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.object.IrisRiverNetwork; -import art.arcane.iris.engine.object.IrisRiverCaveMode; -import art.arcane.iris.engine.object.IrisRiverOverride; -import art.arcane.iris.engine.object.IrisRiverRoutingPolicy; -import art.arcane.iris.engine.object.IrisRiverTerminalMode; -import art.arcane.iris.engine.object.IrisRiverWaterMode; -import art.arcane.iris.engine.object.IrisRiverWorm; -import art.arcane.iris.engine.object.IrisStyledRange; -import art.arcane.iris.engine.object.NoiseStyle; -import art.arcane.iris.engine.river.RiverEdgeId; -import art.arcane.iris.engine.river.RiverNode; -import art.arcane.iris.engine.river.RiverNodeId; -import art.arcane.iris.engine.river.RiverPolyline; -import art.arcane.iris.engine.river.RiverReach; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverAnchor; -import art.arcane.iris.engine.river.RiverBodyProfile; -import art.arcane.iris.engine.river.RiverRoutingContext; -import art.arcane.iris.engine.river.RiverTerrainNodeSample; -import art.arcane.iris.engine.river.RiverTopologyComplexity; -import art.arcane.iris.engine.river.RiverTerrainSourceSample; -import art.arcane.iris.util.project.interpolation.NoiseBounds; -import art.arcane.iris.util.project.stream.ProceduralStream; -import art.arcane.iris.util.project.stream.interpolation.Interpolated; -import art.arcane.volmlib.util.collection.KList; -import org.junit.Test; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -public class IrisRiverRuntimeTest { - @Test - public void tunnelCrossSectionRoundsAndNoiseModulatesBothSurfaces() { - assertEquals(62, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 1D, 2D)); - assertEquals(69, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 0D, 2D)); - assertEquals(65, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 1D, -1D)); - assertEquals(77, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 1D, 3D)); - assertEquals(70, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 0D, 3D)); - assertEquals(71, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 0.5D, -2D)); - } - - @Test - public void widenedTunnelHaloAccountsForMaximumConfiguredMultiplier() { - assertEquals(17, RiverTopologyComplexity.tunnelHalo(7D, 4D, 2D)); - assertEquals(2500L, RiverTopologyComplexity.tunnelSampleColumns(7D, 4D, 2D)); - } - - @Test - public void tunnelWidthMultiplierWidensOnlySubterraneanFootprint() { - IrisRiverNetwork narrowConfiguration = configuration(false); - narrowConfiguration.getTerrain() - .setMaxChannelWidth(4D) - .setMaxBankWidth(0D) - .setBankWidth(range(0D)) - .setMaxIncision(10) - .setTunnelMouthBlend(0D) - .setTunnelWidthMultiplier(range(1D)); - IrisRiverNetwork wideConfiguration = configuration(false); - wideConfiguration.getTerrain() - .setMaxChannelWidth(4D) - .setMaxBankWidth(0D) - .setBankWidth(range(0D)) - .setMaxIncision(10) - .setTunnelMouthBlend(0D) - .setTunnelWidthMultiplier(range(4D)); - - try (IrisRiverRuntime narrow = runtime( - narrowConfiguration, - constantHeight(100D), - constantLandBiome(), - new IrisRegion(), - true, - false - ); IrisRiverRuntime wide = runtime( - wideConfiguration, - constantHeight(100D), - constantLandBiome(), - new IrisRegion(), - true, - false - )) { - int narrowTunnelColumns = 0; - int wideTunnelColumns = 0; - int narrowSurfaceColumns = 0; - int wideSurfaceColumns = 0; - for (int x = -128; x < 128; x += 2) { - for (int z = -128; z < 128; z += 2) { - if (narrow.sampleTunnel(x, z) != null) { - narrowTunnelColumns++; - } - if (wide.sampleTunnel(x, z) != null) { - wideTunnelColumns++; - } - if (narrow.sample(x, z).river().present()) { - narrowSurfaceColumns++; - } - if (wide.sample(x, z).river().present()) { - wideSurfaceColumns++; - } - } - } - - assertTrue(wideTunnelColumns > narrowTunnelColumns * 2); - assertEquals(narrowSurfaceColumns, wideSurfaceColumns); - } - } - - @Test - public void terminalTaperUsesMeasuredReachLength() { - assertEquals(1D, IrisRiverRuntime.terminalWeight(40, 200D, 0.8D), 0D); - assertEquals(0.5D, IrisRiverRuntime.terminalWeight(40, 200D, 0.9D), 0.0000001D); - assertEquals(0.5D, IrisRiverRuntime.terminalWeight(40, 20D, 0.5D), 0.0000001D); - } - - @Test - public void footprintSamplingBuildsOnlyItsCenterTile() { - IrisRiverNetwork configuration = configuration(false); - - try (IrisRiverRuntime runtime = runtime(configuration)) { - runtime.sampleFootprint(-4096D, -4096D, 4096D, 4096D); - - assertEquals(1, runtime.completedTileCount()); - } - } - - @Test - public void footprintPresenceQueryDistinguishesEmptyAndRoutedAreas() { - IrisRiverNetwork emptyConfiguration = configuration(false); - emptyConfiguration.getTopology().getSource().setChance(0D); - - try (IrisRiverRuntime empty = runtime(emptyConfiguration); - IrisRiverRuntime routed = runtime(configuration(false))) { - assertFalse(empty.hasRiverFootprint(-128, -128, 128, 128)); - assertTrue(routed.hasRiverFootprint(-128, -128, 128, 128)); - } - } - - @Test - public void settingsAtSkipsNaturalBiomeWhenBiomeOverridesAreUnreachable() { - IrisRiverNetwork configuration = configuration(false); - IrisRegion region = new IrisRegion().setRiverOverride( - new IrisRiverOverride().setWidthMultiplier(1.75D) - ); - IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND); - AtomicInteger biomeSamples = new AtomicInteger(); - - try (IrisRiverRuntime runtime = runtime( - configuration, - constantHeight(80D), - countedBiome(biome, biomeSamples), - region, - false, - true, - true, - false - )) { - EffectiveRiverSettings settings = runtime.settingsAt(12D, -7D); - - assertEquals(1.75D, settings.widthMultiplier(), 0D); - assertEquals(0, biomeSamples.get()); - } - } - - @Test - public void settingsAtSamplesNaturalBiomeWhenBiomeOverrideIsReachable() { - IrisRiverNetwork configuration = configuration(false); - IrisRegion region = new IrisRegion().setRiverOverride( - new IrisRiverOverride().setWidthMultiplier(1.75D) - ); - IrisBiome biome = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setWidthMultiplier(2.5D)); - AtomicInteger biomeSamples = new AtomicInteger(); - - try (IrisRiverRuntime runtime = runtime( - configuration, - constantHeight(80D), - countedBiome(biome, biomeSamples), - region, - false, - true, - true, - true - )) { - EffectiveRiverSettings settings = runtime.settingsAt(12D, -7D); - - assertEquals(2.5D, settings.widthMultiplier(), 0D); - assertEquals(1, biomeSamples.get()); - } - } - - @Test - public void nodeSamplingResolvesInputsOnceAndSkipsZeroWeightSlope() { - IrisRiverNetwork configuration = configuration(false); - configuration.getTopology().setTerrainHeightWeight(0D); - configuration.getTopology().setTerrainSlopeWeight(0D); - IrisRegion region = new IrisRegion().setRiverOverride( - new IrisRiverOverride().setRoutingCostMultiplier(2D) - ); - IrisBiome biome = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride() - .setRoutingPolicy(IrisRiverRoutingPolicy.BLOCK) - .setRoutingCostMultiplier(0D)); - AtomicInteger heightSamples = new AtomicInteger(); - AtomicInteger slopeSamples = new AtomicInteger(); - AtomicInteger oceanSamples = new AtomicInteger(); - AtomicInteger biomeSamples = new AtomicInteger(); - AtomicInteger regionSamples = new AtomicInteger(); - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> { - heightSamples.incrementAndGet(); - return 80D; - }); - ProceduralStream slope = ProceduralStream.ofDouble((x, z) -> { - slopeSamples.incrementAndGet(); - return 0.25D; - }); - ProceduralStream ocean = ProceduralStream.of((x, z) -> { - oceanSamples.incrementAndGet(); - return false; - }, Interpolated.BOOLEAN); - ProceduralStream biomes = countedBiome(biome, biomeSamples); - ProceduralStream regions = ProceduralStream.of( - (x, z) -> { - regionSamples.incrementAndGet(); - return region; - }, - Interpolated.of(value -> 0D, value -> region) - ); - - try (IrisRiverRuntime runtime = new IrisRiverRuntime(new IrisRiverRuntimeContext( - 4829759234L, - configuration, - mock(IrisData.class), - 63, - 63, - false, - true, - true, - true, - true, - (x, z) -> new NoiseBounds(0D, 512D), - height, - slope, - ocean, - biomes, - regions - ))) { - RiverTerrainNodeSample sample = runtime.sampleNode(12, -7); - RiverTerrainSourceSample sourceSample = runtime.sampleSource(13, -8); - - assertEquals(63D, sample.naturalHeight(), 0D); - assertFalse(sample.ocean()); - assertFalse(sample.riverAllowed()); - assertEquals(0D, sample.routingCost(), 0D); - assertFalse(sourceSample.ocean()); - assertFalse(sourceSample.riverAllowed()); - assertEquals(0, heightSamples.get()); - assertEquals(0, slopeSamples.get()); - assertEquals(2, oceanSamples.get()); - assertEquals(2, biomeSamples.get()); - assertEquals(2, regionSamples.get()); - } - } - - @Test - public void oceanIntentOnlyTerminatesRiversWhereTheNaturalSurfaceIsSubmerged() { - IrisRiverNetwork configuration = configuration(false); - configuration.getTopology().setTerrainHeightWeight(0D); - IrisBiome oceanBiome = new IrisBiome().setInferredType(InferredType.SEA); - IrisRegion region = new IrisRegion(); - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 200D); - ProceduralStream oceanIntent = ProceduralStream.of( - (x, z) -> true, - Interpolated.BOOLEAN - ); - ProceduralStream biomes = ProceduralStream.of( - (x, z) -> oceanBiome, - Interpolated.of(value -> 0D, value -> oceanBiome) - ); - ProceduralStream regions = ProceduralStream.of( - (x, z) -> region, - Interpolated.of(value -> 0D, value -> region) - ); - - try (IrisRiverRuntime runtime = new IrisRiverRuntime(new IrisRiverRuntimeContext( - 4829759234L, - configuration, - mock(IrisData.class), - 63, - 63, - true, - true, - false, - false, - false, - (x, z) -> new NoiseBounds(0D, 512D), - height, - ProceduralStream.ofDouble((x, z) -> 0D), - oceanIntent, - biomes, - regions - ))) { - assertTrue(runtime.sampleNode(-1, 0).ocean()); - assertFalse(runtime.sampleNode(1, 0).ocean()); - assertTrue(runtime.sampleSource(-1, 0).ocean()); - assertFalse(runtime.sampleSource(1, 0).ocean()); - } - } - - @Test - public void wetTerminalRiverCarvesDownAndPublishesAFluidHead() { - IrisRiverNetwork configuration = configuration(false); - - try (IrisRiverRuntime runtime = runtime(configuration)) { - IrisRiverSurfaceSample sample = findRiver(runtime, RiverRouteState.WET); - - assertNotNull(sample); - assertTrue(sample.river().present()); - assertTrue(sample.surfaceFluid()); - assertTrue(sample.terrainHeight() <= sample.naturalHeight()); - assertTrue(sample.waterSurfaceY() >= sample.terrainHeight()); - assertTrue(runtime.completedTileCount() <= 32); - } - } - - @Test - public void failedOceanRouteCanProduceDryTerrainWithoutSurfaceWater() { - IrisRiverNetwork configuration = configuration(true); - - try (IrisRiverRuntime runtime = runtime(configuration)) { - IrisRiverSurfaceSample sample = findRiver(runtime, RiverRouteState.DRY); - - assertNotNull(sample); - assertFalse(sample.surfaceFluid()); - assertTrue(sample.terrainHeight() <= sample.naturalHeight()); - assertTrue(sample.waterSurfaceY() == sample.terrainHeight()); - } - } - - @Test - public void localSinkholeOverrideMakesRequiredOceanTerminalWet() { - IrisRiverNetwork configuration = configuration(true); - IrisBiome land = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setTerminalMode(IrisRiverTerminalMode.SINKHOLE_GROTTO)); - - try (IrisRiverRuntime runtime = runtime(configuration, land, new IrisRegion())) { - IrisRiverSurfaceSample sample = findRiver(runtime, RiverRouteState.WET); - - assertNotNull(sample); - assertTrue(sample.surfaceFluid()); - } - } - - @Test - public void sinkholeTerminalPublishesAGuaranteedSpecificCaveAnchor() { - IrisRiverNetwork configuration = configuration(true); - configuration.getTopology().setMaxRouteReaches(1); - configuration.getCaves() - .setMode(IrisRiverCaveMode.GROTTO_OR_CLOSED_COMPONENT) - .setMaximumPerReach(1); - configuration.getCaves().getEntry() - .setChance(0D) - .setInfluence(0D) - .setStyle(flat()); - IrisBiome land = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setTerminalMode(IrisRiverTerminalMode.SINKHOLE_GROTTO)); - - try (IrisRiverRuntime runtime = runtime(configuration, land, new IrisRegion())) { - List anchors = runtime.candidateAnchors(-128, -128, 256, 256, 16D, 994L); - RiverAnchor terminal = null; - for (RiverAnchor anchor : anchors) { - if (runtime.isTerminalCaveAnchor(anchor)) { - terminal = anchor; - break; - } - } - - assertNotNull(terminal); - assertTrue(runtime.acceptsCaveAnchor(terminal)); - IrisRiverSurfaceSample terminalSample = runtime.sample( - StrictMath.floor(terminal.x()), - StrictMath.floor(terminal.z()) - ); - assertTrue(terminalSample.surfaceFluid()); - assertTrue(Math.round(terminalSample.terrainHeight()) - < Math.round(terminalSample.waterSurfaceY())); - for (RiverAnchor anchor : anchors) { - if (anchor.reachId().equals(terminal.reachId()) - && !runtime.isTerminalCaveAnchor(anchor)) { - assertFalse(runtime.acceptsCaveAnchor(anchor)); - } - } - configuration.getCaves().setMaximumPerReach(0); - assertFalse(runtime.acceptsCaveAnchor(terminal)); - } - } - - @Test - public void localSuppressOverrideRemovesDimensionSinkholeTerminalRoute() { - IrisRiverNetwork configuration = configuration(true); - configuration.getTerrain().setTerminalMode(IrisRiverTerminalMode.SINKHOLE_GROTTO); - IrisBiome land = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setTerminalMode(IrisRiverTerminalMode.SUPPRESS)); - - try (IrisRiverRuntime runtime = runtime(configuration, land, new IrisRegion())) { - assertFalse(hasRiver(runtime)); - } - } - - @Test - public void inactiveCaveHydrologySuppressesLocalSinkholeTerminalRoute() { - IrisRiverNetwork configuration = configuration(true); - IrisBiome land = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setTerminalMode(IrisRiverTerminalMode.SINKHOLE_GROTTO)); - - try (IrisRiverRuntime runtime = runtime(configuration, land, new IrisRegion(), false)) { - assertFalse(hasRiver(runtime)); - } - } - - @Test - public void caveEntryGateHonorsWetStateAndMaximumPerReach() { - IrisRiverNetwork configuration = configuration(false); - configuration.getCaves().setMode(IrisRiverCaveMode.FLOOD_CLOSED_COMPONENT); - configuration.getCaves().setMaximumPerReach(1); - configuration.getCaves().getEntry() - .setChance(0.35D) - .setInfluence(0D) - .setStyle(flat()); - - try (IrisRiverRuntime runtime = runtime(configuration)) { - List anchors = runtime.candidateAnchors(0, 0, 256, 256, 16D, 773L); - Map acceptedPerReach = new HashMap<>(); - boolean acceptedAfterRejectedRawIndex = false; - for (RiverAnchor anchor : anchors) { - if (runtime.acceptsCaveAnchor(anchor)) { - acceptedPerReach.merge(anchor.reachId(), 1, Integer::sum); - acceptedAfterRejectedRawIndex |= anchor.index() >= 1; - } - } - - assertFalse(acceptedPerReach.isEmpty()); - for (int accepted : acceptedPerReach.values()) { - assertEquals(1, accepted); - } - assertTrue(acceptedAfterRejectedRawIndex); - } - } - - @Test - public void deepPoolReachGateIsIndependentSparseAndReachLimited() { - IrisRiverNetwork configuration = configuration(false); - configuration.getCaves().getDeepPools() - .setEnabled(true) - .setMaximumPerReach(1); - configuration.getCaves().getDeepPools().getReach() - .setChance(1D) - .setInfluence(0D) - .setStyle(flat()); - - try (IrisRiverRuntime runtime = runtime(configuration)) { - List anchors = runtime.candidateAnchors(0, 0, 256, 256, 16D, 882L); - Map acceptedPerReach = new HashMap<>(); - for (RiverAnchor anchor : anchors) { - if (runtime.acceptsDeepPoolAnchor(anchor)) { - acceptedPerReach.merge(anchor.reachId(), 1, Integer::sum); - } - } - - assertFalse(acceptedPerReach.isEmpty()); - for (int accepted : acceptedPerReach.values()) { - assertEquals(1, accepted); - } - configuration.getCaves().getDeepPools().setEnabled(false); - for (RiverAnchor anchor : anchors) { - assertFalse(runtime.acceptsDeepPoolAnchor(anchor)); - } - } - } - - @Test - public void finalPolylineSupercoverRejectsOneBlockedColumnMissedByWidthSpacing() { - IrisRiverNetwork configuration = configuration(false); - IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND); - IrisBiome blocked = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setRoutingPolicy(IrisRiverRoutingPolicy.BLOCK)); - ProceduralStream biomes = ProceduralStream.of( - (x, z) -> x == 1D && z == 0D ? blocked : land, - Interpolated.of(value -> 0D, value -> land) - ); - - try (IrisRiverRuntime runtime = runtime(configuration, constantHeight(80D), biomes)) { - assertFalse(runtime.allowsReach(straightReach(80D, 79D))); - } - } - - @Test - public void finalPolylineSupercoverRejectsOneUnincisableTerrainSpike() { - IrisRiverNetwork configuration = configuration(false); - configuration.getTerrain().setMaxIncision(48); - ProceduralStream height = ProceduralStream.of( - (x, z) -> x == 1D && z == 0D ? 200D : 80D, - Interpolated.DOUBLE - ); - - try (IrisRiverRuntime runtime = runtime(configuration, height, constantLandBiome())) { - assertFalse(runtime.allowsReach(straightReach(80D, 79D))); - } - } - - @Test - public void longReachFeasibilityHasADeterministicSampleCeiling() { - IrisRiverNetwork configuration = configuration(false); - int[] heightSamples = new int[1]; - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> { - heightSamples[0]++; - return 80D; - }); - - try (IrisRiverRuntime runtime = runtime(configuration, height, constantLandBiome())) { - heightSamples[0] = 0; - - assertTrue(runtime.allowsReach(straightReach(80D, 79D, 4_096D))); - assertEquals(IrisRiverRuntime.MAXIMUM_REACH_FEASIBILITY_SAMPLES, heightSamples[0]); - } - } - - @Test - public void invariantFeasibilitySettingsAreResolvedOncePerReach() { - IrisRiverNetwork configuration = configuration(false); - AtomicInteger regionSamples = new AtomicInteger(); - IrisRegion region = new IrisRegion(); - ProceduralStream regions = ProceduralStream.of( - (x, z) -> { - regionSamples.incrementAndGet(); - return region; - }, - Interpolated.of(value -> 0D, value -> region) - ); - ProceduralStream height = constantHeight(80D); - ProceduralStream biome = constantLandBiome(); - ProceduralStream oceans = ProceduralStream.of( - (x, z) -> false, - Interpolated.BOOLEAN - ); - try (IrisRiverRuntime runtime = new IrisRiverRuntime(new IrisRiverRuntimeContext( - 4829759234L, - configuration, - mock(IrisData.class), - 63, - 63, - false, - true, - false, - false, - false, - (x, z) -> new NoiseBounds(0D, 512D), - height, - ProceduralStream.ofDouble((x, z) -> 0.025D), - oceans, - biome, - regions - ))) { - assertTrue(runtime.allowsReach(straightReach(80D, 79D, 4_096D))); - assertEquals(1, regionSamples.get()); - } - } - - @Test - public void feasibilityBoundsMatchFullIncisionAcrossRoundingBoundaries() { - double[] heads = new double[]{62.5D, Math.nextDown(62.5D), Math.nextUp(62.5D), 63D}; - double[] heights = new double[]{62.49D, 62.5D, 63D, 80D}; - double[] incisions = new double[]{0D, 0.5D, 16D, 512D}; - double[] depths = new double[]{0.25D, 1D, 4D}; - double[] roughnessBounds = new double[]{0D, 0.75D, 2D}; - double[] bedNoiseValues = new double[]{-1D, 0D, 1D}; - for (double head : heads) { - for (double naturalHeight : heights) { - for (double maximumIncision : incisions) { - for (double depth : depths) { - for (double roughnessBound : roughnessBounds) { - int decision = IrisRiverRuntime.boundedFeasibility( - naturalHeight, - head, - maximumIncision, - depth, - roughnessBound); - if (decision == IrisRiverRuntime.FEASIBILITY_SAMPLE_BED) { - continue; - } - for (double bedNoise : bedNoiseValues) { - double bedHeight = head - depth + roughnessBound * bedNoise; - double finalHeight = Math.min( - naturalHeight, - Math.max( - bedHeight, - naturalHeight - Math.max(0D, maximumIncision))); - boolean expected = Math.round(finalHeight) < Math.round(head); - assertEquals(expected, decision == IrisRiverRuntime.FEASIBILITY_ACCEPT); - } - } - } - } - } - } - } - - @Test - public void mantleBoreSkipsSurfaceFeasibilitySampling() { - IrisRiverNetwork configuration = configuration(false); - int[] heightSamples = new int[1]; - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> { - heightSamples[0]++; - return 512D; - }); - - try (IrisRiverRuntime runtime = runtime( - configuration, - height, - constantLandBiome(), - new IrisRegion(), - true, - false, - false, - true - )) { - heightSamples[0] = 0; - - assertTrue(runtime.allowsReach(straightReach(80D, 79D, 4_096D))); - assertEquals(0, heightSamples[0]); - } - } - - @Test - public void mantleBoreStillHonorsBlockedRoutingPolicy() { - IrisRiverNetwork configuration = configuration(false); - IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND); - IrisBiome blocked = new IrisBiome() - .setInferredType(InferredType.LAND) - .setRiverOverride(new IrisRiverOverride().setRoutingPolicy(IrisRiverRoutingPolicy.BLOCK)); - ProceduralStream biomes = ProceduralStream.of( - (x, z) -> x == 1D && z == 0D ? blocked : land, - Interpolated.of(value -> 0D, value -> land) - ); - - try (IrisRiverRuntime runtime = runtime( - configuration, - constantHeight(512D), - biomes, - new IrisRegion(), - true, - false, - true, - true - )) { - assertFalse(runtime.allowsReach(straightReach(80D, 79D))); - } - } - - @Test - public void regionalDepthMultiplierCannotCollapseWetChannelBelowOneBlock() { - IrisRiverNetwork configuration = configuration(false); - configuration.getTerrain() - .setDepth(range(2D)) - .setBedRoughness(0.65D) - .setBedRoughnessStyle(flat()); - IrisRegion shallow = new IrisRegion() - .setRiverOverride(new IrisRiverOverride().setDepthMultiplier(0.1D)); - - try (IrisRiverRuntime runtime = runtime( - configuration, - constantHeight(80D), - constantLandBiome(), - shallow - )) { - assertTrue(runtime.allowsReach(straightReach(80D, 79D))); - } - } - - @Test - public void incisionLimitTurnsADeepWetReachIntoAHiddenMantleTunnel() { - IrisRiverNetwork configuration = configuration(false); - configuration.getTerrain().setMaxIncision(10); - ProceduralStream height = constantHeight(100D); - - try (IrisRiverRuntime runtime = runtime( - configuration, - height, - constantLandBiome(), - new IrisRegion(), - true, - false - )) { - IrisRiverSurfaceSample sample = null; - IrisRiverTunnelSample tunnel = null; - for (int x = -128; x < 192 && tunnel == null; x += 2) { - for (int z = -128; z < 192; z += 2) { - IrisRiverSurfaceSample candidate = runtime.sample(x, z); - IrisRiverTunnelSample candidateTunnel = runtime.sampleTunnel(x, z); - if (candidate.river().present() - && candidate.river().state() == RiverRouteState.WET - && candidateTunnel != null) { - sample = candidate; - tunnel = candidateTunnel; - break; - } - } - } - - assertNotNull(sample); - assertTrue(sample.subterranean()); - assertFalse(sample.surfaceFluid()); - assertEquals(sample.naturalHeight(), sample.terrainHeight(), 0D); - assertNotNull(tunnel); - assertTrue(tunnel.bedY() < tunnel.waterHeadY()); - assertTrue(tunnel.ceilingY() >= tunnel.waterHeadY()); - } - } - - @Test - public void cliffTransitionCreatesAFlaredTunnelMouth() { - IrisRiverNetwork sealedConfiguration = configuration(false); - sealedConfiguration.getTerrain() - .setMaxIncision(10) - .setTunnelMouthBlend(0D); - IrisRiverNetwork mouthConfiguration = configuration(false); - mouthConfiguration.getTerrain() - .setMaxIncision(10) - .setTunnelMouthBlend(6D); - ProceduralStream height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 100D); - - try (IrisRiverRuntime sealed = runtime( - sealedConfiguration, - height, - constantLandBiome(), - new IrisRegion(), - true, - false - ); IrisRiverRuntime mouth = runtime( - mouthConfiguration, - height, - constantLandBiome(), - new IrisRegion(), - true, - false - )) { - boolean transitionFound = false; - for (int z = -2048; z <= 2048 && !transitionFound; z++) { - IrisRiverSurfaceSample open = mouth.sample(-1, z); - IrisRiverSurfaceSample solid = mouth.sample(0, z); - IrisRiverTunnelSample tunnel = mouth.sampleTunnel(0, z); - IrisRiverTunnelSample unflared = sealed.sampleTunnel(0, z); - if (open.river().present() - && solid.river().present() - && open.river().reachId().equals(solid.river().reachId()) - && open.surfaceFluid() - && tunnel != null - && (unflared == null || tunnel.ceilingY() > unflared.ceilingY())) { - assertFalse(open.subterranean()); - assertTrue(solid.subterranean()); - transitionFound = true; - } - } - - assertTrue(transitionFound); - } - } - - @Test - public void terracedWaterUsesFixedInteriorPoolsAndPreservesNodeHeads() { - IrisRiverNetwork configuration = configuration(false); - configuration.getWater() - .setMode(IrisRiverWaterMode.TERRACED) - .setMaximumPoolRise(8) - .setDropHeight(2) - .setPoolLength(96); - - try (IrisRiverRuntime runtime = runtime(configuration)) { - assertEquals(71D, runtime.terracedWaterSurface(72D, 64D, 600D, 155D / 600D), 0D); - assertEquals(69D, runtime.terracedWaterSurface(72D, 64D, 600D, 156D / 600D), 0D); - assertEquals(69D, runtime.terracedWaterSurface(72D, 64D, 600D, 251D / 600D), 0D); - assertEquals(67D, runtime.terracedWaterSurface(72D, 64D, 600D, 252D / 600D), 0D); - assertEquals(65D, runtime.terracedWaterSurface(72D, 64D, 600D, 443D / 600D), 0D); - assertEquals(63D, runtime.terracedWaterSurface(72D, 64D, 600D, 444D / 600D), 0D); - assertEquals(63D, runtime.terracedWaterSurface(72D, 64D, 600D, 1D), 0D); - assertEquals( - runtime.terracedWaterSurface(72D, 64D, 600D, 1D), - runtime.terracedWaterSurface(64D, 63D, 600D, 0D), - 0D - ); - assertEquals(67D, runtime.terracedWaterSurface(68D, 64D, 100D, 0.33D), 0D); - assertEquals(65D, runtime.terracedWaterSurface(68D, 64D, 100D, 0.34D), 0D); - assertEquals(65D, runtime.terracedWaterSurface(68D, 64D, 100D, 0.66D), 0D); - assertEquals(63D, runtime.terracedWaterSurface(68D, 64D, 100D, 0.67D), 0D); - } - } - - @Test - public void terracedRiverMouthCannotRiseAboveNaturalOcean() { - IrisRiverNetwork configuration = configuration(false); - configuration.getWater() - .setMode(IrisRiverWaterMode.TERRACED) - .setMaximumPoolRise(8) - .setDropHeight(1) - .setPoolLength(128); - RiverNode from = new RiverNode( - new RiverNodeId(0L, 0L), - -600D, - 0D, - 72D, - 72D, - 2D, - 0D, - false, - true - ); - RiverNode to = new RiverNode( - new RiverNodeId(1L, 0L), - 0D, - 0D, - 63D, - 63D, - -Double.MAX_VALUE, - 0D, - true, - true - ); - RiverReach mouth = new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.WET, - 1, - 1, - 4D, - 2D, - 4D, - RiverBodyProfile.constant(4D, 2D, 4D), - true, - false, - new RiverPolyline(new double[]{-600D, 0D}, new double[]{0D, 0D}) - ); - - try (IrisRiverRuntime runtime = runtime( - configuration, - constantHeight(80D), - constantLandBiome(), - new IrisRegion() - )) { - assertEquals(71D, runtime.waterSurface(mouth, 0.1D, false), 0D); - assertEquals(63D, runtime.waterSurface(mouth, 0.9D, true), 0D); - } - } - - @Test - public void fixedRiverHeightIsIndependentFromNaturalOceanHeight() { - IrisRiverNetwork configuration = configuration(false); - configuration.getWater() - .setMode(IrisRiverWaterMode.FIXED) - .setFluidHeight(-48); - - try (IrisRiverRuntime runtime = runtimeWithFluidHeights(configuration, -48, 63)) { - assertEquals(-48D, runtime.waterSurface(null, 0D, false), 0D); - assertEquals(63D, runtime.waterSurface(null, 0D, true), 0D); - } - } - - private static IrisRiverRuntime runtime(IrisRiverNetwork configuration) { - IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND); - IrisRegion region = new IrisRegion(); - return runtime(configuration, land, region); - } - - private static IrisRiverRuntime runtime(IrisRiverNetwork configuration, IrisBiome land, IrisRegion region) { - return runtime(configuration, land, region, true); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - IrisBiome land, - IrisRegion region, - boolean caveHydrologyActive - ) { - ProceduralStream height = ProceduralStream.of( - (x, z) -> 180D - x * 0.01D - z * 0.015D, - Interpolated.DOUBLE - ); - ProceduralStream biome = ProceduralStream.of( - (x, z) -> land, - Interpolated.of(value -> 0D, value -> land) - ); - return runtime(configuration, height, biome, region, caveHydrologyActive); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - ProceduralStream height, - ProceduralStream biome - ) { - return runtime(configuration, height, biome, new IrisRegion()); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - ProceduralStream height, - ProceduralStream biome, - IrisRegion region - ) { - return runtime(configuration, height, biome, region, true); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - ProceduralStream height, - ProceduralStream biome, - IrisRegion region, - boolean caveHydrologyActive - ) { - return runtime(configuration, height, biome, region, false, caveHydrologyActive); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - ProceduralStream height, - ProceduralStream biome, - IrisRegion region, - boolean boreMantleActive, - boolean caveHydrologyActive - ) { - return runtime( - configuration, - height, - biome, - region, - boreMantleActive, - caveHydrologyActive, - true, - true - ); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - ProceduralStream height, - ProceduralStream biome, - IrisRegion region, - boolean boreMantleActive, - boolean caveHydrologyActive, - boolean blockingRoutingPossible, - boolean biomeRiverOverridesPossible - ) { - return runtime( - configuration, - height, - biome, - region, - boreMantleActive, - caveHydrologyActive, - blockingRoutingPossible, - biomeRiverOverridesPossible, - 63, - 63 - ); - } - - private static IrisRiverRuntime runtimeWithFluidHeights( - IrisRiverNetwork configuration, - int riverFluidHeight, - int dimensionFluidHeight - ) { - IrisBiome biome = new IrisBiome().setInferredType(InferredType.LAND); - return runtime( - configuration, - constantHeight(80D), - ProceduralStream.of( - (x, z) -> biome, - Interpolated.of(value -> 0D, value -> biome) - ), - new IrisRegion(), - false, - true, - true, - true, - riverFluidHeight, - dimensionFluidHeight - ); - } - - private static IrisRiverRuntime runtime( - IrisRiverNetwork configuration, - ProceduralStream height, - ProceduralStream biome, - IrisRegion region, - boolean boreMantleActive, - boolean caveHydrologyActive, - boolean blockingRoutingPossible, - boolean biomeRiverOverridesPossible, - int riverFluidHeight, - int dimensionFluidHeight - ) { - ProceduralStream slope = ProceduralStream.ofDouble((x, z) -> 0.025D); - ProceduralStream oceans = ProceduralStream.of( - (x, z) -> height.getDouble(x, z) < 62D, - Interpolated.BOOLEAN - ); - ProceduralStream regions = ProceduralStream.of( - (x, z) -> region, - Interpolated.of(value -> 0D, value -> region) - ); - return new IrisRiverRuntime(new IrisRiverRuntimeContext( - 4829759234L, - configuration, - mock(IrisData.class), - riverFluidHeight, - dimensionFluidHeight, - boreMantleActive, - caveHydrologyActive, - blockingRoutingPossible, - true, - biomeRiverOverridesPossible, - (x, z) -> new NoiseBounds(0D, 512D), - height, - slope, - oceans, - biome, - regions - )); - } - - private static RiverRoutingContext straightReach(double fromHeight, double toHeight) { - return straightReach(fromHeight, toHeight, 32D); - } - - private static RiverRoutingContext straightReach(double fromHeight, double toHeight, double length) { - RiverNode from = new RiverNode( - new RiverNodeId(0L, 0L), - 0D, - 0D, - fromHeight, - fromHeight, - fromHeight, - fromHeight, - false, - true - ); - RiverNode to = new RiverNode( - new RiverNodeId(1L, 0L), - length, - 0D, - toHeight, - toHeight, - toHeight, - toHeight, - false, - true - ); - return new RiverRoutingContext( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - new RiverPolyline(new double[]{0D, length}, new double[]{0D, 0D}) - ); - } - - private static ProceduralStream constantHeight(double height) { - return ProceduralStream.of((x, z) -> height, Interpolated.DOUBLE); - } - - private static ProceduralStream constantLandBiome() { - IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND); - return ProceduralStream.of( - (x, z) -> land, - Interpolated.of(value -> 0D, value -> land) - ); - } - - private static ProceduralStream countedBiome(IrisBiome biome, AtomicInteger samples) { - return ProceduralStream.of( - (x, z) -> { - samples.incrementAndGet(); - return biome; - }, - Interpolated.of(value -> 0D, value -> biome) - ); - } - - private static IrisRiverNetwork configuration(boolean requireOcean) { - IrisRiverNetwork configuration = new IrisRiverNetwork().setEnabled(true); - configuration.getTopology() - .setCellSize(64) - .setTileCells(1) - .setSiteJitter(0D) - .setMaxRouteReaches(4) - .setSinkSearchReaches(3) - .setRequireOcean(requireOcean); - configuration.getTopology().getSource() - .setChance(1D) - .setInfluence(0D) - .setStyle(flat()); - configuration.getTopology().getContinuation() - .setChance(1D) - .setInfluence(0D) - .setStyle(flat()); - configuration.getTopology().setRoutingStyle(flat()); - configuration.getTerrain() - .setChannelWidth(range(24D)) - .setBankWidth(range(12D)) - .setDepth(range(5D)) - .setMaxIncision(512) - .setWorms(new KList<>(new IrisRiverWorm() - .setSeed(1L) - .setTortuosity(0D) - .setDetailTortuosity(0D) - .setMaxOffset(0D) - .setSegments(1))) - .setBedRoughness(0D) - .setBedRoughnessStyle(flat()) - .setDryContinuationChance(1D); - configuration.getTerrain().getIncision() - .setChance(1D) - .setInfluence(0D) - .setStyle(flat()); - configuration.getBiomes().setSelectionStyle(flat()); - return configuration; - } - - private static IrisRiverSurfaceSample findRiver(IrisRiverRuntime runtime, RiverRouteState state) { - for (int x = -128; x < 192; x += 2) { - for (int z = -128; z < 192; z += 2) { - IrisRiverSurfaceSample sample = runtime.sample(x, z); - if (sample.river().present() && sample.river().state() == state) { - return sample; - } - } - } - return null; - } - - private static boolean hasRiver(IrisRiverRuntime runtime) { - for (int x = 0; x < 64; x += 2) { - for (int z = 0; z < 64; z += 2) { - if (runtime.sample(x, z).river().present()) { - return true; - } - } - } - return false; - } - - private static IrisStyledRange range(double value) { - return new IrisStyledRange(value, value, flat()); - } - - private static IrisGeneratorStyle flat() { - return new IrisGeneratorStyle(NoiseStyle.FLAT); - } -} diff --git a/core/src/test/java/art/arcane/iris/util/project/matter/slices/RiverCaveHydrologyMatterTest.java b/core/src/test/java/art/arcane/iris/util/project/matter/slices/RiverCaveHydrologyMatterTest.java deleted file mode 100644 index c0008d65f..000000000 --- a/core/src/test/java/art/arcane/iris/util/project/matter/slices/RiverCaveHydrologyMatterTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package art.arcane.iris.util.project.matter.slices; - -import art.arcane.iris.engine.river.cave.RiverCaveAction; -import art.arcane.iris.engine.river.cave.RiverCaveFluidKind; -import art.arcane.iris.engine.river.cave.RiverCaveHydrology; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -public class RiverCaveHydrologyMatterTest { - @Test - public void everyActionAndBiomeRoundTrips() throws IOException { - RiverCaveHydrologyMatter matter = new RiverCaveHydrologyMatter(); - for (RiverCaveAction action : RiverCaveAction.values()) { - for (RiverCaveFluidKind fluidKind : RiverCaveFluidKind.values()) { - RiverCaveHydrology expected = new RiverCaveHydrology( - action, - "iris:flooded_grotto", - fluidKind - ); - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - matter.writeNode(expected, new DataOutputStream(bytes)); - - RiverCaveHydrology actual = matter.readNode( - new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))); - - assertEquals(expected, actual); - } - } - } - - @Test - public void emptyBiomeAndInvalidActionRemainUnambiguous() throws IOException { - RiverCaveHydrologyMatter matter = new RiverCaveHydrologyMatter(); - RiverCaveHydrology expected = RiverCaveHydrology.of(RiverCaveAction.DRY_AIR); - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - matter.writeNode(expected, new DataOutputStream(bytes)); - - assertEquals(expected, matter.readNode( - new DataInputStream(new ByteArrayInputStream(bytes.toByteArray())))); - assertThrows(IOException.class, () -> matter.readNode( - new DataInputStream(new ByteArrayInputStream(new byte[]{99})))); - assertThrows(IOException.class, () -> matter.readNode( - new DataInputStream(new ByteArrayInputStream(new byte[]{1, 99})))); - } -} diff --git a/probe/src/main/java/art/arcane/iris/probe/RiverTileProbe.java b/probe/src/main/java/art/arcane/iris/probe/RiverTileProbe.java deleted file mode 100644 index 4c141e7b4..000000000 --- a/probe/src/main/java/art/arcane/iris/probe/RiverTileProbe.java +++ /dev/null @@ -1,157 +0,0 @@ -package art.arcane.iris.probe; - -import art.arcane.iris.engine.river.RiverBodyProfile; -import art.arcane.iris.engine.river.RiverEdgeId; -import art.arcane.iris.engine.river.RiverNode; -import art.arcane.iris.engine.river.RiverNodeId; -import art.arcane.iris.engine.river.RiverPolyline; -import art.arcane.iris.engine.river.RiverReach; -import art.arcane.iris.engine.river.RiverRouteState; -import art.arcane.iris.engine.river.RiverSample; -import art.arcane.iris.engine.river.RiverTile; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public final class RiverTileProbe { - private static final int WARMUP_ROUNDS = 5; - private static final int MEASURED_ROUNDS = 25; - private static final int SAMPLES_PER_ROUND = 8_192; - - private RiverTileProbe() { - } - - public static void main(String[] args) { - RiverTile tile = createTile(); - long expectedSignature = sampleRound(tile); - for (int round = 0; round < WARMUP_ROUNDS; round++) { - requireSignature(expectedSignature, sampleRound(tile)); - } - - long[] timings = new long[MEASURED_ROUNDS]; - for (int round = 0; round < MEASURED_ROUNDS; round++) { - long start = System.nanoTime(); - long signature = sampleRound(tile); - timings[round] = System.nanoTime() - start; - requireSignature(expectedSignature, signature); - System.out.printf(Locale.ROOT, - "IRIS_RIVER_TILE_SAMPLE round=%d nanos=%d signature=%016x%n", - round, - timings[round], - signature); - } - long[] sorted = timings.clone(); - Arrays.sort(sorted); - System.out.printf(Locale.ROOT, - "IRIS_RIVER_TILE_RESULT version=1 rounds=%d samples_per_round=%d median_nanos=%d p95_nanos=%d signature=%016x%n", - MEASURED_ROUNDS, - SAMPLES_PER_ROUND, - sorted[MEASURED_ROUNDS / 2], - sorted[(int) StrictMath.ceil(MEASURED_ROUNDS * 0.95D) - 1], - expectedSignature); - } - - private static RiverTile createTile() { - List reaches = new ArrayList<>(64); - for (int reachIndex = 0; reachIndex < 64; reachIndex++) { - RiverNode from = node(0L, reachIndex, 0D, reachIndex * 32D); - RiverNode to = node(1L, reachIndex, 2_048D, reachIndex * 32D); - RiverBodyProfile profile = profile(reachIndex); - double[] x = new double[33]; - double[] z = new double[33]; - for (int point = 0; point < x.length; point++) { - x[point] = point * 64D; - z[point] = reachIndex * 32D - + StrictMath.sin(point * 0.625D + reachIndex * 0.25D) * 12D; - } - reaches.add(new RiverReach( - RiverEdgeId.of(from.id(), to.id()), - from, - to, - RiverRouteState.WET, - 1 + reachIndex % 4, - 1 + reachIndex % 3, - profile.maximumWidth(), - profile.maximumBankWidth(), - profile.maximumDepth(), - profile, - false, - false, - new RiverPolyline(x, z) - )); - } - return new RiverTile(0, 0, 0, 0, 2_048, 2_048, reaches); - } - - private static RiverBodyProfile profile(int reachIndex) { - double[] positions = new double[17]; - double[] widths = new double[17]; - double[] bankWidths = new double[17]; - double[] depths = new double[17]; - double[] roofScales = new double[17]; - for (int index = 0; index < positions.length; index++) { - double position = index / 16D; - double body = StrictMath.sin(StrictMath.PI * position); - positions[index] = position; - widths[index] = 8D + reachIndex % 5 + body * 6D; - bankWidths[index] = 4D + reachIndex % 3 + body * 4D; - depths[index] = 3D + reachIndex % 2 + body * 2D; - roofScales[index] = 1D - body * 0.4D; - } - return new RiverBodyProfile(positions, widths, bankWidths, depths, roofScales); - } - - private static RiverNode node(long cellX, long cellZ, double x, double z) { - return new RiverNode( - new RiverNodeId(cellX, cellZ), - x, - z, - 64D, - 64D, - 64D, - 64D, - false, - true - ); - } - - private static long sampleRound(RiverTile tile) { - long signature = 0xCBF29CE484222325L; - for (int sampleIndex = 0; sampleIndex < SAMPLES_PER_ROUND; sampleIndex++) { - double x = Math.floorMod(sampleIndex * 1_229, 2_048) + 0.375D; - double z = Math.floorMod(sampleIndex * 811, 2_048) + 0.625D; - double additionalRadius = 16D + sampleIndex % 17; - RiverSample sample = tile.sampleExpanded(x, z, additionalRadius); - signature = mix(signature, sample.present() ? 1L : 0L); - if (!sample.present()) { - continue; - } - signature = mix(signature, sample.reachId().stableId()); - signature = mix(signature, Double.doubleToLongBits(sample.distance())); - signature = mix(signature, Double.doubleToLongBits(sample.alongReach())); - signature = mix(signature, Double.doubleToLongBits(sample.carveWeight())); - signature = mix(signature, Double.doubleToLongBits(sample.width())); - signature = mix(signature, Double.doubleToLongBits(sample.bankWidth())); - signature = mix(signature, Double.doubleToLongBits(sample.depth())); - signature = mix(signature, sample.section().ordinal()); - } - return signature; - } - - private static long mix(long hash, long value) { - return (hash ^ value) * 0x100000001B3L; - } - - private static void requireSignature(long expected, long actual) { - if (actual != expected) { - throw new IllegalStateException(String.format( - Locale.ROOT, - "River tile output changed: expected %016x but got %016x", - expected, - actual - )); - } - } -}