diff --git a/Core/Scripts/BLUEPRINT_FORMAT.md b/Core/Scripts/BLUEPRINT_FORMAT.md index 6e3d925..f905988 100644 --- a/Core/Scripts/BLUEPRINT_FORMAT.md +++ b/Core/Scripts/BLUEPRINT_FORMAT.md @@ -59,7 +59,7 @@ Every section: `[u32 tag][u64 payload-length in bytes][payload]`. | `TDTL` / `0x4C544454` | 30 B: `u16 detailVersion` · `f32 reliefAmpM` · `f32 reliefFreqIslands` · `i32 reliefSeedOffset` · `f32 edgeAmpM` · `f32 edgeFreqIslands` · `i32 edgeSeedOffset` · `f32 edgeMaxShiftM` | Optional — present iff the terrain detail passes shaped this blueprint's `HGTS` (config `TerrainDetail: "v1"`, requires the curve). **`detailVersion` selects the body layout and a reader that does not recognise it SKIPS the section** (leaving detail metadata null) rather than misreading a differently shaped payload — the one section whose body is versioned rather than extended, because v1's layout was retired rather than grown. **v1 (retired, never shipped)** — shelf micro-relief + D8 drainage incision, 38 B; the incision produced grid-aligned artifacts and was reverted whole, so the only v1 payloads that exist are in that batch's own tree. **v2 (current)** — shelf micro-relief (±`reliefAmpM` output metres, shelf-ness weighted) + shelf-edge variation: a per-column shift of the curve's shelf/riser knot block K3/K4/K5, drawn from a Simplex field seeded `resolvedWorldSeed + edgeSeedOffset` at `edgeFreqIslands/MapSize`, amplitude ±`edgeAmpM` **metres of INPUT height** — a displacement of the shelf boundary contour, not an elevation change. `edgeAmpM` is recorded **as applied**, after the clamp to `edgeMaxShiftM` (the preset's band-squeeze bound), so the record always describes the terrain rather than the request. **Metadata only** — heights are already detailed. Machinery: `Tools/Scripts/TerrainDetailPass.cs`. | | `EROS` / `0x534F5245` | 67 B: `u16 erosionVersion` · `i32 dropletCount` · `i32 lifetime` · `i32 brushRadius` · `i32 seedOffset` · `f32 carveCapM` · `f32 depositCapM` · `f32 seaMarginM` · `f32 inertia` · `f32 capacityFactor` · `f32 minSlopeM` · `f32 erodeRate` · `f32 depositRate` · `f32 evaporation` · `f32 gravity` · `f32 craterCoreFactor` · `f32 craterFeatherFactor` · `u8 craterMode` | Optional — present iff the droplet hydraulic-erosion pass shaped this blueprint's `HGTS` (config `Erosion: "v1"`, terrain-water tasks 17–19). **`erosionVersion` selects the body layout; an unrecognised version is SKIPPED whole** (erosion metadata left null), same rule as `TDTL`. **v1 (task 17)** — 58 B, no `depositCapM`; deposition was bilinear over 4 cells and unbounded, which built isolated cones (measured 15.5 m). **v2 (task 18)** — 62 B; deposition brush-spread and per-cell bounded. **v3 (task 19, current)** — replaces v2's single `craterExclFactor` with `craterCoreFactor` (the protected strike core), `craterFeatherFactor` and `craterMode` (`0` = full, `1` = feather). Only v2+ payloads exist outside tasks 17–18's own batch trees. The FOUR governors are `dropletCount`/`lifetime`/`carveCapM`/`depositCapM`; both caps are enforced against one per-cell NET displacement ledger (positive = carved below the height the pass found, negative = built up above it) and asserted on exit. `seaMarginM` is the flood-guard clamp — no cell is carved below sea + margin, and below-sea cells are untouched in BOTH directions, so the rendered coastline cannot move **and the crater's flooded bay can be neither carved open nor silted shut regardless of `craterMode`**. Crater radii are FACTORS of `PRMS.CraterRadius`: nothing inside `craterCoreFactor ×` it is modified; `feather` ramps erosion 0→full from there out to `craterFeatherFactor ×` it, `full` applies full strength immediately. Droplets are deterministic from `resolvedWorldSeed + seedOffset` (PCG32). Remaining fields are the droplet-model strength dials; slopes/amounts in metres (1 raw = 251 m). All values recorded **as applied** (post config clamping). **Metadata only** — heights are already eroded, and the classify-side sections (`BIOM`/`WBID`/…) never saw the pass by design. Machinery: `Tools/Scripts/HydraulicErosion.cs`. | | `WBID` / `0x44494257` | `MapSize²` × `u16` water-body id, same pixel order as `HGTS` | Optional (absent = no water data, e.g. a legacy re-encode). `0` = no water, `1` = **the** ocean body, `2..N` = lakes. Ids assigned in deterministic scan order (X outer / Y inner, first-encountered pixel), lakes labeled with the **same 4-connectivity as `CalculateTrueOcean`**. Membership is exactly the generator's water classification — the biome grid's Ocean/Lake pixels and this grid's nonzero pixels are the same set **by construction** (shared predicates). Length must equal `2·MapSize²`. | -| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake, 2 river — a stepped river REACH, one flat level per reach; task 23) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. | +| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. | | `WSRF` / `0x46525357` | `MapSize²` × `u16` quantized water-surface elevation, same pixel order | Optional, paired with `WBID`. `0` is the reserved **no-water sentinel**; a real level `L` (raw height units) encodes as `1 + round(L × 32768)` so it can never encode to 0; decode `(q − 1)/32768` (`BlueprintFormat.EncodeWaterLevel`/`DecodeWaterLevel`). Covers `[0 … ~1.99997]` raw at `1/32768` raw ≈ **7.7 mm** of world height (1 raw = 251 m) — far finer than the 1 m voxel. Nonzero exactly where `WBID` is nonzero; the value is the pixel's body level. | | `RDHW` / `0x57484452` | `i32 pathCount`, then per path: `i32 pointCount` + `pointCount` × (`f32 X` · `f32 Y`) | Highway tier. **Tags, not file position, identify the tier** — the v1 order-fragility is gone. Road sections may legitimately be **present but empty** (zero paths): the `SkipRoads` config toggle exports a road-less iteration blueprint. | | `RDBR` / `0x52424452` | same layout | Branch tier. | diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs index f23a402..2ae8816 100644 --- a/Core/Scripts/ConfigManager.cs +++ b/Core/Scripts/ConfigManager.cs @@ -113,42 +113,14 @@ namespace IslaApocalypse.Core // Change this if your namespace is different // terrain-aware), "lowground" follows the lowest ground and wanders like a // real river — the task-22 gate decides which ships; "lowground" is the // provisional default pending that verdict. Width/depth scales are taste - // dials on the flow-proportional bed profile. LOWGROUND is the LOCKED - // default — the task-22/23 gate verdict ("short" stays available for the - // record). RiverSeaMargin is the bed's + // dials on the flow-proportional bed profile. RiverSeaMargin is the bed's // absolute floor above sea — the erosion flood-guard discipline: no river // bed may create inland below-sea cells, so the rendered coastline cannot // move even with rivers carved. - // RiverStepDropM/RiverWaterDepthM (task 23): the stepped-water dials — each - // river is a chain of flat water-body reaches; a new reach starts every - // StepDrop metres of bed descent and sits WaterDepth metres above its bed. - // Smaller drop = more, finer steps = smoother water (the smoothing dial; - // tilted continuous-slope water is the deferred model B). public static string Rivers = "off"; public static string RiverRoutingStyle = "lowground"; - public static float RiverWidthScale = 1.75f; // widened at the task-23 gate's ask - // Task 24 (the gate's polish): finer steps read as a descending river rather - // than a pond staircase; deeper water sits contained in its banks. - // RiverTribWaterMinFlow is the drainage a TRIBUTARY reach needs to carry - // water (0 = water them end to end); above it the water TAPERS to dry over - // RiverTribTaperPx so a stream head fades instead of ending in a wall. - // RiverLakeMinTargetPx is the smallest water body a lake-ender may target. - // Task 25: RiverFillFraction fills the channel to a fraction of its LOCAL bed - // depth (a fixed height was a trickle at deep mouths and overtopped shallow - // heads); RiverWaterDepthM stays as the absolute minimum. RiverBankFlare - // widens the bank shoulder (half-widths beyond the channel) and it now uses a - // smootherstep, so water meets land as a shore instead of a wall. - // RiverTribWaterMinFlow defaults to 0 — tributaries carry water their full - // promoted length (the gate's preference), still fading at the tip. - public static float RiverStepDropM = 0.6f; - public static float RiverWaterDepthM = 1.0f; - public static float RiverFillFraction = 0.80f; - public static float RiverBankFlare = 3.2f; - public static float RiverBankMaxCutM = 3.0f; // shoulder-only cut cap (task 25) - public static int RiverTribWaterMinFlow = 0; - public static int RiverTribTaperPx = 120; - public static int RiverLakeMinTargetPx = 20000; - public static float RiverDepthScale = 1.5f; // deepened at the task-24 gate's ask + public static float RiverWidthScale = 1.0f; + public static float RiverDepthScale = 1.0f; public static float RiverSeaMargin = 0.2f; // m above sea, bed floor // Island falloff shaping (task 11). @@ -356,22 +328,6 @@ namespace IslaApocalypse.Core // Change this if your namespace is different if (data.ContainsKey("RiverWidthScale")) RiverWidthScale = (float)data["RiverWidthScale"]; if (data.ContainsKey("RiverDepthScale")) RiverDepthScale = (float)data["RiverDepthScale"]; if (data.ContainsKey("RiverSeaMargin")) RiverSeaMargin = (float)data["RiverSeaMargin"]; - if (data.ContainsKey("RiverStepDropM")) RiverStepDropM = (float)data["RiverStepDropM"]; - if (data.ContainsKey("RiverWaterDepthM")) RiverWaterDepthM = (float)data["RiverWaterDepthM"]; - if (data.ContainsKey("RiverTribWaterMinFlow")) RiverTribWaterMinFlow = (int)data["RiverTribWaterMinFlow"]; - if (data.ContainsKey("RiverTribTaperPx")) RiverTribTaperPx = (int)data["RiverTribTaperPx"]; - if (data.ContainsKey("RiverLakeMinTargetPx")) RiverLakeMinTargetPx = (int)data["RiverLakeMinTargetPx"]; - if (data.ContainsKey("RiverFillFraction")) RiverFillFraction = (float)data["RiverFillFraction"]; - if (data.ContainsKey("RiverBankFlare")) RiverBankFlare = (float)data["RiverBankFlare"]; - RiverFillFraction = Mathf.Clamp(RiverFillFraction, 0.1f, 1f); - if (data.ContainsKey("RiverBankMaxCutM")) RiverBankMaxCutM = (float)data["RiverBankMaxCutM"]; - RiverBankFlare = Mathf.Clamp(RiverBankFlare, 1.2f, 8f); - RiverBankMaxCutM = Mathf.Clamp(RiverBankMaxCutM, 0f, 30f); - RiverTribWaterMinFlow = Mathf.Max(RiverTribWaterMinFlow, 0); - RiverTribTaperPx = Mathf.Clamp(RiverTribTaperPx, 0, 2000); - RiverLakeMinTargetPx = Mathf.Max(RiverLakeMinTargetPx, 0); - RiverStepDropM = Mathf.Clamp(RiverStepDropM, 0.25f, 10f); - RiverWaterDepthM = Mathf.Clamp(RiverWaterDepthM, 0.2f, 5f); RiverWidthScale = Mathf.Clamp(RiverWidthScale, 0.1f, 5f); RiverDepthScale = Mathf.Clamp(RiverDepthScale, 0.1f, 5f); RiverSeaMargin = Mathf.Clamp(RiverSeaMargin, 0f, 5f); diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs index 8f05503..d86ed9b 100644 --- a/Core/Scripts/MapDataParser.cs +++ b/Core/Scripts/MapDataParser.cs @@ -47,7 +47,6 @@ namespace IslaApocalypse.Core { public const byte TYPE_OCEAN = 0; public const byte TYPE_LAKE = 1; - public const byte TYPE_RIVER = 2; // task 23: a stepped river REACH (one flat level) public const byte SALINITY_FRESH = 0; public const byte SALINITY_SALT = 1; @@ -163,24 +162,6 @@ namespace IslaApocalypse.Core { public static WorldBlueprint LoadMapData(string seedStr) { - // ISLA_BLUEPRINT_PATH (task 25, FILE SAFETY): load an explicit blueprint - // FILE instead of the seed-named one in the runtime root. Capture and A/B - // tooling points this straight at a batch folder, so it never has to copy - // a blueprint over — or delete — the file the developer has staged at the - // root to view a world in 3D. Loud, so a forgotten env var cannot be - // mistaken for the root blueprint. - string envPath = OS.GetEnvironment("ISLA_BLUEPRINT_PATH"); - if (!string.IsNullOrEmpty(envPath)) - { - if (File.Exists(envPath)) - { - GD.Print($"[MapDataParser] ⚠ ISLA_BLUEPRINT_PATH override: loading '{envPath}' " + - "(NOT the runtime-root blueprint)."); - return LoadMapDataFromPath(envPath); - } - GD.PrintErr($"[MapDataParser] ISLA_BLUEPRINT_PATH set but '{envPath}' does not exist — " + - "falling back to the runtime-root blueprint."); - } string filePath = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat"); return LoadMapDataFromPath(filePath); } @@ -451,7 +432,7 @@ namespace IslaApocalypse.Core body.SurfaceLevel = reader.ReadSingle(); body.PixelCount = reader.ReadInt32(); body.Centroid = new Vector2(reader.ReadSingle(), reader.ReadSingle()); - if (body.Type > WaterBodyInfo.TYPE_RIVER) + if (body.Type > WaterBodyInfo.TYPE_LAKE) { GD.PrintErr($"[MapDataParser] ERROR: water body {i} has unknown type {body.Type}."); return false; diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs index eaa8b7c..e8ff480 100644 --- a/Tools/Scripts/MapGenerator.cs +++ b/Tools/Scripts/MapGenerator.cs @@ -237,16 +237,7 @@ public partial class MapGenerator : TextureRect // only: biomes and WBID are already computed from classify — the oracle is // untouched by construction. NO WATER — part 2b. if (_riversOn) - { CarveRivers(); - // The 0_height/0_water snapshots were captured before rivers existed — - // re-draw and re-capture so the exported PNGs show the carved beds and - // the new river water (task-22 nit 2). - DrawHeightStageTexture(); - await CaptureStage("0_height"); - DrawWaterStageTexture(); - await CaptureStage("0_water"); - } if (ConfigManager.SkipRoads) { @@ -335,12 +326,12 @@ public partial class MapGenerator : TextureRect // 6. Pull the image from the invisible monitor and save it! Image capture = offscreenVP.GetTexture().GetImage(); string seedStr = _noise.Seed.ToString(); - string fileName = System.IO.Path.Combine(OutputDir(), $"Map_Seed_{seedStr}_{label}.png"); + string fileName = $"user://Map_Seed_{seedStr}_{label}.png"; Error saveResult = capture.SavePng(fileName); if (saveResult == Error.Ok) - GD.Print($"{T()} Map saved: {fileName}"); + GD.Print($"{T()} Map saved: {ProjectSettings.GlobalizePath(fileName)}"); else GD.PrintErr($"{T()} Failed to save map. Godot Error code: {saveResult}"); @@ -348,40 +339,18 @@ public partial class MapGenerator : TextureRect offscreenVP.QueueFree(); } - /// - /// Where generated artifacts go. Normally the runtime root (`user://`), but - /// ISLA_EXPORT_DIR redirects EVERYTHING this run writes — blueprints and stage - /// snapshots — into a task folder instead (task 25, FILE SAFETY). Batch and A/B - /// tooling sets it so a generation can never overwrite the blueprint the - /// developer has staged at the root to view a world in 3D, and so no cleanup - /// move/delete is needed afterwards. Loud on use. - /// - private string OutputDir() - { - string dir = OS.GetEnvironment("ISLA_EXPORT_DIR"); - if (!string.IsNullOrEmpty(dir)) - { - if (System.IO.Directory.Exists(dir)) return dir; - GD.PrintErr($"[MapGenerator] ISLA_EXPORT_DIR '{dir}' does not exist — writing to the runtime root instead."); - } - return ProjectSettings.GlobalizePath("user://"); - } - private void ExportMapData() { string seedStr = _noise.Seed.ToString(); - string outDir = OutputDir(); - if (outDir != ProjectSettings.GlobalizePath("user://")) - GD.Print($"{T()} ⚠ ISLA_EXPORT_DIR override: writing artifacts to '{outDir}' (NOT the runtime root)."); // PRIMARY: the v2 tagged-section container (Core/Scripts/BLUEPRINT_FORMAT.md), // under the name the server looks for. - string v2Path = System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}.dat"); + string v2Path = ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}.dat"); BlueprintWriter.WriteV2(v2Path, BuildBlueprint()); // SAFETY NET: the legacy v1 format beside it, until the developer has lived // with v2 across several regenerations. Removal is a future task. - ExportMapDataV1(System.IO.Path.Combine(outDir, $"MapData_Seed_{seedStr}_v1.dat")); + ExportMapDataV1(ProjectSettings.GlobalizePath($"user://MapData_Seed_{seedStr}_v1.dat")); } /// @@ -867,17 +836,6 @@ public partial class MapGenerator : TextureRect isClassifyWater[x * MapSize + y] = IsWaterPixel(x, y); } - // Task 24: SIGNIFICANT water — cells of bodies at least RiverLakeMinTargetPx - // in size, from the water-bodies table the stage above already built. A - // lake-ender routes to this so it enters the lagoon, not a puddle. - var bigBodies = new System.Collections.Generic.HashSet(); - foreach (var b in _waterBodies) - if (b.PixelCount >= ConfigManager.RiverLakeMinTargetPx) bigBodies.Add(b.Id); - bool[] isSignificantWater = new bool[MapSize * MapSize]; - for (int x = 0; x < MapSize; x++) - for (int y = 0; y < MapSize; y++) - isSignificantWater[x * MapSize + y] = bigBodies.Contains(_waterBodyIds[x, y]); - float southX = -1f, southY = -1f; foreach (var t in _towns) if (t.Position.Y > southY) { southX = t.Position.X; southY = t.Position.Y; } @@ -888,16 +846,10 @@ public partial class MapGenerator : TextureRect ? RiverCarvePass.STYLE_SHORT : RiverCarvePass.STYLE_LOWGROUND, WidthScale = ConfigManager.RiverWidthScale, DepthScale = ConfigManager.RiverDepthScale, - SeaMarginM = ConfigManager.RiverSeaMargin, - TribWaterMinFlowPx = ConfigManager.RiverTribWaterMinFlow, - TribTaperPx = ConfigManager.RiverTribTaperPx, - FillFraction = ConfigManager.RiverFillFraction, - BankFlare = ConfigManager.RiverBankFlare, - BankMaxCutM = ConfigManager.RiverBankMaxCutM, - LakeMinTargetPx = ConfigManager.RiverLakeMinTargetPx + SeaMarginM = ConfigManager.RiverSeaMargin }; var st = RiverCarvePass.Apply(_heightMap, MapSize, isOcean, isClassifyWater, - isSignificantWater, southX, southY, seaMap, seaFlat, + southX, southY, seaMap, seaFlat, _impactCenter.X, _impactCenter.Y, _impactRadius * ConfigManager.CraterErosionCore, () => (Time.GetTicksMsec()) / 1000.0, p); @@ -911,45 +863,6 @@ public partial class MapGenerator : TextureRect for (int y = 0; y < MapSize; y++) if (_heightMap[x, y] > topAfter) topAfter = _heightMap[x, y]; - // --- Stepped river water (task 23, part 2b): reaches as flat water bodies. - // Levels-not-cells, the existing model: WBID cells + WBTB entries per reach; - // the writer derives WSRF from body levels. Touches no heights, so the - // flood-guard count above stays valid; touches no classify data, so the - // BIOME oracle holds (0_water changes — that IS the river water). - ushort nextBodyId = 1; - foreach (var b in _waterBodies) - if (b.Id >= nextBodyId) nextBodyId = (ushort)(b.Id + 1); - var reaches = RiverCarvePass.AddSteppedWater(_heightMap, MapSize, _waterBodyIds, - nextBodyId, st.Carved, seaMap, seaFlat, - _impactCenter.X, _impactCenter.Y, - _impactRadius * ConfigManager.CraterErosionCore, - ConfigManager.RiverStepDropM, ConfigManager.RiverWaterDepthM, - ConfigManager.RiverFillFraction); - long riverWetPx = 0; - foreach (var reach in reaches) - { - _waterBodies.Add(new WaterBodyInfo - { - Id = reach.Id, - Type = WaterBodyInfo.TYPE_RIVER, - Salinity = WaterBodyInfo.SALINITY_FRESH, - SurfaceLevel = reach.Level, - PixelCount = reach.PixelCount, - Centroid = new Vector2((float)reach.Cx, (float)reach.Cy) - }); - riverWetPx += reach.PixelCount; - } - int mainCarved = 0, tribCarved = 0; - foreach (var cr in st.Carved) { if (cr.Kind == "tributary") tribCarved++; else mainCarved++; } - int tribReaches = 0; long tribWet = 0; - foreach (var reach in reaches) - if (reach.River == "trib") { tribReaches++; tribWet += reach.PixelCount; } - GD.Print($"{T()} [Rivers] water: {reaches.Count} stepped reaches ({tribReaches} on tributaries) " + - $"across {mainCarved} mains + {tribCarved} tributaries, {riverWetPx} wet px " + - $"({tribWet} tributary), step drop {ConfigManager.RiverStepDropM:F2} m, depth {ConfigManager.RiverWaterDepthM:F1} m, " + - $"trib flow threshold {ConfigManager.RiverTribWaterMinFlow} px taper {ConfigManager.RiverTribTaperPx} px, " + - $"fill {ConfigManager.RiverFillFraction:P0} of bed depth (min {ConfigManager.RiverWaterDepthM:F1} m), bank flare {ConfigManager.RiverBankFlare:F1}x (shoulder cut cap {ConfigManager.RiverBankMaxCutM:F1} m)."); - GD.Print($"{T()} [Rivers] v1 '{ConfigManager.RiverRoutingStyle}': plan {st.AnalysisSeconds:F1}s, " + $"routing {st.RoutingSeconds:F1}s, carve {st.CarveSeconds:F1}s " + $"({(Time.GetTicksMsec() - tRiv0) / 1000.0:F1}s total). " + diff --git a/Tools/Scripts/README.md b/Tools/Scripts/README.md index 2ae0acd..53398ab 100644 --- a/Tools/Scripts/README.md +++ b/Tools/Scripts/README.md @@ -109,48 +109,7 @@ Generates the entire 2D blueprint. Roughly in order: sea + `RiverSeaMargin` everywhere (the erosion flood-guard discipline: below-sea cells read-only, zero new below-sea cells, coastline provably fixed; asserted per generation). Slots AFTER towns (town placement reads the render map and must not shift) and BEFORE roads. - Crater core excluded. - - **River water (task 23, C0b finale)** — the payoff. Each carved main river becomes a chain of - **stepped flat water-body REACHES**: a new reach starts every `RiverStepDropM` (2 m) of bed - descent and sits `RiverWaterDepthM` (1.2 m) above its bed, each reach strictly lower than the - one upstream, stepping down to the sea (small vertical drops — riffles — smoothable purely by - the step dial; tilted continuous-slope water is the deferred model B). Reaches are ordinary - water bodies: WBID cells + a WBTB entry (`type 2 = river`, fresh) each; WSRF derives from body - levels exactly as for lakes, and river water renders through the C1/task-13 path with the - task-15 presence rule at its banks. Lake-enders route the last reach into their lake by the - lowground Dijkstra (a pooling terminal IS a local minimum — blind descent dead-ends there); - rivers meet lakes and the sea without repainting them (existing bodies are never overwritten). - Only the lowland reaches are ROUTE-SMOOTHED (RDP+Chaikin, the road pipeline): the upland stems - already thread carved valley floors, and smoothing them off-line cut valley walls (measured). - The BIOME oracle holds (classify untouched; `1_biomes` md5-identical); `0_water` and the - re-captured `0_height` change — that IS the river water and its beds (both snapshots are - re-captured after the river pass so the exported PNGs show them). Max-cut is now reported - CUMULATIVELY vs the pre-pass surface. - - **Polish (task 24)** — tributaries carry water too, gated by along-course flow - (`RiverTribWaterMinFlow`, 0 = water them end to end) and **tapered** at the wet→dry - transition over `RiverTribTaperPx`: the strip narrows and its surface drops to the bed, so a - stream head fades instead of ending in a wall of water. Lake-enders route to the nearest - **significant** water body (`RiverLakeMinTargetPx`, 20k cells) — "nearest wet cell" was - satisfied by a 322-px puddle beside a 197k-px lagoon. Steps are finer (`RiverStepDropM` 0.6 m) - and beds deeper (`RiverDepthScale` 1.5, `RiverWaterDepthM` 2.2 m) so the chain reads as a graded - descent rather than a pond staircase: 1034 reaches, adjacent-reach level gaps median 0.12 m. - - **Polish r2 (task 25)** — tributaries carry water their **full** promoted length - (`RiverTribWaterMinFlow` 0) while still fading at the tip (the taper zone is always - `RiverTribTaperPx` long, starting wherever water begins). The water surface is - `RiverFillFraction` (0.80) of the **local** bed depth rather than a fixed height, floored by - `RiverWaterDepthM` — a fixed height was a trickle in a 13 m mouth and over the rim at a 1.5 m - head. Bank shoulders flare `RiverBankFlare` (3.2) half-widths with a smootherstep so water meets - land as a shore, and the **shoulder** may not lower a cell more than `RiverBankMaxCutM` (3 m) - **measured against the pre-pass surface** — gentle ground flares, a ridge crossing keeps steep - walls, and max cut fell to 16.8 m with no cell past 20 m. - - **FILE SAFETY** — `ISLA_BLUEPRINT_PATH` (load a specific blueprint file) and `ISLA_EXPORT_DIR` - (write all of a run's blueprints and snapshots into a task folder) exist so tooling never writes - to, or deletes from, the runtime root — the developer stages `MapData_Seed_*.dat` there to view - worlds in 3D. Batch drivers set both and contain no delete of any kind. + Crater core excluded. **NO WATER yet** — part 2b waters the gated routing style. 3. **Sea level and water** — sea level per the configured model (`SeaLevelModel`: `"flat"` scalar — the default, `SeaLevelValue` 0.15 — or the legacy `"field"` latitude Lerp); flood fill diff --git a/Tools/Scripts/RiverCarvePass.cs b/Tools/Scripts/RiverCarvePass.cs index 60f5f10..a5d803b 100644 --- a/Tools/Scripts/RiverCarvePass.cs +++ b/Tools/Scripts/RiverCarvePass.cs @@ -66,30 +66,6 @@ public static class RiverCarvePass public float WidthScale = 1.0f; public float DepthScale = 1.0f; public float SeaMarginM = 0.2f; // bed floor above sea, everywhere - // Task 24: a tributary reach is WET where its along-course flow exceeds this; - // upstream of that it TAPERS to dry over TribTaperPx rather than ending in a - // wall of water. 0 = water tributaries end to end. - public int TribWaterMinFlowPx = 0; // task 25: 0 = tributaries wet full length - public int TribTaperPx = 120; - // Task 25 (the gate's "trickle at the bottom of a ditch" note): the reach's - // water surface is a FRACTION of the local bed depth rather than a fixed - // height — the channel reads FILLED at every scale and cannot overfill onto - // the plain, which a fixed depth does at the shallow heads. WaterDepthM - // survives as the absolute minimum so tiny channels still hold water. - public float FillFraction = 0.80f; - // Bank shoulders flare this many half-widths beyond the channel (was a hard - // 2×) with a gentler-than-smoothstep curve, so water meets land as a shore. - public float BankFlare = 3.2f; - // ...but the SHOULDER may never lower a cell by more than this. A gentle flare - // across a ridge would otherwise cut a big notch: widening the flare alone took - // cells deeper than 20 m from 339 to 1917 (measured). With the cap, gentle - // ground still flares into a shore — which is where the "water in a groove" - // complaint lives — while a ridge crossing keeps steep walls, which is what a - // gorge actually looks like. The channel bed itself is not affected by this. - public float BankMaxCutM = 3.0f; - // Lake-enders route to the nearest water body of at least this size — the - // nearest wet PIXEL was a puddle (task-23 gate finding). - public int LakeMinTargetPx = 20_000; public DrainageAnalysis.Params PlanParams = new(); } @@ -108,43 +84,17 @@ public static class RiverCarvePass public double VolumeM3; } - /// The carved geometry the water stage consumes (main rivers only). - public class CarvedRiver - { - public string Name, Kind; - public bool Southern; - public long DrainagePx; - public List<(float x, float y)> Dense; // head → mouth, ~1-px samples - public float[] Bed; // raw units, monotone non-increasing - public float[] HalfW; // px - public float[] DepthM; // local bed depth, metres (task 25 fill) - public bool ReachedWaterTerminal; // lake-enders: extension reached classify water - // Task 24: along-course flow (px of drainage) per sample, and the first index - // that carries water. Between WetFrom-TaperPx and WetFrom the water tapers - // (narrowing and shallowing to the bed) so a stream head fades out. - public float[] Flow; - public int WetFrom; - public int TaperPx; - } - public class Stats { public List Rivers = new(); - public List Carved = new(); // for the stepped-water stage (task 23) - internal float[] PrePass; // cumulative-cut baseline public long CarvedCells; public double CarvedVolumeM3; - public float MaxCutM; // CUMULATIVE vs pre-pass heights (task-22 nit 1 fixed) + public float MaxCutM; public double AnalysisSeconds, RoutingSeconds, CarveSeconds; } - /// Row-major mask of classify water belonging to - /// bodies of at least LakeMinTargetPx cells (task 24). Lake-enders route to THIS, - /// not to any wet pixel: the task-23 build routed one into a puddle a few hundred - /// px short of the obvious lagoon, because "nearest classify water" is satisfied - /// by a 3-cell pond. public static Stats Apply(float[,] height, int mapSize, bool[] isOcean, - bool[] isClassifyWater, bool[] isSignificantWater, float southX, float southY, + bool[] isClassifyWater, float southX, float southY, float[,] seaMap, float seaFlat, float craterCx, float craterCy, float craterCoreRadius, Func secondsNow, Params p) @@ -172,14 +122,6 @@ public static class RiverCarvePass // --- carve --- t0 = secondsNow(); - // Pre-pass snapshot: max-cut is measured CUMULATIVELY against the heights - // this pass found, not per-write — overlapping stamps re-cut a cell and the - // per-write number understated the true deepest cut ~4× (task-22 nit 1). - float[] pre = new float[n * n]; - for (int x = 0; x < n; x++) - for (int y = 0; y < n; y++) - pre[x * n + y] = height[x, y]; - stats.PrePass = pre; int riverIdx = 0; foreach (var t in plan.Trunks) { @@ -198,31 +140,10 @@ public static class RiverCarvePass var route = giantRoutes[gi]; gi++; var course = new List<(float x, float y)>(g.Course); course.Reverse(); // head → terminal - - // Lake-enders (task 23): the stem pools on dry ground short of its lake - // BECAUSE its pooling point is a local minimum — a blind descent walk - // dead-ends there immediately (measured: 0 steps). Route to the nearest - // classify-water cell with the same lowground Dijkstra the routed giants - // use, so the bed (and then the water) actually joins the lake. - bool reachedLake = false; - if (g.Kind == "lake-ender") - { - // Target SIGNIFICANT water (task 24). Fall back to any classify water - // only if no significant body is reachable, so a seed whose lake-ender - // genuinely has only small ponds still connects rather than dead-ending. - var ext = RouteToOcean(height, n, isSignificantWater, - (int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt); - if (ext.Count == 0) - ext = RouteToOcean(height, n, isClassifyWater, - (int)g.Terminal.x, (int)g.Terminal.y, STYLE_LOWGROUND, SeaAt); - if (ext.Count > 0) { route = ext; reachedLake = true; } - } - var rs = CarveRiver($"giant{gi}", g.Kind, g.DrainageAreaPx, course, route, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats); rs.SouthernCandidate = g.SouthernCandidate; - rs.ReachedOcean = g.Kind == "routed" ? (route != null && route.Count > 0) : reachedLake; - if (stats.Carved.Count > 0) stats.Carved[^1].ReachedWaterTerminal = reachedLake; + rs.ReachedOcean = g.Kind != "routed" || (route != null && route.Count > 0); foreach (var trib in g.Tributaries) CarveTributary(trib, height, n, SeaAt, coreSq, craterCx, craterCy, p, stats); } @@ -230,61 +151,13 @@ public static class RiverCarvePass return stats; } - // ---- Route smoothing (task 23): the road pass's AAA pipeline, numerically ---- - // RDP(4.0) decimation + 4 Chaikin corner-cutting passes, endpoints pinned — - // the same constants and structure as MapGenerator.SmoothPath, mirrored here - // because this pass is Godot-free. Kills the 8-connected Dijkstra 45° kinks; - // the bed then carves along the smoothed centreline. - private static List<(float x, float y)> SmoothCourse(List<(float x, float y)> raw) - { - if (raw.Count < 3) return raw; - var dec = Rdp(raw, 0, raw.Count - 1, 4.0f); - if (dec.Count < 3) return raw; - var sm = dec; - for (int pass = 0; pass < 4; pass++) - { - var nxt = new List<(float x, float y)>(sm.Count * 2) { sm[0] }; - for (int i = 0; i + 1 < sm.Count; i++) - { - var a = sm[i]; var b = sm[i + 1]; - nxt.Add((a.x * 0.75f + b.x * 0.25f, a.y * 0.75f + b.y * 0.25f)); - nxt.Add((a.x * 0.25f + b.x * 0.75f, a.y * 0.25f + b.y * 0.75f)); - } - nxt.Add(sm[^1]); - sm = nxt; - } - return sm; - } - - private static List<(float x, float y)> Rdp(List<(float x, float y)> pts, int i0, int i1, float tol) - { - if (i1 - i0 <= 1) return new List<(float x, float y)> { pts[i0], pts[i1] }; - var a = pts[i0]; var b = pts[i1]; - float abx = b.x - a.x, aby = b.y - a.y; - float abLen = MathF.Sqrt(abx * abx + aby * aby); - float maxD = 0f; int maxI = i0; - for (int i = i0 + 1; i < i1; i++) - { - float d = abLen < 1e-6f - ? MathF.Sqrt((pts[i].x - a.x) * (pts[i].x - a.x) + (pts[i].y - a.y) * (pts[i].y - a.y)) - : MathF.Abs(abx * (a.y - pts[i].y) - (a.x - pts[i].x) * aby) / abLen; - if (d > maxD) { maxD = d; maxI = i; } - } - if (maxD <= tol) return new List<(float x, float y)> { pts[i0], pts[i1] }; - var left = Rdp(pts, i0, maxI, tol); - var right = Rdp(pts, maxI, i1, tol); - left.RemoveAt(left.Count - 1); - left.AddRange(right); - return left; - } - /// /// Deterministic Dijkstra from the start cell to the nearest ocean cell under /// the selected style's cost model. Returns the path start → ocean (1-px steps), /// or an empty list if no path exists (reported upstream, never asserted away). /// private static List<(float x, float y)> RouteToOcean(float[,] height, int n, - bool[] targets, int sx, int sy, byte style, Func seaAt) + bool[] isOcean, int sx, int sy, byte style, Func seaAt) { int total = n * n; var gcost = new float[total]; @@ -306,7 +179,7 @@ public static class RiverCarvePass int c = pq.Dequeue(); if (closed[c]) continue; closed[c] = true; - if (targets[c]) { goal = c; break; } + if (isOcean[c]) { goal = c; break; } int cx = c / n, cy = c % n; float hc = height[cx, cy]; for (int k = 0; k < 8; k++) @@ -346,10 +219,7 @@ public static class RiverCarvePass { var course = new List<(float x, float y)>(trib.Course); course.Reverse(); // head → confluence - // Task 24: tributaries are registered as carved rivers (name "trib") so the - // water stage sees them — they carved but stayed dry in task 23. They are NOT - // added to stats.Rivers, so the per-river console table stays the 6 mains. - CarveRiver("trib", "tributary", trib.DrainageAreaPx, course, null, + CarveRiver(null, "tributary", trib.DrainageAreaPx, course, null, height, n, seaAt, coreSq, craterCx, craterCy, p, stats); } @@ -364,16 +234,9 @@ public static class RiverCarvePass float coreSq, float craterCx, float craterCy, Params p, Stats stats) { // Full head→mouth polyline: upland stem, then the lowland reach if any. - // ONLY the lowland reach is smoothed: the Dijkstra 45° kinks live there, on - // near-flat ground where a rounded corner costs nothing. The upland stems - // already thread the carved valley FLOORS — smoothing them off-line cut - // valley walls (measured: max cut 14.6 → 27.3 m before this was split). var pts = new List<(float x, float y)>(upland); if (lowlandRoute != null && lowlandRoute.Count > 1) - { - var smoothedRoute = SmoothCourse(lowlandRoute); - pts.AddRange(smoothedRoute.GetRange(1, smoothedRoute.Count - 1)); - } + pts.AddRange(lowlandRoute.GetRange(1, lowlandRoute.Count - 1)); // Densify to ~1-px samples (plan courses are decimated ×4). var dense = new List<(float x, float y)>(); @@ -446,7 +309,7 @@ public static class RiverCarvePass for (int i = 0; i < m; i++) { float hw = halfW[i]; - float outer = hw * p.BankFlare; + float outer = hw * 2f; int cx0 = (int)MathF.Floor(dense[i].x - outer), cx1 = (int)MathF.Ceiling(dense[i].x + outer); int cy0 = (int)MathF.Floor(dense[i].y - outer), cy1 = (int)MathF.Ceiling(dense[i].y + outer); float rimH = bed[i] + depth[i] / M_PER_UNIT; @@ -472,188 +335,27 @@ public static class RiverCarvePass } else { - // Shoulder: 0 at the rim → 1 at natural ground, over a flare of - // (BankFlare-1) half-widths. Smootherstep (6t⁵−15t⁴+10t³) leaves - // the rim nearly tangent to the water plane, so the bank reads as - // a shore rather than the wall a plain smoothstep left. - float f = (r - hw) / MathF.Max(1e-3f, hw * (p.BankFlare - 1f)); - if (f > 1f) f = 1f; - f = f * f * f * (f * (6f * f - 15f) + 10f); + float f = (r - hw) / hw; // 0..1 across the shoulder + f = f * f * (3f - 2f * f); // smoothstep target = rimH + (old - rimH) * f; - // Clamp against the PRE-PASS surface, not the current height: - // overlapping stamps re-visit a cell, so a per-write cap lets each - // pass take another BankMaxCut (measured: capping against `old` - // changed the carved volume by 1 m³ out of 1.29 M — i.e. nothing). - float shoulderFloor = stats.PrePass[x * n + y] - p.BankMaxCutM / M_PER_UNIT; - if (target < shoulderFloor) target = shoulderFloor; } float floor = sea + p.SeaMarginM / M_PER_UNIT; if (target < floor) target = floor; if (target < old) { float cutM = (old - target) * M_PER_UNIT; - // Cumulative depth vs the PRE-PASS surface (nit 1): the - // honest "how deep did we cut here in total" number. - float cumM = (stats.PrePass[x * n + y] - target) * M_PER_UNIT; height[x, y] = target; stats.CarvedCells++; stats.CarvedVolumeM3 += cutM; - if (cumM > stats.MaxCutM) stats.MaxCutM = cumM; - if (cumM > rs.MaxCutM) rs.MaxCutM = cumM; + if (cutM > stats.MaxCutM) stats.MaxCutM = cutM; + if (cutM > rs.MaxCutM) rs.MaxCutM = cutM; rs.VolumeM3 += cutM; } } } } - if (name != null) - { - if (kind != "tributary") stats.Rivers.Add(rs); - // Along-course flow: drainage grows from a headwater trickle to the full - // figure at the mouth. Quadratic in t so the substantial lower half - // dominates — a first-order stand-in for real accumulation, which the - // plan only records per-river. - var flow = new float[m]; - for (int i = 0; i < m; i++) - { - float t = m > 1 ? (float)i / (m - 1) : 1f; - flow[i] = drainagePx * (0.05f + 0.95f * t * t); - } - int wetFrom = 0; - if (kind == "tributary" && p.TribWaterMinFlowPx > 0) - { - wetFrom = m; // dry unless the threshold is met - for (int i = 0; i < m; i++) - if (flow[i] >= p.TribWaterMinFlowPx) { wetFrom = i; break; } - } - stats.Carved.Add(new CarvedRiver - { - Name = name, Kind = kind, DrainagePx = drainagePx, - Dense = dense, Bed = bed, HalfW = halfW, DepthM = depth, - Flow = flow, WetFrom = wetFrom, TaperPx = p.TribTaperPx - }); - } + if (name != null) stats.Rivers.Add(rs); return rs; } - - /// - /// The stepped-water builder (task 23, part 2b): segments each carved main - /// river into REACHES — flat water bodies stepping down the bed toward the - /// outlet — and stamps their ids into the WBID grid. Reuses the existing - /// levels-not-cells water model exactly: one body per reach, one flat level - /// each; the writer derives WSRF from body levels as it always has. The step - /// drops are the smoothing dial (smaller drop = more, finer steps); tilted - /// water is the deferred model B and is NOT built here. - /// - /// Emission is plain data (no engine types): the caller turns reaches into - /// WBTB entries. Wet cells: inside the channel half-width, currently dry in - /// WBID, at/above sea (below-sea cells belong to the ocean/crater-seam rule), - /// bed below the reach level. Existing water bodies are never overwritten — - /// a river MEETS a lake or the sea, it does not repaint them. - /// - public class Reach - { - public ushort Id; - public string River; - public float Level; // raw units - public int PixelCount; - public double Cx, Cy; // centroid accumulators → mean - } - - public static List AddSteppedWater(float[,] height, int mapSize, - ushort[,] wbid, ushort firstId, List rivers, - float[,] seaMap, float seaFlat, float craterCx, float craterCy, - float craterCoreRadius, float stepDropM, float waterDepthM, float fillFraction) - { - int n = mapSize; - float SeaAt(int x, int y) => seaMap != null ? seaMap[x, y] : seaFlat; - float coreSq = craterCoreRadius * craterCoreRadius; - var reaches = new List(); - ushort nextId = firstId; - - foreach (var r in rivers) - { - int m = r.Dense.Count; - if (m < 2) continue; - // Task 24: start at the wet-from index (tributary threshold; 0 for mains), - // but back up by the taper length so the transition is a FADE, not a wall. - int i = Math.Max(0, r.WetFrom - r.TaperPx); - if (i >= m) continue; // entirely below threshold: dry - // The fade zone is always TaperPx long starting at i. With a threshold it - // spans (WetFrom-Taper → WetFrom); with tributaries fully watered - // (WetFrom = 0, task 25) it spans the first TaperPx from the HEAD, so a - // full-length tributary still fades in at its tip instead of starting as a - // wall of water. - int taperStart = i, taperEnd = Math.Min(m - 1, i + r.TaperPx); - float lastLevel = float.MaxValue; - while (i < m) - { - // Reach spans from i while the bed stays within stepDropM of the - // reach's starting bed; its flat level sits waterDepthM above that - // start (deepening toward the next step — the pool behind a riffle). - float startBed = r.Bed[i]; - // Fill the channel: the surface sits at FillFraction of the LOCAL bed - // depth, never below the absolute minimum. A fixed height made deep - // mouths a trickle and overtopped shallow heads; a fraction is right at - // both ends and cannot spill onto the plain. - float localDepthM = r.DepthM != null ? r.DepthM[i] : waterDepthM; - float fillM = MathF.Max(waterDepthM, localDepthM * fillFraction); - if (fillM > localDepthM) fillM = localDepthM; // never above the rim - float level = startBed + fillM / M_PER_UNIT; - if (level >= lastLevel) // enforce strict descent - level = lastLevel - 0.01f / M_PER_UNIT; - int j = i; - while (j < m && r.Bed[j] > startBed - stepDropM / M_PER_UNIT) j++; - - var reach = new Reach { Id = nextId, River = r.Name, Level = level }; - for (int k2 = i; k2 < j; k2++) - { - // Taper: 0 at the dry end of the fade zone → 1 at full water. The - // water narrows AND its surface drops to the bed, so a stream head - // thins out and disappears instead of ending in a flat wall. - float taper = 1f; - if (taperEnd > taperStart && k2 < taperEnd) - taper = (float)(k2 - taperStart) / (taperEnd - taperStart); - if (taper <= 0.02f) continue; - float hw = r.HalfW[k2] * (0.25f + 0.75f * taper); - int x0 = (int)MathF.Floor(r.Dense[k2].x - hw), x1 = (int)MathF.Ceiling(r.Dense[k2].x + hw); - int y0 = (int)MathF.Floor(r.Dense[k2].y - hw), y1 = (int)MathF.Ceiling(r.Dense[k2].y + hw); - for (int x = x0; x <= x1; x++) - { - if (x < 0 || x >= n) continue; - for (int y = y0; y <= y1; y++) - { - if (y < 0 || y >= n) continue; - if (wbid[x, y] != 0) continue; // never repaint existing water - float rx = x - r.Dense[k2].x, ry = y - r.Dense[k2].y; - if (rx * rx + ry * ry > hw * hw) continue; - float ddx = x - craterCx, ddy = y - craterCy; - if (ddx * ddx + ddy * ddy < coreSq) continue; - float h = height[x, y]; - float sea = SeaAt(x, y); - if (h < sea) continue; // ocean/seam territory - // The tapered surface sits between the bed and the reach - // level, so the wet strip shallows as it narrows. - float localLevel = taper >= 1f - ? level - : r.Bed[k2] + (level - r.Bed[k2]) * taper; - if (h >= localLevel) continue; // bank above the water line - wbid[x, y] = nextId; - reach.PixelCount++; - reach.Cx += x; reach.Cy += y; - } - } - } - if (reach.PixelCount > 0) - { - reach.Cx /= reach.PixelCount; reach.Cy /= reach.PixelCount; - reaches.Add(reach); - nextId++; - lastLevel = level; - } - i = j; - } - } - return reaches; - } } diff --git a/Tools/Scripts/RiverCarvePass.cs.uid b/Tools/Scripts/RiverCarvePass.cs.uid deleted file mode 100644 index 728bfbb..0000000 --- a/Tools/Scripts/RiverCarvePass.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dufsppmyn4g8v