From 479b94d6a2bf3b9e807b37d6cf1f43a2c3fc1361 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 2 Sep 2026 00:29:32 -0400 Subject: [PATCH] Standing surface pools - hydrology.surfacePools defines bowls filled with their own fluid; riverPolicy.surfacePools opts areas in - Pools are independent SURFACE_POOL courses with one STANDING_POOL segment, shaped by the erosion field, kept clear of rivers and off slopes - Locators accept the pool selector and pool ids; validator, renderer, probes and docs follow --- .../core/pack/PackHydrologyValidator.java | 137 ++++++++++++- .../art/arcane/iris/engine/IrisComplex.java | 15 ++ .../engine/framework/render/IrisRenderer.java | 4 +- .../hydrology/HydrologyCandidateKind.java | 3 +- .../HydrologyCandidateRejection.java | 3 +- .../hydrology/HydrologyFeatureQuery.java | 8 +- .../hydrology/HydrologyFeatureType.java | 7 +- .../engine/hydrology/HydrologyPlanner.java | 183 ++++++++++++++++++ .../hydrology/HydrologyPlannerSettings.java | 47 ++++- .../hydrology/HydrologyTerrainSample.java | 46 ++++- .../iris/engine/hydrology/RiverCourse.java | 7 +- .../engine/hydrology/RiverCourseType.java | 3 +- .../policy/EffectiveRiverPolicy.java | 2 + .../hydrology/policy/RiverPolicyResolver.java | 5 + .../runtime/IrisHydrologyRuntime.java | 34 +++- .../surface/SurfaceFootprintCompiler.java | 71 ++++++- .../iris/engine/object/IrisHydrology.java | 4 + .../iris/engine/object/IrisRiverPolicy.java | 4 + .../engine/object/IrisSurfacePoolConfig.java | 54 ++++++ .../core/pack/PackHydrologyValidatorTest.java | 64 ++++++ .../render/IrisRendererRiverTest.java | 1 + .../HydrologyFootprintCompilerTest.java | 2 +- .../engine/hydrology/HydrologyModelTest.java | 4 +- ...CrossTileCaveAdmissionIntegrationTest.java | 10 +- .../HydrologyPlannerGlobalInvariantTest.java | 10 +- .../HydrologyPlannerSettingsTest.java | 2 +- .../HydrologyPlannerSurfacePoolTest.java | 119 ++++++++++++ .../hydrology/HydrologyPlannerTest.java | 34 ++-- .../HydrologyStyledGeometryTest.java | 6 +- .../hydrology/HydrologyTerrainSampleTest.java | 2 +- .../hydrology/HydrologyTileCacheTest.java | 6 +- .../surface/ChannelProfileBuilderTest.java | 2 +- .../MantleHydrologyComponentTest.java | 4 +- .../arcane/iris/probe/HydrologyPackProbe.java | 4 +- .../iris/probe/HydrologyPlannerProbe.java | 12 +- .../arcane/iris/probe/RiverTransectProbe.java | 2 +- 36 files changed, 841 insertions(+), 80 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisSurfacePoolConfig.java create mode 100644 core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSurfacePoolTest.java diff --git a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java index 9e8715371..3436a7d37 100644 --- a/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java +++ b/core/src/main/java/art/arcane/iris/core/pack/PackHydrologyValidator.java @@ -92,7 +92,7 @@ final class PackHydrologyValidator { biomes, errors ); - HydrologyValidation hydrology = validateHydrology(dimensionPath, dimension, errors, warnings); + HydrologyValidation hydrology = validateHydrology(dimensionPath, dimension, biomes, errors, warnings); if (!hydrology.active()) { continue; } @@ -103,6 +103,7 @@ final class PackHydrologyValidator { dimension, dimensionPolicy, hydrology.profileIds(), + hydrology.surfacePoolIds(), regions, biomes, imageMaps, @@ -116,6 +117,7 @@ final class PackHydrologyValidator { private static HydrologyValidation validateHydrology( String dimensionPath, JSONObject dimension, + Map biomes, List errors, List warnings ) { @@ -156,7 +158,8 @@ final class PackHydrologyValidator { profileIds, errors ); - return new HydrologyValidation(riversEnabled, deepFluidsActive, profileIds); + Set surfacePoolIds = validateSurfacePools(dimensionPath + ".hydrology", hydrology, profileIds, biomes, errors); + return new HydrologyValidation(riversEnabled, deepFluidsActive || !surfacePoolIds.isEmpty(), profileIds, surfacePoolIds); } private static Set validateRivers( @@ -596,6 +599,108 @@ final class PackHydrologyValidator { return Set.copyOf(profileIds); } + private static Set validateSurfacePools( + String path, + JSONObject hydrology, + Set riverProfileIds, + Map biomes, + List errors + ) { + if (!hydrology.has("surfacePools")) { + return Set.of(); + } + Object rawPools = hydrology.opt("surfacePools"); + if (!(rawPools instanceof JSONArray pools)) { + errors.add(path + ".surfacePools must be an array."); + return Set.of(); + } + if (pools.length() > MAX_DEEP_FLUIDS) { + errors.add(path + ".surfacePools must contain at most " + MAX_DEEP_FLUIDS + " entries."); + } + LinkedHashSet ids = new LinkedHashSet<>(); + for (int index = 0; index < pools.length(); index++) { + String poolPath = path + ".surfacePools[" + index + "]"; + JSONObject pool = pools.optJSONObject(index); + if (pool == null) { + errors.add(poolPath + " must be an object."); + continue; + } + String id = requiredId(poolPath, pool, errors); + if (id != null) { + if (HydrologyFeatureQuery.isReservedKeyword(id)) { + errors.add(poolPath + ".id '" + id + "' is a reserved hydrology feature selector."); + } else if (riverProfileIds.contains(id)) { + errors.add(poolPath + ".id '" + id + "' collides with a river profile."); + } else if (!ids.add(id)) { + errors.add(poolPath + ".id must be unique inside hydrology.surfacePools."); + } + } + validateFluidPalette(poolPath, pool, true, errors); + PackJsonFieldChecks.validateOptionalDoubleRange(poolPath, pool, "density", 0D, 64D, errors); + PackJsonFieldChecks.validateOptionalIntegerRange(poolPath, pool, "spacing", 32, 8192, errors); + PackJsonFieldChecks.validateOptionalIntegerRange(poolPath, pool, "minimumRadius", 2, 16, errors); + PackJsonFieldChecks.validateOptionalIntegerRange(poolPath, pool, "maximumRadius", 2, 16, errors); + PackJsonFieldChecks.validateOptionalIntegerRange(poolPath, pool, "depth", 1, 8, errors); + if (integerValue(pool, "minimumRadius", 4) > integerValue(pool, "maximumRadius", 7)) { + errors.add(poolPath + ".minimumRadius must not exceed maximumRadius."); + } + Object rawBiome = pool.opt("biome"); + if (rawBiome instanceof String biome && !biome.isBlank() && !biomes.containsKey(biome)) { + errors.add(poolPath + ".biome references unknown biome '" + biome + "'."); + } + } + return Set.copyOf(ids); + } + + private static void validatePolicyPoolsInDimension( + String path, + JSONObject policy, + String dimensionKey, + Set pools, + List errors + ) { + if (policy == null) { + return; + } + JSONArray references = policy.optJSONArray("surfacePools"); + if (references == null) { + return; + } + for (int index = 0; index < references.length(); index++) { + String pool = references.optString(index, null); + if (pool != null && ID_PATTERN.matcher(pool).matches() && !pools.contains(pool)) { + addDistinct(errors, path + ".surfacePools[" + index + "] references unknown surface pool '" + + pool + "' in Dimension '" + dimensionKey + "'."); + } + } + } + + private static void validatePolicyPools(String path, JSONObject policy, List errors) { + if (!policy.has("surfacePools") || policy.opt("surfacePools") == JSONObject.NULL) { + return; + } + Object rawPools = policy.opt("surfacePools"); + if (!(rawPools instanceof JSONArray pools)) { + errors.add(path + ".surfacePools must be an array or null."); + return; + } + if (pools.length() > MAX_POLICY_REFERENCES) { + errors.add(path + ".surfacePools must contain at most " + MAX_POLICY_REFERENCES + " entries."); + } + Set seen = new HashSet<>(); + for (int index = 0; index < pools.length(); index++) { + Object raw = pools.opt(index); + String referencePath = path + ".surfacePools[" + index + "]"; + if (!(raw instanceof String id) || !ID_PATTERN.matcher(id).matches()) { + errors.add(referencePath + " must name a surface pool id."); + continue; + } + if (!seen.add(id)) { + errors.add(referencePath + " duplicates surface pool '" + id + "'."); + } + } + } + private static boolean validateDeepFluids( String path, JSONObject hydrology, @@ -787,6 +892,7 @@ final class PackHydrologyValidator { validateNullableEnum(path, policy, "routing", ROUTING_MODES, errors); validateNullableBoolean(path, policy, "outletAdmission", errors); validatePolicyProfiles(path, policy, errors); + validatePolicyPools(path, policy, errors); for (String biomeField : POLICY_BIOME_FIELDS) { validatePolicyBiomes(path, policy, biomeField, biomes, errors); } @@ -803,6 +909,7 @@ final class PackHydrologyValidator { JSONObject dimension, JSONObject dimensionPolicy, Set profiles, + Set pools, Map regions, Map biomes, Map imageMaps, @@ -816,6 +923,13 @@ final class PackHydrologyValidator { profiles, errors ); + validatePolicyPoolsInDimension( + dimensionPath + ".riverPolicy", + dimensionPolicy, + dimensionKey, + pools, + errors + ); Deque pending = new ArrayDeque<>(); addPolicyBiomeReferences(pending, dimensionPolicy); @@ -835,6 +949,13 @@ final class PackHydrologyValidator { profiles, errors ); + validatePolicyPoolsInDimension( + "Region '" + regionKey + "'.riverPolicy", + policy, + dimensionKey, + pools, + errors + ); addPolicyBiomeReferences(pending, policy); for (String biomeField : REGION_BIOME_FIELDS) { addStringArray(pending, region.optJSONArray(biomeField)); @@ -867,6 +988,13 @@ final class PackHydrologyValidator { profiles, errors ); + validatePolicyPoolsInDimension( + "Biome '" + biomeKey + "'.riverPolicy", + policy, + dimensionKey, + pools, + errors + ); addPolicyBiomeReferences(pending, policy); addStringArray(pending, biome.optJSONArray("children")); addBiomeReference(pending, biome.optString("carvingBiome", null)); @@ -1374,10 +1502,11 @@ final class PackHydrologyValidator { private record HydrologyValidation( boolean riversEnabled, boolean deepFluidsActive, - Set profileIds + Set profileIds, + Set surfacePoolIds ) { private static HydrologyValidation inactive() { - return new HydrologyValidation(false, false, Set.of()); + return new HydrologyValidation(false, false, Set.of(), Set.of()); } private boolean active() { 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 aa82cb086..9885f746d 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -39,6 +39,7 @@ 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.IrisDeepFluidConfig; +import art.arcane.iris.engine.object.IrisSurfacePoolConfig; import art.arcane.iris.engine.object.IrisHydrology; import art.arcane.iris.engine.object.IrisRiverHydrology; import art.arcane.iris.engine.object.IrisRiverProfile; @@ -553,6 +554,20 @@ public class IrisComplex implements DataProvider { throw new IllegalArgumentException("Duplicate hydrology fluid profile: " + profileKey); } } + for (IrisSurfacePoolConfig pool : hydrology.getSurfacePools()) { + if (pool == null) { + throw new IllegalArgumentException("hydrology.surfacePools cannot contain null entries"); + } + String profileKey = requireHydrologyKey(pool.getId(), "hydrology.surfacePools[].id"); + ProceduralStream stream = configuredFluidStream( + pool.getFluidPalette(), + rng.nextParallelRNG(9900 + streamIndex++), + "hydrology surface pool " + profileKey + ); + if (streams.putIfAbsent(profileKey, stream) != null) { + throw new IllegalArgumentException("Duplicate hydrology fluid profile: " + profileKey); + } + } return Collections.unmodifiableMap(streams); } 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 c95073fde..1ddd68c40 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 @@ -64,6 +64,7 @@ public final class IrisRenderer { private static final int RIVER_MOUTH = new Color(54, 164, 205).getRGB(); private static final int RIVER_WATERFALL = new Color(190, 236, 255).getRGB(); private static final int CONFIRMED_DEEP_LAVA = new Color(194, 50, 22).getRGB(); + private static final int STANDING_POOL = new Color(232, 120, 40).getRGB(); private static final int PROJECTED_SOURCE = new Color(225, 75, 178).getRGB(); private static final int PROJECTED_OUTLET = new Color(235, 145, 54).getRGB(); private static final int PROJECTED_DEEP_FLUID = new Color(175, 70, 118).getRGB(); @@ -199,6 +200,7 @@ public final class IrisRenderer { case INLAND_GROTTO -> INLAND_GROTTO; case MOUTH -> RIVER_MOUTH; case DEEP_POOL, DEEP_CHANNEL -> CONFIRMED_DEEP_LAVA; + case STANDING_POOL -> STANDING_POOL; }; } @@ -215,7 +217,7 @@ public final class IrisRenderer { return switch (kind) { case SOURCE -> PROJECTED_SOURCE; case OUTLET -> PROJECTED_OUTLET; - case DEEP_FLUID -> PROJECTED_DEEP_FLUID; + case DEEP_FLUID, POOL -> PROJECTED_DEEP_FLUID; }; } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateKind.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateKind.java index 64e0f587f..e6dd9330f 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateKind.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateKind.java @@ -3,5 +3,6 @@ package art.arcane.iris.engine.hydrology; public enum HydrologyCandidateKind { SOURCE, OUTLET, - DEEP_FLUID + DEEP_FLUID, + POOL } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateRejection.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateRejection.java index db9c8b94e..2cf36b17a 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateRejection.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyCandidateRejection.java @@ -19,5 +19,6 @@ public enum HydrologyCandidateRejection { SURFACE_TRANSITION_UNSUPPORTED, SURFACE_SEGMENT_UNSUPPORTED, SURFACE_SINKHOLE_CLEARANCE, - SURFACE_SHAPE_UNSUPPORTED + SURFACE_SHAPE_UNSUPPORTED, + POLICY_EXCLUDED; } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureQuery.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureQuery.java index 86843fc5c..ca54784fc 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureQuery.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureQuery.java @@ -32,6 +32,11 @@ public record HydrologyFeatureQuery(Set types, String prof HydrologyFeatureType.DEEP_POOL, HydrologyFeatureType.DEEP_CHANNEL ); + private static final Set PROFILED = Set.of( + HydrologyFeatureType.DEEP_POOL, + HydrologyFeatureType.DEEP_CHANNEL, + HydrologyFeatureType.STANDING_POOL + ); private static final Map> BUILT_INS = builtIns(); public HydrologyFeatureQuery { @@ -53,7 +58,7 @@ public record HydrologyFeatureQuery(Set types, String prof String key = normalizeKeyword(normalized); Set builtInTypes = BUILT_INS.get(key); return builtInTypes == null - ? new HydrologyFeatureQuery(DEEP, normalized) + ? new HydrologyFeatureQuery(PROFILED, normalized) : new HydrologyFeatureQuery(builtInTypes, null); } @@ -91,6 +96,7 @@ public record HydrologyFeatureQuery(Set types, String prof builtIns.put("mouth", Set.of(HydrologyFeatureType.MOUTH)); builtIns.put("ridge_tunnel", Set.of(HydrologyFeatureType.RIDGE_BORE)); builtIns.put("deep", DEEP); + builtIns.put("pool", Set.of(HydrologyFeatureType.STANDING_POOL)); return Collections.unmodifiableMap(builtIns); } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureType.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureType.java index 44828cf5f..c285d2f62 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureType.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureType.java @@ -15,11 +15,13 @@ public enum HydrologyFeatureType { INLAND_GROTTO, MOUTH, DEEP_POOL, - DEEP_CHANNEL; + DEEP_CHANNEL, + // A standing surface pool (lava or any fluid) with no course: a bowl cut into the ground with a lip. + STANDING_POOL; public boolean isSurface() { return switch (this) { - case SURFACE_POOL, RIFFLE, CASCADE, WATERFALL, MOUTH -> true; + case SURFACE_POOL, RIFFLE, CASCADE, WATERFALL, MOUTH, STANDING_POOL -> true; default -> false; }; } @@ -59,6 +61,7 @@ public enum HydrologyFeatureType { case DEEP_CHANNEL -> 10; case DEEP_POOL -> 11; case SURFACE_POOL -> 12; + case STANDING_POOL -> 13; }; } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java index 0475d7c48..104445833 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java @@ -5,6 +5,10 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelView; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate; import art.arcane.iris.engine.hydrology.cave.HydrologyCaveContainmentPlanner; import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan; +import art.arcane.iris.engine.hydrology.surface.ChannelProfile; +import art.arcane.iris.engine.hydrology.surface.ValleyProfile; +import art.arcane.iris.engine.hydrology.surface.ValleyProfileSolver; +import art.arcane.iris.engine.hydrology.surface.SurfaceCenterline; import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder; import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult; import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal; @@ -45,6 +49,7 @@ public final class HydrologyPlanner { private static final long COURSE_SALT = 0x434f55525345L; private static final long SEGMENT_SALT = 0x5345474d454e54L; private static final long DEEP_FLUID_SALT = 0x44454550464cL; + private static final long SURFACE_POOL_SALT = 0x504f4f4cL; private static final long DEEP_FLUID_X_OFFSET_SALT = 0x44454550584fL; private static final long DEEP_FLUID_Z_OFFSET_SALT = 0x444545505a4fL; private static final long DEEP_CHANNEL_HEADING_SALT = 0x44454550484447L; @@ -1101,6 +1106,7 @@ public final class HydrologyPlanner { if (includeDeepFluids) { compileDeepFluidCourses(key, courses, diagnostics); } + compileSurfacePools(key, courses, diagnostics); HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(courses); HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface( validation.plannedSurface() @@ -6112,6 +6118,183 @@ public final class HydrologyPlanner { )); } + /** + * Standing surface pools: a jittered lattice of candidate sites per pool profile, admitted where the + * policy lists the pool, the ground is open land clear of every accepted course, and the bowl fits + * the incision cap. Each accepted pool is an independent course with one STANDING_POOL segment. + */ + private void compileSurfacePools( + HydrologyTileKey key, + List courses, + List diagnostics + ) { + int tileSize = settings.routing().tileSize(); + for (HydrologyPlannerSettings.SurfacePool pool : settings.surfacePools()) { + if (!pool.enabled() || pool.maximumPerTile() == 0) { + continue; + } + long profileSeed = HydrologyHash.mix(worldSeed, SURFACE_POOL_SALT, HydrologyHash.text(pool.id())); + int target = Math.min(pool.maximumPerTile(), expectedCount(pool.density(), HydrologyHash.mix( + profileSeed, key.tileX(), key.tileZ()))); + int accepted = 0; + for (DeepSite site : poolSites(key, pool, profileSeed, tileSize)) { + if (accepted >= target) { + addPoolDiagnostic(site, HydrologyCandidateRejection.SOURCE_QUOTA, diagnostics); + continue; + } + HydrologyCandidateRejection rejection = poolAdmission(pool, site, courses); + if (rejection != null) { + addPoolDiagnostic(site, rejection, diagnostics); + continue; + } + RiverCourse course = buildSurfacePoolCourse(pool, site); + if (course == null) { + addPoolDiagnostic(site, HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, diagnostics); + continue; + } + courses.add(course); + accepted++; + } + } + } + + private ArrayList poolSites( + HydrologyTileKey key, + HydrologyPlannerSettings.SurfacePool pool, + long profileSeed, + int tileSize + ) { + int minimumX = key.minimumBlockX(tileSize); + int minimumZ = key.minimumBlockZ(tileSize); + int spacing = pool.spacing(); + int xOffset = HydrologyHash.between(HydrologyHash.mix(profileSeed, DEEP_FLUID_X_OFFSET_SALT), 0, spacing - 1); + int zOffset = HydrologyHash.between(HydrologyHash.mix(profileSeed, DEEP_FLUID_Z_OFFSET_SALT), 0, spacing - 1); + long firstCellX = ceilDiv((long) minimumX - xOffset, spacing); + long firstCellZ = ceilDiv((long) minimumZ - zOffset, spacing); + long lastCellX = Math.floorDiv((long) minimumX + tileSize - 1L - xOffset, spacing); + long lastCellZ = Math.floorDiv((long) minimumZ + tileSize - 1L - zOffset, spacing); + ArrayList sites = new ArrayList<>(); + for (long cellZ = firstCellZ; cellZ <= lastCellZ; cellZ++) { + for (long cellX = firstCellX; cellX <= lastCellX; cellX++) { + long stable = HydrologyHash.mix(profileSeed, cellX, cellZ); + // Jitter inside the cell keeps pools off a visible grid. + int jitter = Math.max(1, spacing / 3); + int x = Math.toIntExact(xOffset + cellX * spacing + HydrologyHash.between(HydrologyHash.mix(stable, 11), -jitter, jitter)); + int z = Math.toIntExact(zOffset + cellZ * spacing + HydrologyHash.between(HydrologyHash.mix(stable, 12), -jitter, jitter)); + HydrologyTerrainSample terrain = sampleLandBasis(x, z); + sites.add(new DeepSite(x, z, terrain == null ? 0 : terrain.naturalHeight(), stable)); + } + } + sites.sort(Comparator.comparingLong(DeepSite::stableId)); + return sites; + } + + private HydrologyCandidateRejection poolAdmission( + HydrologyPlannerSettings.SurfacePool pool, + DeepSite site, + List courses + ) { + HydrologyTerrainSample terrain = sampleLandBasis(site.x(), site.z()); + if (terrain == null || terrain.ocean() || terrain.naturalHeight() <= settings.seaLevel() + 1) { + return HydrologyCandidateRejection.SURFACE_EXPOSURE; + } + if (!terrain.transitAllowed() || !terrain.surfacePoolKeys().contains(pool.id())) { + return HydrologyCandidateRejection.POLICY_EXCLUDED; + } + long clearance = pool.maximumRadius() + (long) StrictMath.ceil(settings.surface().shoreWidth()) + + settings.surface().banks().maximumBlendWidth() + settings.surface().maximumWidth(); + long clearanceSquared = clearance * clearance; + for (RiverCourse course : courses) { + for (HydraulicSegment segment : course.segments()) { + for (HydrologyPoint point : segment.centerline()) { + long deltaX = (long) point.x() - site.x(); + long deltaZ = (long) point.z() - site.z(); + if (deltaX * deltaX + deltaZ * deltaZ < clearanceSquared) { + return HydrologyCandidateRejection.SOURCE_SPACING; + } + } + } + } + return null; + } + + private RiverCourse buildSurfacePoolCourse(HydrologyPlannerSettings.SurfacePool pool, DeepSite site) { + int radius = HydrologyHash.between(HydrologyHash.mix(site.stableId(), 3), pool.minimumRadius(), pool.maximumRadius()); + HydrologyTerrainSample terrain = sampleLandBasis(site.x(), site.z()); + List points = List.of( + new HydrologyPoint(site.x() - 1, site.head(), site.z()), + new HydrologyPoint(site.x(), site.head(), site.z()), + new HydrologyPoint(site.x() + 1, site.head(), site.z()) + ); + SurfaceCenterline centerline = SurfaceCenterline.densify(points); + int count = centerline.size(); + double[] halfWidth = new double[count]; + double[] depth = new double[count]; + double[] bank = new double[count]; + Arrays.fill(halfWidth, radius); + Arrays.fill(depth, pool.depth()); + Arrays.fill(bank, terrain == null ? 1D : terrain.bankMultiplier()); + ChannelProfile profile = new ChannelProfile(halfWidth, depth, bank); + ValleyProfile valley = new ValleyProfileSolver(settings.surface(), this::sampleBasis, settings.seaLevel(), 0) + .solve(centerline, profile, SurfaceTerminal.SINKHOLE, Integer.MIN_VALUE); + if (!valley.accepted()) { + return null; + } + int center = Math.min(1, valley.exposedStations() - 1); + int head = valley.head()[center]; + // A pool belongs in a hollow or on level ground: the whole rim must sit within a few blocks of the + // water, or the bowl becomes a scar dug into a slope. + int rimAllowance = pool.depth() + settings.surface().banks().inset() + 2; + for (int station = 0; station < valley.exposedStations(); station++) { + if (valley.crossMax()[station] - valley.head()[station] > rimAllowance) { + return null; + } + } + long courseId = HydrologyHash.mix(worldSeed, SURFACE_POOL_SALT, HydrologyHash.text(pool.id()), site.stableId()); + long segmentId = HydrologyHash.mix(courseId, SEGMENT_SALT, HydrologyFeatureType.STANDING_POOL.ordinal()); + ArrayList centerlinePoints = new ArrayList<>(points.size()); + for (HydrologyPoint point : points) { + centerlinePoints.add(new HydrologyPoint(point.x(), head, point.z())); + } + HydraulicSegment segment = new HydraulicSegment( + segmentId, + courseId, + HydrologyFeatureType.STANDING_POOL, + head, + head, + radius * 2, + pool.depth(), + false, + false, + centerlinePoints + ); + return new RiverCourse( + courseId, + RiverCourseType.SURFACE_POOL, + OptionalLong.empty(), + OptionalLong.empty(), + pool.id(), + 1, + List.of(), + List.of(segment) + ); + } + + private void addPoolDiagnostic( + DeepSite site, + HydrologyCandidateRejection rejection, + List diagnostics + ) { + diagnostics.add(new HydrologyDiagnosticCandidate( + HydrologyHash.mix(site.stableId(), DIAGNOSTIC_SALT, rejection.ordinal()), + HydrologyCandidateKind.POOL, + HydrologyFeatureType.STANDING_POOL, + new HydrologyPoint(site.x(), site.head(), site.z()), + rejection, + 0 + )); + } + private void compileDeepFluidCourses( HydrologyTileKey key, List courses, diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java index a0bc9c701..f35abfabb 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java @@ -10,7 +10,8 @@ public record HydrologyPlannerSettings( Underground underground, Outlets outlets, Geometry geometry, - List deepFluids + List deepFluids, + List surfacePools ) { private static final long PLAN_FORMAT_REVISION = 6L; private static final int MAXIMUM_CROSS_TILE_COLOR_PERIOD = 4; @@ -24,7 +25,8 @@ public record HydrologyPlannerSettings( throw new IllegalArgumentException("Hydrology planner settings cannot contain null sections."); } deepFluids = deepFluids == null ? List.of() : List.copyOf(deepFluids); - int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids); + surfacePools = surfacePools == null ? List.of() : List.copyOf(surfacePools); + int publicationRadius = publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools); if (crossTileColorPeriod(publicationRadius, routing.tileSize()) > MAXIMUM_CROSS_TILE_COLOR_PERIOD) { throw new IllegalArgumentException("Hydrology publication envelope exceeds the bounded cross-tile admission period."); } @@ -50,6 +52,7 @@ public record HydrologyPlannerSettings( 12 ), Geometry.defaults(), + List.of(), List.of() ); } @@ -68,11 +71,14 @@ public record HydrologyPlannerSettings( for (DeepFluid deepFluid : deepFluids) { hash = HydrologyHash.mix(hash, deepFluid.hashCode(), HydrologyHash.text(deepFluid.id())); } + for (SurfacePool pool : surfacePools) { + hash = HydrologyHash.mix(hash, pool.hashCode(), HydrologyHash.text(pool.id())); + } return hash; } public int publicationRadius() { - return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids); + return publicationRadius(routing, surface, underground, outlets, geometry, deepFluids, surfacePools); } int crossTileColorPeriod() { @@ -85,7 +91,8 @@ public record HydrologyPlannerSettings( Underground underground, Outlets outlets, Geometry geometry, - List deepFluids + List deepFluids, + List surfacePools ) { int alignedHalo = Math.floorDiv( Math.min( @@ -133,6 +140,13 @@ public record HydrologyPlannerSettings( : 0; radius = Math.max(radius, Math.max(poolReach, channelReach)); } + for (SurfacePool pool : surfacePools) { + if (!pool.enabled()) { + continue; + } + radius = Math.max(radius, pool.maximumRadius() + (int) StrictMath.ceil(surface.shoreWidth()) + + surface.banks().maximumBlendWidth()); + } return radius; } @@ -278,6 +292,31 @@ public record HydrologyPlannerSettings( } } + /** A standing surface pool profile: a bowl of the given radius and depth, filled with its own fluid. */ + public record SurfacePool( + String id, + boolean enabled, + double density, + int spacing, + int minimumRadius, + int maximumRadius, + int depth, + int maximumPerTile, + String biomeKey + ) { + public SurfacePool { + if (id == null || id.isBlank()) { + throw new IllegalArgumentException("Surface pool id must not be blank."); + } + id = id.trim(); + if (!Double.isFinite(density) || density < 0D || spacing < 1 + || minimumRadius < 1 || maximumRadius < minimumRadius || depth < 1 || maximumPerTile < 0) { + throw new IllegalArgumentException("Surface pool settings are invalid."); + } + biomeKey = biomeKey == null || biomeKey.isBlank() ? null : biomeKey.trim(); + } + } + // The routing gates read one hydraulic threshold: the drop that makes a reach a waterfall. public record Hydraulics(int waterfallMinimumDrop) { public Hydraulics { diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSample.java b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSample.java index 4f282689b..f01cd668b 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSample.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSample.java @@ -29,7 +29,8 @@ public record HydrologyTerrainSample( String shoreBiomeKey, String bankBiomeKey, String floodedCaveBiomeKey, - List preferredProfileKeys + List preferredProfileKeys, + List surfacePoolKeys ) { public HydrologyTerrainSample { requireFiniteNonNegative(slope, "slope"); @@ -51,6 +52,7 @@ public record HydrologyTerrainSample( bankBiomeKey = normalizeKey(bankBiomeKey, parentBiomeKey); floodedCaveBiomeKey = normalizeKey(floodedCaveBiomeKey, surfaceBiomeKey); preferredProfileKeys = normalizeProfiles(preferredProfileKeys); + surfacePoolKeys = surfacePoolKeys == null ? List.of() : List.copyOf(surfacePoolKeys); } public static HydrologyTerrainSample openLand(int naturalHeight, double slope, String parentBiomeKey) { @@ -81,7 +83,8 @@ public record HydrologyTerrainSample( parentBiomeKey, parentBiomeKey, parentBiomeKey, - List.of("default") + List.of("default"), + List.of() ); } @@ -113,7 +116,41 @@ public record HydrologyTerrainSample( parentBiomeKey, parentBiomeKey, parentBiomeKey, - List.of("default") + List.of("default"), + List.of() + ); + } + + public HydrologyTerrainSample withSurfacePoolKeys(List replacementPoolKeys) { + return new HydrologyTerrainSample( + naturalHeight, + slope, + ocean, + caveAvailable, + caveFloorY, + caveFluidY, + transitAllowed, + outletAllowed, + surfaceSourceAllowed, + surfaceSourceRequired, + undergroundSourceAllowed, + undergroundSourceRequired, + routingCost, + surfaceSourceWeight, + undergroundSourceWeight, + widthMultiplier, + depthMultiplier, + incisionMultiplier, + routingMultiplier, + bankMultiplier, + parentBiomeKey, + surfaceBiomeKey, + mouthBiomeKey, + shoreBiomeKey, + bankBiomeKey, + floodedCaveBiomeKey, + preferredProfileKeys, + replacementPoolKeys ); } @@ -145,7 +182,8 @@ public record HydrologyTerrainSample( shoreBiomeKey, bankBiomeKey, floodedCaveBiomeKey, - preferredProfileKeys + preferredProfileKeys, + surfacePoolKeys ); } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourse.java b/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourse.java index ec95a79a6..2c3191273 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourse.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourse.java @@ -38,10 +38,11 @@ public record RiverCourse( } } validateSinkholeContinuation(type, segments); - if (type == RiverCourseType.DEEP_FLUID && (sourceNodeId.isPresent() || outletId.isPresent())) { - throw new IllegalArgumentException("Independent deep-fluid courses cannot reference drainage nodes or river outlets."); + boolean independent = type == RiverCourseType.DEEP_FLUID || type == RiverCourseType.SURFACE_POOL; + if (independent && (sourceNodeId.isPresent() || outletId.isPresent())) { + throw new IllegalArgumentException("Independent deep-fluid and pool courses cannot reference drainage nodes or river outlets."); } - if (type != RiverCourseType.DEEP_FLUID && (sourceNodeId.isEmpty() || outletId.isEmpty())) { + if (!independent && (sourceNodeId.isEmpty() || outletId.isEmpty())) { throw new IllegalArgumentException("Surface and underground courses require a source and outlet."); } } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourseType.java b/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourseType.java index f4a11595e..269726e7b 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourseType.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourseType.java @@ -3,5 +3,6 @@ package art.arcane.iris.engine.hydrology; public enum RiverCourseType { SURFACE, UNDERGROUND, - DEEP_FLUID + DEEP_FLUID, + SURFACE_POOL } diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/policy/EffectiveRiverPolicy.java b/core/src/main/java/art/arcane/iris/engine/hydrology/policy/EffectiveRiverPolicy.java index 48a4454e7..647b7b51f 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/policy/EffectiveRiverPolicy.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/policy/EffectiveRiverPolicy.java @@ -16,6 +16,7 @@ public record EffectiveRiverPolicy( List shoreBiomes, List bankBiomes, List floodedCaveBiomes, + List surfacePools, double widthMultiplier, double depthMultiplier, double incisionMultiplier, @@ -31,6 +32,7 @@ public record EffectiveRiverPolicy( shoreBiomes = List.copyOf(shoreBiomes); bankBiomes = List.copyOf(bankBiomes); floodedCaveBiomes = List.copyOf(floodedCaveBiomes); + surfacePools = List.copyOf(surfacePools); } public boolean allowsSources() { diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/policy/RiverPolicyResolver.java b/core/src/main/java/art/arcane/iris/engine/hydrology/policy/RiverPolicyResolver.java index a4eb89541..c521f949e 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/policy/RiverPolicyResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/policy/RiverPolicyResolver.java @@ -43,6 +43,7 @@ public final class RiverPolicyResolver { private List shoreBiomes = List.of(); private List bankBiomes = List.of(); private List floodedCaveBiomes = List.of(); + private List surfacePools = List.of(); private double widthMultiplier = 1D; private double depthMultiplier = 1D; private double incisionMultiplier = 1D; @@ -80,6 +81,9 @@ public final class RiverPolicyResolver { if (policy.getFloodedCaveBiomes() != null) { floodedCaveBiomes = List.copyOf(policy.getFloodedCaveBiomes()); } + if (policy.getSurfacePools() != null) { + surfacePools = List.copyOf(policy.getSurfacePools()); + } if (policy.getWidthMultiplier() != null) { widthMultiplier = policy.getWidthMultiplier(); } @@ -108,6 +112,7 @@ public final class RiverPolicyResolver { shoreBiomes, bankBiomes, floodedCaveBiomes, + surfacePools, widthMultiplier, depthMultiplier, incisionMultiplier, diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java b/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java index 733f8f217..ea8904301 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/runtime/IrisHydrologyRuntime.java @@ -22,6 +22,7 @@ import art.arcane.iris.engine.hydrology.policy.RiverPolicyResolver; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisCoastalRiverGrottoConfig; import art.arcane.iris.engine.object.IrisDeepFluidConfig; +import art.arcane.iris.engine.object.IrisSurfacePoolConfig; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisHydrology; import art.arcane.iris.engine.object.IrisInlandRiverGrottoConfig; @@ -364,11 +365,14 @@ public final class IrisHydrologyRuntime implements AutoCloseable { } public List featureQueryKeys() { - ArrayList deepFluidIds = new ArrayList<>(settings.deepFluids().size()); + ArrayList profiledIds = new ArrayList<>(settings.deepFluids().size() + settings.surfacePools().size()); for (HydrologyPlannerSettings.DeepFluid deepFluid : settings.deepFluids()) { - deepFluidIds.add(deepFluid.id()); + profiledIds.add(deepFluid.id()); } - return HydrologyFeatureQuery.suggestions(deepFluidIds); + for (HydrologyPlannerSettings.SurfacePool pool : settings.surfacePools()) { + profiledIds.add(pool.id()); + } + return HydrologyFeatureQuery.suggestions(profiledIds); } @Override @@ -487,7 +491,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable { selectKey(policy.shoreBiomes(), parentBiomeKey, biomePatchNoise, 3), selectKey(policy.bankBiomes(), parentBiomeKey, biomePatchNoise, 4), selectKey(policy.floodedCaveBiomes(), parentBiomeKey, biomePatchNoise, 5), - profiles + profiles, + policy.surfacePools() ); return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(resolvedNaturalHeight, terrain); } @@ -764,10 +769,29 @@ public final class IrisHydrologyRuntime implements AutoCloseable { plannerUnderground, plannerOutlets, plannerGeometry, - deepFluids(hydrology.getDeepFluids(), minimumWorldY, routing, plannerRouting.refinementSpacing()) + deepFluids(hydrology.getDeepFluids(), minimumWorldY, routing, plannerRouting.refinementSpacing()), + surfacePools(hydrology.getSurfacePools()) ); } + private static List surfacePools(List configurations) { + ArrayList pools = new ArrayList<>(); + for (IrisSurfacePoolConfig configuration : configurations) { + pools.add(new HydrologyPlannerSettings.SurfacePool( + configuration.getId(), + configuration.getDensity() > 0D, + configuration.getDensity(), + configuration.getSpacing(), + configuration.getMinimumRadius(), + configuration.getMaximumRadius(), + configuration.getDepth(), + Math.min(64, maximumSources(configuration.getDensity(), 0)), + configuration.getBiome() + )); + } + return List.copyOf(pools); + } + private static HydrologyPlannerSettings.Geometry geometry( IrisRiverGeometryConfig geometry, HydrologyPlannerSettings.ChannelShape surfaceShape diff --git a/core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFootprintCompiler.java b/core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFootprintCompiler.java index 78c160134..a92512ca1 100644 --- a/core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFootprintCompiler.java +++ b/core/src/main/java/art/arcane/iris/engine/hydrology/surface/SurfaceFootprintCompiler.java @@ -42,7 +42,8 @@ public final class SurfaceFootprintCompiler { } public SurfaceFootprint compile(RiverCourse course) { - if (course.type() != RiverCourseType.SURFACE) { + boolean pool = course.type() == RiverCourseType.SURFACE_POOL; + if (course.type() != RiverCourseType.SURFACE && !pool) { return SurfaceFootprint.empty(); } ArrayList exposed = new ArrayList<>(); @@ -59,10 +60,13 @@ public final class SurfaceFootprintCompiler { if (stations.count() < 1) { return SurfaceFootprint.empty(); } - SurfaceTerminal terminal = terminal(course, exposed); + SurfaceTerminal terminal = pool ? SurfaceTerminal.SINKHOLE : terminal(course, exposed); SurfaceCenterline centerline = SurfaceCenterline.densify(stations.points()); - ChannelProfile channel = new ChannelProfileBuilder(settings.surface(), sampler, geometry) - .build(centerline, course.profileKey(), terminal == SurfaceTerminal.OCEAN_MOUTH); + ChannelProfile channel = pool + ? poolProfile(exposed.getFirst(), centerline) + : new ChannelProfileBuilder(settings.surface(), sampler, geometry) + .build(centerline, course.profileKey(), terminal == SurfaceTerminal.OCEAN_MOUTH); + String poolBiome = pool ? poolBiome(course.profileKey()) : null; ValleyProfile valley = ValleyProfile.fromHeads(stations.head(), stations.exposedStations()); ErosionField field = new ErosionFieldCompiler(settings.surface(), sampler, settings.seaLevel()).compile( HydrologyHash.mix(course.id(), COURSE_SEED_SALT), @@ -90,7 +94,7 @@ public final class SurfaceFootprintCompiler { column.x(), column.z(), column.terrain(), - layer(feature, column, course.profileKey()), + pool ? poolLayer(feature, column, course.profileKey(), poolBiome) : layer(feature, column, course.profileKey()), column.role(), column.apron() )); @@ -98,6 +102,63 @@ public final class SurfaceFootprintCompiler { return new SurfaceFootprint(columns, field.uncontainedWetCells()); } + /** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */ + private static HydrologyColumnLayer poolLayer( + HydrologyFeatureRef feature, + SurfaceColumn column, + String profileKey, + String poolBiome + ) { + HydrologyTerrainSample terrain = column.terrain(); + String biome = poolBiome == null ? terrain.parentBiomeKey() : poolBiome; + boolean channel = column.role() == SurfaceRole.CHANNEL; + boolean shore = column.role() == SurfaceRole.SHORE; + return new HydrologyColumnLayer( + feature, + column.height(), + column.headY(), + column.headY(), + channel, + shore, + !channel, + channel, + false, + false, + true, + channel, + false, + profileKey, + biome, + biome, + biome, + biome, + terrain.floodedCaveBiomeKey() + ); + } + + private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) { + int count = centerline.size(); + double[] halfWidth = new double[count]; + double[] depth = new double[count]; + double[] bank = new double[count]; + for (int station = 0; station < count; station++) { + HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]); + halfWidth[station] = Math.max(0.5D, segment.width() / 2D); + depth[station] = Math.max(1D, segment.depth()); + bank[station] = terrain == null ? 1D : terrain.bankMultiplier(); + } + return new ChannelProfile(halfWidth, depth, bank); + } + + private String poolBiome(String poolId) { + for (HydrologyPlannerSettings.SurfacePool pool : settings.surfacePools()) { + if (pool.id().equals(poolId)) { + return pool.biomeKey(); + } + } + return null; + } + private static HydrologyColumnLayer layer(HydrologyFeatureRef feature, SurfaceColumn column, String profileKey) { HydrologyTerrainSample terrain = column.terrain(); if (column.apron()) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisHydrology.java b/core/src/main/java/art/arcane/iris/engine/object/IrisHydrology.java index 8e56f5a4e..bc6e04768 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisHydrology.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisHydrology.java @@ -18,4 +18,8 @@ public class IrisHydrology { @ArrayType(type = IrisDeepFluidConfig.class) @Desc("Independent deep-fluid systems that do not consume the surface or underground river source budgets.") private KList deepFluids = new KList<>(); + + @ArrayType(type = IrisSurfacePoolConfig.class, min = 0) + @Desc("Standing surface pools such as lava pools. Each region or biome opts in through riverPolicy.surfacePools.") + private KList surfacePools = new KList<>(); } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverPolicy.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverPolicy.java index 99d34da5a..89c088dca 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRiverPolicy.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRiverPolicy.java @@ -54,6 +54,10 @@ public class IrisRiverPolicy { @Desc("Flooded cave and grotto biome selection. An empty list explicitly clears the inherited selection.") private KList floodedCaveBiomes = null; + @ArrayType(type = String.class) + @Desc("Standing surface pool ids (hydrology.surfacePools) allowed in this area. An empty list explicitly clears the inherited selection.") + private KList surfacePools = null; + @MinNumber(0.0001) @MaxNumber(16) @Desc("Multiplier applied to accepted river width.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisSurfacePoolConfig.java b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfacePoolConfig.java new file mode 100644 index 000000000..569e08548 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisSurfacePoolConfig.java @@ -0,0 +1,54 @@ +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.iris.engine.object.annotations.Required; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Accessors(chain = true) +@NoArgsConstructor +@Desc("A standing surface pool: a bowl cut into open ground and filled with a fluid, placed where the river policy allows it.") +@Data +public class IrisSurfacePoolConfig { + @Required + @Desc("Pool identifier. Policies list it under surfacePools and locators accept it as a selector.") + private String id = "lava_pool"; + + @Required + @Desc("Fluid palette filling the pool.") + private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("lava"); + + @MinNumber(0) + @MaxNumber(64) + @Desc("Expected pools per hydrology tile where the policy allows them.") + private double density = 0.75D; + + @MinNumber(32) + @MaxNumber(8192) + @Desc("Nominal spacing in blocks between candidate pool sites.") + private int spacing = 384; + + @MinNumber(2) + @MaxNumber(16) + @Desc("Smallest pool radius in blocks.") + private int minimumRadius = 4; + + @MinNumber(2) + @MaxNumber(16) + @Desc("Largest pool radius in blocks.") + private int maximumRadius = 7; + + @MinNumber(1) + @MaxNumber(8) + @Desc("Fluid depth at the centre of the pool.") + private int depth = 2; + + @RegistryListResource(IrisBiome.class) + @Desc("Biome applied to the pool bed and its rim. Leave empty to keep the surrounding biome.") + private String biome = ""; +} diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java index b7d5a230b..7d4f6bee7 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackHydrologyValidatorTest.java @@ -156,6 +156,70 @@ public class PackHydrologyValidatorTest { assertTrue(result.errors().toString(), result.errors().stream().noneMatch((String error) -> error.contains(".bed"))); } + @Test + public void surfacePoolRulesRejectBadRangesAndUnknownPolicyReferences() throws Exception { + File pack = pack(""" + { + "regions": ["region"], + "dimensionHeight": {"min": -256, "max": 512}, + "hydrology": { + "rivers": {"enabled": true, "routing": {"tileSize": 1024, "sampleSpacing": 64, "oceanOutlets": true}}, + "surfacePools": [ + { + "id": "lava_pool", + "fluidPalette": {"palette": [{"block": "minecraft:lava"}]}, + "spacing": 16, + "minimumRadius": 8, + "maximumRadius": 4, + "depth": 9, + "biome": "no/such/biome" + }, + {"id": "pool", "fluidPalette": {"palette": [{"block": "minecraft:water"}]}} + ] + }, + "riverPolicy": {"surfacePools": ["lava_pool", "missing_pool"]} + } + """); + + PackHydrologyValidator.Validation result = validate(pack); + + assertContains(result.errors(), "spacing must be at least 32"); + assertContains(result.errors(), "minimumRadius must not exceed maximumRadius"); + assertContains(result.errors(), "depth must be at most 8"); + assertContains(result.errors(), "biome references unknown biome 'no/such/biome'"); + assertContains(result.errors(), "'pool' is a reserved hydrology feature selector"); + assertContains(result.errors(), "references unknown surface pool 'missing_pool'"); + } + + @Test + public void surfacePoolsAcceptAConfiguredPoolAndItsPolicyReference() throws Exception { + File pack = pack(""" + { + "regions": ["region"], + "dimensionHeight": {"min": -256, "max": 512}, + "hydrology": { + "rivers": {"enabled": true, "routing": {"tileSize": 1024, "sampleSpacing": 64, "oceanOutlets": true}}, + "surfacePools": [ + { + "id": "lava_pool", + "fluidPalette": {"palette": [{"block": "minecraft:lava"}]}, + "density": 0.75, + "spacing": 384, + "minimumRadius": 4, + "maximumRadius": 7, + "depth": 2 + } + ] + }, + "riverPolicy": {"surfacePools": ["lava_pool"]} + } + """); + + PackHydrologyValidator.Validation result = validate(pack); + + assertTrue(result.errors().toString(), result.errors().stream().noneMatch((String error) -> error.contains("surfacePool"))); + } + @Test public void rejectsMouthLevelingDistanceBeyondTheRouteLength() throws Exception { File pack = pack(riversDimension(""" 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 index 2b11e4366..2d931fcd6 100644 --- 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 @@ -46,6 +46,7 @@ public class IrisRendererRiverTest { expected.put(HydrologyFeatureType.MOUTH, new Color(54, 164, 205).getRGB()); expected.put(HydrologyFeatureType.DEEP_POOL, new Color(194, 50, 22).getRGB()); expected.put(HydrologyFeatureType.DEEP_CHANNEL, new Color(194, 50, 22).getRGB()); + expected.put(HydrologyFeatureType.STANDING_POOL, new Color(232, 120, 40).getRGB()); assertEquals(HydrologyFeatureType.values().length, expected.size()); for (HydrologyFeatureType type : HydrologyFeatureType.values()) { diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompilerTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompilerTest.java index b94ff5153..5da3bd5d9 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompilerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompilerTest.java @@ -268,7 +268,7 @@ public class HydrologyFootprintCompilerTest { base.underground(), base.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - List.of(deepFluid) + List.of(deepFluid), List.of() ); HydraulicSegment segment = new HydraulicSegment( 15L, diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyModelTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyModelTest.java index c8bed809d..37f864625 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyModelTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyModelTest.java @@ -510,6 +510,7 @@ public class HydrologyModelTest { "mouth", "ridge_tunnel", "deep", + "pool", "acid", "deep_lava" ), suggestions); @@ -527,7 +528,8 @@ public class HydrologyModelTest { "inland_grotto", "mouth", "ridge_tunnel", - "deep" + "deep", + "pool" ); assertEquals(builtInKeywords, HydrologyFeatureQuery.suggestions(List.of())); diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java index c2ac2671f..f31ca3dc1 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerCrossTileCaveAdmissionIntegrationTest.java @@ -288,7 +288,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest { 8 ), stableGeometry(), - List.of(deepFluid) + List.of(deepFluid), List.of() ); } @@ -362,7 +362,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest { base.outlets().maximumPerTile() ), base.geometry(), - base.deepFluids() + base.deepFluids(), List.of() ); } @@ -404,7 +404,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); }; } @@ -445,7 +445,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest { sample.shoreBiomeKey(), sample.bankBiomeKey(), sample.floodedCaveBiomeKey(), - sample.preferredProfileKeys() + sample.preferredProfileKeys(), List.of() ); }; } @@ -478,7 +478,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java index 835adb226..2889838cd 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerGlobalInvariantTest.java @@ -704,7 +704,7 @@ public class HydrologyPlannerGlobalInvariantTest { 8 ), HydrologyPlannerSettings.Geometry.defaults(), - deepFluids + deepFluids, List.of() ); } @@ -752,7 +752,7 @@ public class HydrologyPlannerGlobalInvariantTest { ), base.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - base.deepFluids() + base.deepFluids(), List.of() ); } @@ -945,7 +945,7 @@ public class HydrologyPlannerGlobalInvariantTest { "shore", "dry", "flooded", - List.of("default") + List.of("default"), List.of() ); } @@ -996,7 +996,7 @@ public class HydrologyPlannerGlobalInvariantTest { "shore", "dry", "flooded", - List.of("default") + List.of("default"), List.of() ); } @@ -1045,7 +1045,7 @@ public class HydrologyPlannerGlobalInvariantTest { "shore", "dry", "flooded", - List.of("default") + List.of("default"), List.of() ); } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java index fe6bb08d3..2d0f8e40c 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettingsTest.java @@ -62,7 +62,7 @@ public class HydrologyPlannerSettingsTest { base.underground(), base.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - List.of(overlongChannel) + List.of(overlongChannel), List.of() )); } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSurfacePoolTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSurfacePoolTest.java new file mode 100644 index 000000000..a952f8346 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSurfacePoolTest.java @@ -0,0 +1,119 @@ +package art.arcane.iris.engine.hydrology; + +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; + +public class HydrologyPlannerSurfacePoolTest { + private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0); + + @Test + public void poolsAreCarvedWhereThePolicyAllowsThemAndNowhereElse() { + HydrologyPlannerSettings settings = settings(2D, 128, 4, 6, 2); + HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land") + .withSurfacePoolKeys(x < 512 ? List.of("lava_pool") : List.of()); + + HydrologyTile tile = new HydrologyPlanner(91L, settings, terrain).plan(TILE); + HydrologyTile replay = new HydrologyPlanner(91L, settings, terrain).plan(TILE); + + assertEquals(tile, replay); + List pools = pools(tile); + assertFalse(tile.diagnosticCandidates().toString(), pools.isEmpty()); + for (RiverCourse pool : pools) { + assertEquals("lava_pool", pool.profileKey()); + assertEquals(1, pool.segments().size()); + HydraulicSegment segment = pool.segments().getFirst(); + assertEquals(HydrologyFeatureType.STANDING_POOL, segment.type()); + assertTrue(segment.width() >= 8 && segment.width() <= 12); + assertTrue(segment.start().x() < 512); + assertEquals(79, segment.upstreamHeadY()); + assertEquals(segment.upstreamHeadY(), segment.downstreamHeadY()); + } + } + + @Test + public void aPoolIsABowlWithALipInsideTheFootprint() { + HydrologyPlannerSettings settings = settings(2D, 256, 5, 5, 2); + HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land") + .withSurfacePoolKeys(List.of("lava_pool")); + + HydrologyTile tile = new HydrologyPlanner(7L, settings, terrain).plan(TILE); + RiverCourse pool = pools(tile).getFirst(); + HydrologyPoint center = pool.segments().getFirst().centerline().get(1); + + int wet = 0; + int shore = 0; + for (HydrologyColumnSample column : tile.footprint().columns().values()) { + HydrologyColumnLayer layer = column.primarySurfaceLayer().orElse(null); + if (layer == null || layer.feature().courseId() != pool.id()) { + continue; + } + assertEquals(HydrologyFeatureType.STANDING_POOL, layer.feature().type()); + assertTrue(column.terrainHeight() <= column.naturalHeight()); + if (layer.channel()) { + wet++; + assertEquals("lava_pool", layer.profileKey()); + assertEquals(79, layer.fluidHeadY()); + assertTrue(layer.bedY() < layer.fluidHeadY()); + assertTrue(center.distanceSquared2D(new HydrologyPoint(column.x(), 0, column.z())) <= 8 * 8); + } else if (layer.shore()) { + shore++; + assertTrue(column.terrainHeight() >= 80); + } + } + assertTrue("wet=" + wet, wet >= 60 && wet <= 140); + assertTrue("shore=" + shore, shore > 0); + } + + @Test + public void poolsKeepClearOfEachOther() { + HydrologyPlannerSettings settings = settings(8D, 64, 6, 6, 2); + HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land") + .withSurfacePoolKeys(List.of("lava_pool")); + + HydrologyTile tile = new HydrologyPlanner(3L, settings, terrain).plan(TILE); + List pools = pools(tile); + + assertTrue(pools.size() >= 2); + for (int first = 0; first < pools.size(); first++) { + for (int second = first + 1; second < pools.size(); second++) { + HydrologyPoint a = pools.get(first).segments().getFirst().centerline().get(1); + HydrologyPoint b = pools.get(second).segments().getFirst().centerline().get(1); + assertTrue(a.distanceSquared2D(b) >= 40L * 40L); + } + } + } + + private static List pools(HydrologyTile tile) { + ArrayList pools = new ArrayList<>(); + for (RiverCourse course : tile.courses()) { + if (course.type() == RiverCourseType.SURFACE_POOL) { + pools.add(course); + } + } + return pools; + } + + private static HydrologyPlannerSettings settings(double density, int spacing, int minimumRadius, int maximumRadius, int depth) { + HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults(); + HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing( + 1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D); + return new HydrologyPlannerSettings( + base.seaLevel(), + routing, + base.surface(), + base.hydraulics(), + base.underground(), + base.outlets(), + base.geometry(), + List.of(), + List.of(new HydrologyPlannerSettings.SurfacePool( + "lava_pool", true, density, spacing, minimumRadius, maximumRadius, depth, 64, null)) + ); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java index de0ae6303..1f1a344b2 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyPlannerTest.java @@ -514,7 +514,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("alpha") + List.of("alpha"), List.of() ); }; HydrologyTile tile = new HydrologyPlanner(812L, settings, terrain, solidCaveView()).plan(TILE); @@ -588,7 +588,7 @@ public class HydrologyPlannerTest { base.underground(), base.outlets(), base.geometry(), - base.deepFluids() + base.deepFluids(), List.of() ); HydrologyTerrainSampler repeatedRidges = (int x, int z) -> { if (x >= 112) { @@ -1122,7 +1122,7 @@ public class HydrologyPlannerTest { base.underground(), base.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - base.deepFluids() + base.deepFluids(), List.of() ); HydrologyPlannerSettings settings = withOutlets(shaped, new HydrologyPlannerSettings.Outlets( true, @@ -1408,7 +1408,7 @@ public class HydrologyPlannerTest { base.underground(), base.outlets(), base.geometry(), - base.deepFluids() + base.deepFluids(), List.of() ); HydrologyTerrainSampler terrain = (int x, int z) -> { if (x >= 112) { @@ -1769,7 +1769,7 @@ public class HydrologyPlannerTest { 4 ), stableGeometry(), - deepFluids + deepFluids, List.of() ); } @@ -1824,7 +1824,7 @@ public class HydrologyPlannerTest { 1 ), HydrologyPlannerSettings.Geometry.defaults(), - List.of() + List.of(), List.of() ); } @@ -1893,7 +1893,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("alpha") + List.of("alpha"), List.of() ); }; } @@ -1958,7 +1958,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("alpha", "beta") + List.of("alpha", "beta"), List.of() ); } @@ -1990,7 +1990,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("alpha") + List.of("alpha"), List.of() ); } @@ -2031,7 +2031,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("beta", "alpha") + List.of("beta", "alpha"), List.of() ); } @@ -2063,7 +2063,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("alpha") + List.of("alpha"), List.of() ); } @@ -2095,7 +2095,7 @@ public class HydrologyPlannerTest { "shore", "dry", "flooded", - List.of("alpha", "beta") + List.of("alpha", "beta"), List.of() ); } @@ -2121,7 +2121,7 @@ public class HydrologyPlannerTest { settings.underground(), settings.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - settings.deepFluids() + settings.deepFluids(), List.of() ); } @@ -2150,7 +2150,7 @@ public class HydrologyPlannerTest { ), settings.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - settings.deepFluids() + settings.deepFluids(), List.of() ); } @@ -2180,7 +2180,7 @@ public class HydrologyPlannerTest { ), settings.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - settings.deepFluids() + settings.deepFluids(), List.of() ); } @@ -2209,7 +2209,7 @@ public class HydrologyPlannerTest { ), settings.outlets(), HydrologyPlannerSettings.Geometry.defaults(), - settings.deepFluids() + settings.deepFluids(), List.of() ); } @@ -2269,7 +2269,7 @@ public class HydrologyPlannerTest { settings.underground(), outlets, HydrologyPlannerSettings.Geometry.defaults(), - settings.deepFluids() + settings.deepFluids(), List.of() ); } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java index d9c00d093..1e1b52312 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyStyledGeometryTest.java @@ -231,7 +231,7 @@ public class HydrologyStyledGeometryTest { 4 ), HydrologyPlannerSettings.Geometry.defaults(), - deepFluids + deepFluids, List.of() ); } @@ -264,7 +264,7 @@ public class HydrologyStyledGeometryTest { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); } int height = 118 - Math.floorDiv(x, 12) + (int) StrictMath.round(StrictMath.sin(z / 18D) * 2D); @@ -296,7 +296,7 @@ public class HydrologyStyledGeometryTest { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSampleTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSampleTest.java index 712d0bb93..03c42a22c 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSampleTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSampleTest.java @@ -77,7 +77,7 @@ public final class HydrologyTerrainSampleTest { "shore", "dry", "flooded", - profiles + profiles, List.of() ); } } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java index 72258e553..9e574a62e 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java @@ -259,7 +259,7 @@ public class HydrologyTileCacheTest { 2 ), HydrologyPlannerSettings.Geometry.defaults(), - List.of() + List.of(), List.of() ); } @@ -297,7 +297,7 @@ public class HydrologyTileCacheTest { 2 ), HydrologyPlannerSettings.Geometry.defaults(), - List.of() + List.of(), List.of() ); } @@ -338,7 +338,7 @@ public class HydrologyTileCacheTest { "shore", "dry", "flooded", - List.of("default") + List.of("default"), List.of() ); } } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java index e224ef72c..e1e52f509 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/surface/ChannelProfileBuilderTest.java @@ -104,7 +104,7 @@ public class ChannelProfileBuilderTest { "parent", "parent", "parent", - List.of("water") + List.of("water"), List.of() ); return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY); } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java index 9716a4925..54134bf9e 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleHydrologyComponentTest.java @@ -1566,7 +1566,7 @@ public class MantleHydrologyComponentTest { 8 ), HydrologyPlannerSettings.Geometry.defaults(), - List.of() + List.of(), List.of() ); } @@ -1603,7 +1603,7 @@ public class MantleHydrologyComponentTest { "shore", "dry", "flooded", - List.of("default") + List.of("default"), List.of() ); } diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java index fc9fa53b1..e4e1a5288 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java @@ -215,7 +215,8 @@ public final class HydrologyPackProbe { enum CoverageFamily { SURFACE, UNDERGROUND, - DEEP + DEEP, + POOL } enum VerificationFamily { @@ -4080,6 +4081,7 @@ public final class HydrologyPackProbe { case SURFACE -> CoverageFamily.SURFACE; case UNDERGROUND -> CoverageFamily.UNDERGROUND; case DEEP_FLUID -> CoverageFamily.DEEP; + case SURFACE_POOL -> CoverageFamily.POOL; }; return new ConfiguredCoverage(family, course.profileKey()); } diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java index 4ab40b4cf..3c3503143 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlannerProbe.java @@ -265,7 +265,7 @@ public final class HydrologyPlannerProbe { 4 ), HydrologyPlannerSettings.Geometry.defaults(), - List.of() + List.of(), List.of() ); } @@ -304,7 +304,7 @@ public final class HydrologyPlannerProbe { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); }; } @@ -355,7 +355,7 @@ public final class HydrologyPlannerProbe { "shore", "bank", "flooded", - List.of("water") + List.of("water"), List.of() ); }; } @@ -476,7 +476,7 @@ public final class HydrologyPlannerProbe { 8 ), HydrologyPlannerSettings.Geometry.defaults(), - List.of(deepFluid) + List.of(deepFluid), List.of() ); } @@ -521,7 +521,7 @@ public final class HydrologyPlannerProbe { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); }; } @@ -554,7 +554,7 @@ public final class HydrologyPlannerProbe { "shore", "dry", "flooded", - List.of("water") + List.of("water"), List.of() ); } diff --git a/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java b/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java index d1d44d931..559a8fe74 100644 --- a/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/RiverTransectProbe.java @@ -186,7 +186,7 @@ public final class RiverTransectProbe { writeTileDiagnostics(new File(configuration.output(), "tile-diagnostics.png"), tile, complex, runtime, seaLevel); List summaries = new ArrayList<>(); for (RiverCourse course : tile.courses()) { - if (course.type() != RiverCourseType.SURFACE) { + if (course.type() != RiverCourseType.SURFACE && course.type() != RiverCourseType.SURFACE_POOL) { continue; } List exposedPath = exposedPath(course);