using System; using System.Collections.Generic; using Godot; using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// /// ⭐ PASS 2b — HYDRAULIC EROSION, THE CALLER (chat2/11). Runs on /// the RENDER field of a shaped result — after the curve + detail (pass 2a), before the crater /// carve (which does not exist yet) — exactly the reference's slot (MapGenerator.cs:737-805). /// /// ═══ THE THREE THINGS THE CALLER OWNS (ported from the reference caller) ═══ /// /// 1. THE SPLIT. Only the render field is eroded; the classify field is finalized before the pass /// and never sees it — biomes / water / region labeling classify pre-erosion (the oracle). /// If the two fields are aliased (curve off), the render field is COPIED first, as the /// reference allocated a separate classify array when erosion was on. /// 2. THE FLOOD GUARD. Render-map water pixels are counted BEFORE and AFTER; any change throws /// "EROSION FLOOD-GUARD VIOLATION". With the pass's sea clamp (below-sea cells read-only in /// both directions; carve floor at sea + margin) this is the active proof that no coastline /// moved — mainland and every island alike. /// 3. THE CRATER EXCLUSION, INERT. The weight is passed whole (core / feather radii, mode) but /// with no crater in v2 the radius is 0 ⇒ weight 1 everywhere. It activates when the crater /// carve lands; the reference's "CraterErosionCore < carve factor" warning is DORMANT until /// then (it concerns the erosion↔carve interaction, which does not exist yet). /// /// The governors are clamped as the reference's ConfigManager clamped them (count [0, 50 M], /// lifetime [1, 4096], carve cap [0, 60], deposit cap [0, 60], sea margin [0, 5], inertia /// [0, 0.99], evaporation [0, 0.5]) and a clamp is reported, not silent. /// public static class ErosionPass { public sealed class Result { public Pass2Result Shaped; // the result with the eroded RENDER field (classify untouched) public HydraulicErosion.Stats Stats; public HydraulicErosion.Params Params; public long WetBefore, WetAfter; public ulong Ms; public List Notes = new(); } /// Count render-map water pixels (height below the flat sea) — the flood guard's instrument. public static long CountWaterPixels(float[,] height, int mapSize, float sea) { long wet = 0; for (int x = 0; x < mapSize; x++) for (int y = 0; y < mapSize; y++) if (height[x, y] < sea) wet++; return wet; } /// /// Erode 's render field (a copy if aliased to classify) and return the /// result. Throws on a governor-cap violation (from the pass) or a flood-guard violation. /// public static Result Apply(Pass2Result p2, TerrainGenConfig cfg) { ulong t0 = Time.GetTicksMsec(); var r = new Result(); int n = p2.MapSize; float sea = cfg.SeaLevel; // 1. THE SPLIT — never erode an array the classify field shares. float[,] render = p2.Height; if (p2.FieldsAreAliased) { render = (float[,])p2.Height.Clone(); r.Notes.Add("[Erosion] render and classify were aliased (curve off) — the render field was copied before eroding, as the reference allocated a separate classify array when erosion was on."); } // The governors, clamped as the reference's ConfigManager clamped them — reported, not silent. int count = Math.Clamp(cfg.ErosionDropletCount, 0, 50_000_000); int life = Math.Clamp(cfg.ErosionDropletLifetime, 1, 4096); float carve = Math.Clamp(cfg.ErosionCarveCapM, 0f, 60f); float deposit = Math.Clamp(cfg.ErosionDepositCapM, 0f, 60f); float margin = Math.Clamp(cfg.ErosionSeaMarginM, 0f, 5f); float inertia = Math.Clamp(cfg.ErosionInertia, 0f, 0.99f); float evap = Math.Clamp(cfg.ErosionEvaporation, 0f, 0.5f); if (count != cfg.ErosionDropletCount || life != cfg.ErosionDropletLifetime || carve != cfg.ErosionCarveCapM || deposit != cfg.ErosionDepositCapM || margin != cfg.ErosionSeaMarginM || inertia != cfg.ErosionInertia || evap != cfg.ErosionEvaporation) r.Notes.Add($"[Erosion] ⚠ governor out of bounds — clamped: count {cfg.ErosionDropletCount}→{count}, lifetime {cfg.ErosionDropletLifetime}→{life}, carve {cfg.ErosionCarveCapM}→{carve} m, deposit {cfg.ErosionDepositCapM}→{deposit} m, margin {cfg.ErosionSeaMarginM}→{margin} m, inertia {cfg.ErosionInertia}→{inertia}, evaporation {cfg.ErosionEvaporation}→{evap}."); // 3. THE CRATER EXCLUSION — inert: no crater ⇒ radius 0 ⇒ weight 1 everywhere. float craterRadius = cfg.CraterRadius; // 0 in v2 until the crater task float coreR = craterRadius * cfg.CraterErosionCore; float featherR = craterRadius * cfg.CraterErosionFeather; const float CRATER_CARVE_FACTOR = 0.80f; // the reference's carve radius factor if (craterRadius > 0f && cfg.CraterErosionCore < CRATER_CARVE_FACTOR) r.Notes.Add($"[Erosion] ⚠ CraterErosionCore {cfg.CraterErosionCore:F2} is inside the carve radius ({CRATER_CARVE_FACTOR:F2} × CraterRadius) — erosion will modify carve-authored terrain and the carve will amplify those deltas across the waterline (the reference's warning; live only once a crater exists)."); // 2. THE FLOOD GUARD — before. r.WetBefore = CountWaterPixels(render, n, sea); r.Params = new HydraulicErosion.Params { DropletCount = count, Lifetime = life, CarveCapM = carve, DepositCapM = deposit, SeaMarginM = margin, BrushRadius = Math.Max(0, cfg.ErosionBrushRadius), Inertia = inertia, CapacityFactor = cfg.ErosionCapacity, MinSlopeM = cfg.ErosionMinSlopeM, ErodeRate = cfg.ErosionErodeRate, DepositRate = cfg.ErosionDepositRate, Evaporation = evap, Gravity = cfg.ErosionGravity, CraterMode = cfg.CraterErosionFeatherMode ? HydraulicErosion.CRATER_MODE_FEATHER : HydraulicErosion.CRATER_MODE_FULL, Seed = cfg.Seed + HydraulicErosion.SEED_OFFSET, }; r.Stats = HydraulicErosion.Apply(render, n, null, sea, cfg.CraterCenterX, cfg.CraterCenterY, coreR, featherR, r.Params); // 2. THE FLOOD GUARD — after. Any change refuses the generation. r.WetAfter = CountWaterPixels(render, n, sea); if (r.WetAfter != r.WetBefore) throw new InvalidOperationException( $"[ErosionPass] EROSION FLOOD-GUARD VIOLATION: render-map water pixels {r.WetBefore} -> {r.WetAfter}. Refusing to generate."); r.Ms = Time.GetTicksMsec() - t0; var st = r.Stats; r.Notes.Add($"[Erosion] crater exclusion: {(craterRadius > 0f ? $"core {coreR:F0} px → feather {featherR:F0} px" : "INERT (no crater; weight 1 everywhere)")}."); r.Notes.Add($"[Erosion] v1: {st.Spawned} droplets ({st.SkippedNoLand} skipped), {st.Steps:N0} steps, {r.Ms / 1000.0:F1}s wall. " + $"Eroded {st.ErodedVolumeM3:F0} m³ over {st.ModifiedCells:N0} touched cells (max cell carve {st.MaxCellErosionM:F2} m vs cap {carve:F2} m), " + $"deposited {st.DepositedVolumeM3:F0} m³ (max cell deposit {st.MaxCellDepositM:F2} m vs cap {deposit:F2} m). " + $"Deaths: {st.DiedSea} sea / {st.DiedEdge} edge / {st.DiedDry} dry / {st.DiedLifetime} lifetime. " + $"Water pixels {r.WetBefore:N0} -> {r.WetAfter:N0} (flood guard holds)."); r.Shaped = p2.WithHeight(render, r.Notes, r.Ms); return r; } } }