diff --git a/Core/README.md b/Core/README.md
index 7654e74..8e5f52d 100644
--- a/Core/README.md
+++ b/Core/README.md
@@ -17,9 +17,7 @@ dispatches on the file's first byte:
- **v2 (primary)** — raw `ISLA` magic, u32 version gate, then tagged sections
`[u32 tag][u64 length][payload]`. Unknown tags are skipped by length, so future sections are
invisible to older readers. Validated on read: version, MapSize bounds, section lengths against
- the file, biome/tier ordinal ranges. Since the water-bodies stage, v2 files also carry the
- optional `WBID`/`WBTB`/`WSRF` water sections (per-pixel body ids, the body table, quantized
- surface levels) — parsed into `WorldBlueprint` and consumed by nothing at runtime yet.
+ the file, biome/tier ordinal ranges.
- **v1 (legacy)** — the positional `"ISLA_V1"` format, still written beside v2 as `_v1.dat` and
still loadable (with a deprecation warning) until a future removal task.
diff --git a/Core/Scripts/BLUEPRINT_FORMAT.md b/Core/Scripts/BLUEPRINT_FORMAT.md
index 87eb253..b857c3f 100644
--- a/Core/Scripts/BLUEPRINT_FORMAT.md
+++ b/Core/Scripts/BLUEPRINT_FORMAT.md
@@ -55,11 +55,7 @@ Every section: `[u32 tag][u64 payload-length in bytes][payload]`.
| `HGTS` / `0x53544748` | `MapSize²` × `f32` height, **X outer / Y inner** | Mandatory. The second index is the map's north/south axis; the server consumes it as world **Z**. (This axis convention was never written down for v1 — it is now normative.) Length must equal `4·MapSize²`. |
| `BIOM` / `0x4D4F4942` | `MapSize²` × `u8` biome ordinal, same pixel order | Mandatory. Ordinals from `Enums.cs::Biome` — **append-only, never reorder** (the ordinal IS the wire value). Writer refuses ordinals > 255; reader rejects ordinals ≥ the known biome count (parse-time validation; the palette's runtime Dirt fallback for in-memory values is unchanged). Length must equal `MapSize²`. |
| `TOWN` / `0x4E574F54` | `i32 count`, then per town: `f32 X` · `f32 Y` · `u8 tier` · `u8 isHighwayNode` (0/1) | Tier ordinals from `Enums.cs::TownTier`, append-only, range-checked on read. The highway-node flag is what the generator's road topology was built from (v1 dropped it); carried and exposed on the parsed blueprint, consumed by nothing server-side yet. |
-| `TCRV` / `0x56524354` | 50 B: `u16 curveVersion` · input knots `f32 t1..t4` · `f32 spikeMax` · output bands `f32 sea` · `f32 orangeCeil` · `f32 redCeil` · `f32 plateauLo` · `f32 plateauHi` · `f32 peakCap` · `f32 tailSlope` | Optional — present iff the height-redistribution curve shaped this blueprint's `HGTS` (config `TerrainCurve: "v2"`); absent = raw legacy profile. **`spikeMax` semantics by version:** v1 = the pooled calibration max (identical every seed); **v2 = the SEED'S own effective raw pre-curve maximum** — the per-seed spike normalizer, which is why blueprints are no longer reproducible from curve constants alone and the value is recorded here. **Metadata only:** exported heights are already curved; nothing re-applies the map. The curve lives in `Tools/Scripts/HeightCurve.cs` (calibration provenance in its header + the task-05/06 reports). |
-| `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) · `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. |
+| `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. |
| `RDBR` / `0x52424452` | same layout | Branch tier. |
| `RDRG` / `0x47524452` | same layout | Rugged tier. |
| `RDTL` / `0x4C544452` | same layout | Trail tier. |
@@ -89,38 +85,9 @@ the file, not the original generation.
### Size
-`total = 8 (header) + Σ per section (12 + payload)`. The pixel grids dominate: without water
-sections `5·MapSize²` bytes (4 height + 1 biome) ≈ **320 MiB at 8K**, vs v1's `8·MapSize²` ≈
-512 MiB — the u8 biome section saves ~192 MiB. With the water sections the per-pixel cost is
-`9·MapSize²` (adds 2 B `WBID` + 2 B `WSRF`) ≈ **576 MiB at 8K** plus a trivial `WBTB` (20 B per
-body).
-
-### Config knobs that shape blueprint content (not part of the byte format)
-
-- **`SeaLevelModel`** (`"flat"` | `"field"`) + **`SeaLevelValue`** (float; used when flat) —
- D-033: selects what `MapGenerator.GetSeaLevel` returns. `"flat"` (the default, value 0.15) is
- one scalar sea level everywhere; `"field"` is the legacy latitude Lerp (0.26 north → 0.15
- south). Generator-only — all nine `GetSeaLevel` call sites inherit the model; nothing at
- runtime computes sea level. The model in force shapes the coastline, masks, biomes, water
- bodies, and `WBTB` levels of the blueprint being generated; the file itself does not record
- which model produced it (the `WBTB` levels are the observable).
-- **`SkipRoads`** (bool, default false) — exports with present-but-empty road sections (see the
- road-section table note).
-- **`TerrainCurve`** (`"v2"` | `"off"`, default `"v2"`; `"v1"` retired by the task-06
- recalibration and rejected loudly) — whether the height-redistribution curve (storm ladder:
- 75 % orange coverage, 50 m plateau, **per-seed-normalized 420 m peaks**) shapes `HGTS`.
- Biome/water classification is curve-invariant by construction (it classifies the retained
- uncurved heights); town positions and everything 3D follow the curved terrain. When on,
- `TCRV` records the effective parameters including the per-seed `spikeMax`.
-
-### Deliberately NOT a section: basins (`BSIN`)
-
-The priority-flood pit-fill that runs in the water-bodies stage is **diagnostics only** and
-serializes nothing. Rationale, on the record so nobody adds a basin section later without meeting
-the argument: basin geometry is a pure function of the heightmap, and it goes stale the moment the
-coast-smoothing change set (or any terrain change) touches heights — a serialized `BSIN` would be
-a cache with a silent invalidation hazard. The rivers stage recomputes basins fresh at generation
-time (measured ~12 s at 8K — cheap enough to never cache).
+`total = 8 (header) + Σ per section (12 + payload)`. The pixel grid dominates: `5·MapSize²` bytes
+(4 height + 1 biome) ≈ **320 MiB at 8K**, vs v1's `8·MapSize²` ≈ 512 MiB — the u8 biome section
+saves ~192 MiB at 8K.
### Adding a new section (the intended extension path)
diff --git a/Core/Scripts/BlueprintFormat.cs b/Core/Scripts/BlueprintFormat.cs
index 99d4597..153e166 100644
--- a/Core/Scripts/BlueprintFormat.cs
+++ b/Core/Scripts/BlueprintFormat.cs
@@ -29,27 +29,6 @@ namespace IslaApocalypse.Core
public const uint TAG_ROADS_BRANCH = 0x52424452; // "RDBR"
public const uint TAG_ROADS_RUGGED = 0x47524452; // "RDRG"
public const uint TAG_ROADS_TRAIL = 0x4C544452; // "RDTL"
- public const uint TAG_WATER_BODY_IDS = 0x44494257; // "WBID"
- public const uint TAG_WATER_BODY_TABLE = 0x42544257; // "WBTB"
- public const uint TAG_WATER_SURFACE = 0x46525357; // "WSRF"
- public const uint TAG_TERRAIN_CURVE = 0x56524354; // "TCRV"
-
- // WSRF quantization: u16, 0 reserved as the no-water sentinel. A real level L
- // (raw blueprint height units) encodes as 1 + round(L × 32768), so a genuine
- // level can never encode to 0. Decodes back via (q − 1) / 32768. Covers
- // [0 .. ~1.99997] raw at 1/32768 raw resolution ≈ 7.7 mm of world height
- // (1 raw unit = 251 m) — far below the 1 m voxel.
- public const float WSRF_SCALE = 1f / 32768f;
-
- public static ushort EncodeWaterLevel(float level)
- {
- return (ushort)Godot.Mathf.Clamp(1 + Godot.Mathf.RoundToInt(level * 32768f), 1, ushort.MaxValue);
- }
-
- public static float DecodeWaterLevel(ushort quantized)
- {
- return (quantized - 1) * WSRF_SCALE;
- }
// MapSize sanity bounds, checked before any allocation on read.
public const int MIN_MAP_SIZE = 256;
diff --git a/Core/Scripts/BlueprintWriter.cs b/Core/Scripts/BlueprintWriter.cs
index 56bc07c..d904590 100644
--- a/Core/Scripts/BlueprintWriter.cs
+++ b/Core/Scripts/BlueprintWriter.cs
@@ -31,20 +31,8 @@ namespace IslaApocalypse.Core
writer.Write(BlueprintFormat.VERSION);
WriteSection(writer, BlueprintFormat.TAG_PARAMS, w => WriteParams(w, bp));
- if (bp.TerrainCurve != null)
- WriteSection(writer, BlueprintFormat.TAG_TERRAIN_CURVE, w => WriteTerrainCurve(w, bp.TerrainCurve));
WriteSection(writer, BlueprintFormat.TAG_HEIGHTS, w => WriteHeights(w, bp));
WriteSection(writer, BlueprintFormat.TAG_BIOMES, w => WriteBiomes(w, bp));
-
- // Water sections are written only when the blueprint carries water data
- // (a legacy v1 re-encode has none — the sections are simply absent).
- if (bp.WaterBodyIds != null)
- {
- WriteSection(writer, BlueprintFormat.TAG_WATER_BODY_IDS, w => WriteWaterBodyIds(w, bp));
- WriteSection(writer, BlueprintFormat.TAG_WATER_BODY_TABLE, w => WriteWaterBodyTable(w, bp));
- WriteSection(writer, BlueprintFormat.TAG_WATER_SURFACE, w => WriteWaterSurface(w, bp));
- }
-
WriteSection(writer, BlueprintFormat.TAG_TOWNS, w => WriteTowns(w, bp));
WriteSection(writer, BlueprintFormat.TAG_ROADS_HIGHWAY, w => WriteRoadTier(w, bp.Highways));
WriteSection(writer, BlueprintFormat.TAG_ROADS_BRANCH, w => WriteRoadTier(w, bp.BranchRoads));
@@ -124,71 +112,6 @@ namespace IslaApocalypse.Core
}
}
- private static void WriteTerrainCurve(BinaryWriter writer, TerrainCurveInfo c)
- {
- writer.Write(c.Version);
- writer.Write(c.T1); writer.Write(c.T2); writer.Write(c.T3); writer.Write(c.T4);
- writer.Write(c.SpikeMax);
- writer.Write(c.Sea); writer.Write(c.OrangeCeil); writer.Write(c.RedCeil);
- writer.Write(c.PlateauLo); writer.Write(c.PlateauHi); writer.Write(c.PeakCap);
- writer.Write(c.TailSlope);
- }
-
- private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
- {
- int n = bp.MapSize;
- for (int x = 0; x < n; x++)
- for (int y = 0; y < n; y++)
- writer.Write(bp.WaterBodyIds[x, y]);
- }
-
- private static void WriteWaterBodyTable(BinaryWriter writer, WorldBlueprint bp)
- {
- writer.Write(bp.WaterBodies.Count);
- foreach (WaterBodyInfo body in bp.WaterBodies)
- {
- writer.Write(body.Id);
- writer.Write(body.Type);
- writer.Write(body.Salinity);
- writer.Write(body.SurfaceLevel);
- writer.Write(body.PixelCount);
- writer.Write(body.Centroid.X);
- writer.Write(body.Centroid.Y);
- }
- }
-
- private static void WriteWaterSurface(BinaryWriter writer, WorldBlueprint bp)
- {
- int n = bp.MapSize;
-
- // A parsed blueprint carries the quantized surface verbatim; a freshly
- // generated one derives it from body ids + per-body levels.
- if (bp.WaterSurfaceQ != null)
- {
- for (int x = 0; x < n; x++)
- for (int y = 0; y < n; y++)
- writer.Write(bp.WaterSurfaceQ[x, y]);
- return;
- }
-
- // id -> encoded level lookup (ids are small and dense: 1..N)
- int maxId = 0;
- foreach (WaterBodyInfo body in bp.WaterBodies)
- if (body.Id > maxId) maxId = body.Id;
- ushort[] encoded = new ushort[maxId + 1];
- foreach (WaterBodyInfo body in bp.WaterBodies)
- encoded[body.Id] = BlueprintFormat.EncodeWaterLevel(body.SurfaceLevel);
-
- for (int x = 0; x < n; x++)
- {
- for (int y = 0; y < n; y++)
- {
- ushort id = bp.WaterBodyIds[x, y];
- writer.Write(id == 0 ? (ushort)0 : encoded[id]);
- }
- }
- }
-
private static void WriteTowns(BinaryWriter writer, WorldBlueprint bp)
{
writer.Write(bp.Towns.Count);
diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs
index a28342c..a9de115 100644
--- a/Core/Scripts/ConfigManager.cs
+++ b/Core/Scripts/ConfigManager.cs
@@ -14,25 +14,6 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
public static float DensityMultiplier = 1.0f; // <-- Replaces TownCount
public static int ChunkRadius = 24; // Default chunk size, can be overridden by config
- // Iteration toggle: skip the ~25-min A* road pass entirely. The blueprint is
- // still exported (road sections present but empty) — an iteration artifact,
- // not a shippable world. Default false.
- public static bool SkipRoads = false;
-
- // Sea-level model (D-033, terrain-water task 04): "flat" = one scalar sea
- // level everywhere (SeaLevelValue); "field" = the legacy latitude Lerp
- // (0.26 north .. 0.15 south). Generator-only — the runtime never computes
- // sea level. Default: flat 0.15.
- public static string SeaLevelModel = "flat";
- public static float SeaLevelValue = 0.15f;
-
- // Height-redistribution curve (tasks 05/06, graduation M-7): "v2" applies the
- // calibrated storm-ladder curve with the per-seed peak spike (HeightCurve.cs);
- // "off" is the raw legacy profile. "v1" was dropped with the v2 recalibration
- // (task-05 blueprints are regenerable). Biome classification is curve-invariant
- // by construction either way. Default: v2.
- public static string TerrainCurve = "v2";
-
public static void LoadConfig()
{
string path = "res://ServerConfig.json";
@@ -80,39 +61,7 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
// Extract the ChunkRadius
if (data.ContainsKey("ChunkRadius")) {
ChunkRadius = (int)data["ChunkRadius"];
- }
-
- // Extract the SkipRoads iteration toggle
- if (data.ContainsKey("SkipRoads"))
- {
- SkipRoads = (bool)data["SkipRoads"];
- }
-
- // Extract the sea-level model
- if (data.ContainsKey("SeaLevelModel"))
- {
- string model = (string)data["SeaLevelModel"];
- if (model == "flat" || model == "field")
- SeaLevelModel = model;
- else
- GD.PrintErr($"[ConfigManager] Unknown SeaLevelModel '{model}'. Keeping '{SeaLevelModel}'.");
- }
- if (data.ContainsKey("SeaLevelValue"))
- {
- SeaLevelValue = (float)data["SeaLevelValue"];
- }
-
- // Extract the terrain-curve gate
- if (data.ContainsKey("TerrainCurve"))
- {
- string curve = (string)data["TerrainCurve"];
- if (curve == "off" || curve == "v2")
- TerrainCurve = curve;
- else if (curve == "v1")
- GD.PrintErr($"[ConfigManager] TerrainCurve 'v1' was retired by the v2 recalibration (task 06). Keeping '{TerrainCurve}' — use \"v2\" or \"off\".");
- else
- GD.PrintErr($"[ConfigManager] Unknown TerrainCurve '{curve}'. Keeping '{TerrainCurve}'.");
- }
+ }
switch (profile)
{
diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs
index c40ec40..1a084fe 100644
--- a/Core/Scripts/MapDataParser.cs
+++ b/Core/Scripts/MapDataParser.cs
@@ -36,46 +36,6 @@ namespace IslaApocalypse.Core
public string GeneratorGitHash = ""; // short hash of the generator repo, "" if unknown
}
- ///
- /// One water body from the blueprint's WBTB section: the ocean (exactly one, id 1)
- /// or a lake (ids 2..N). SurfaceLevel is the documented TRANSITIONAL rule — one flat
- /// level per body, GetSeaLevel at the body's pixel centroid under the still-live
- /// latitude field; superseded when the flat-scalar sea model lands. Salinity is a
- /// provisional placeholder for the future fresh/salt mechanic (ocean salt, lake fresh).
- ///
- public class WaterBodyInfo
- {
- public const byte TYPE_OCEAN = 0;
- public const byte TYPE_LAKE = 1;
- public const byte SALINITY_FRESH = 0;
- public const byte SALINITY_SALT = 1;
-
- public ushort Id;
- public byte Type;
- public byte Salinity;
- public float SurfaceLevel; // raw blueprint height units
- public int PixelCount;
- public Vector2 Centroid; // map pixels
- }
-
- ///
- /// The height-redistribution curve that shaped this blueprint's heights (v2 TCRV
- /// section, terrain-water task 05). Null when the blueprint was generated with the
- /// curve off (or predates it) — heights are then the raw legacy profile. Pure
- /// metadata: the server never re-applies the curve; exported heights are already
- /// curved.
- ///
- public class TerrainCurveInfo
- {
- public ushort Version;
- // Input knots. SpikeMax is the spike domain's top: under curve v1 it was the
- // pooled calibration max (identical every seed); from v2 it is the SEED'S own
- // effective raw maximum — blueprints are no longer reproducible from curve
- // constants alone, which is exactly why it is recorded here.
- public float T1, T2, T3, T4, SpikeMax;
- public float Sea, OrangeCeil, RedCeil, PlateauLo, PlateauHi, PeakCap, TailSlope; // output bands
- }
-
public class WorldBlueprint
{
public int MapSize;
@@ -93,16 +53,6 @@ namespace IslaApocalypse.Core
// generation parameters. Params is null when the source was a legacy v1 file.
public int FormatVersion = 1;
public BlueprintParams Params;
-
- // Water-bodies data (v2 WBID/WBTB/WSRF sections, terrain-water task 03).
- // Null / empty when the source carries no water sections (legacy v1, or a v2
- // written before the water stage existed). Consumed by nothing at runtime yet.
- public ushort[,] WaterBodyIds; // 0 = no water, 1 = ocean, 2..N = lakes
- public List WaterBodies = new List();
- public ushort[,] WaterSurfaceQ; // quantized levels — BlueprintFormat.DecodeWaterLevel
-
- // The curve that shaped HeightMap (v2 TCRV section); null = raw legacy profile.
- public TerrainCurveInfo TerrainCurve;
}
// 2. The Parser Utility
@@ -222,10 +172,6 @@ namespace IslaApocalypse.Core
else if (tag == BlueprintFormat.TAG_ROADS_BRANCH) sectionOk = ParseRoadTier(reader, blueprint.BranchRoads);
else if (tag == BlueprintFormat.TAG_ROADS_RUGGED) sectionOk = ParseRoadTier(reader, blueprint.RuggedRoads);
else if (tag == BlueprintFormat.TAG_ROADS_TRAIL) sectionOk = ParseRoadTier(reader, blueprint.TrailRoads);
- else if (tag == BlueprintFormat.TAG_WATER_BODY_IDS) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: false);
- else if (tag == BlueprintFormat.TAG_WATER_SURFACE) sectionOk = ParseWaterGrid(reader, blueprint, payloadLength, isSurface: true);
- else if (tag == BlueprintFormat.TAG_WATER_BODY_TABLE) sectionOk = ParseWaterBodyTable(reader, blueprint);
- else if (tag == BlueprintFormat.TAG_TERRAIN_CURVE) sectionOk = ParseTerrainCurve(reader, blueprint);
else
{
// The property the redesign exists to buy: future sections (water,
@@ -348,61 +294,6 @@ namespace IslaApocalypse.Core
return true;
}
- private static bool ParseWaterGrid(BinaryReader reader, WorldBlueprint blueprint, ulong payloadLength, bool isSurface)
- {
- int n = blueprint.MapSize;
- string name = isSurface ? "WSRF" : "WBID";
- if (payloadLength != 2UL * (ulong)n * (ulong)n)
- {
- GD.PrintErr($"[MapDataParser] ERROR: {name} section is {payloadLength} bytes, expected {2UL * (ulong)n * (ulong)n}.");
- return false;
- }
- ushort[,] grid = new ushort[n, n];
- for (int x = 0; x < n; x++)
- for (int y = 0; y < n; y++)
- grid[x, y] = reader.ReadUInt16();
- if (isSurface) blueprint.WaterSurfaceQ = grid;
- else blueprint.WaterBodyIds = grid;
- return true;
- }
-
- private static bool ParseWaterBodyTable(BinaryReader reader, WorldBlueprint blueprint)
- {
- int count = reader.ReadInt32();
- for (int i = 0; i < count; i++)
- {
- var body = new WaterBodyInfo();
- body.Id = reader.ReadUInt16();
- body.Type = reader.ReadByte();
- body.Salinity = reader.ReadByte();
- body.SurfaceLevel = reader.ReadSingle();
- body.PixelCount = reader.ReadInt32();
- body.Centroid = new Vector2(reader.ReadSingle(), reader.ReadSingle());
- if (body.Type > WaterBodyInfo.TYPE_LAKE)
- {
- GD.PrintErr($"[MapDataParser] ERROR: water body {i} has unknown type {body.Type}.");
- return false;
- }
- blueprint.WaterBodies.Add(body);
- }
- return true;
- }
-
- private static bool ParseTerrainCurve(BinaryReader reader, WorldBlueprint blueprint)
- {
- var c = new TerrainCurveInfo();
- c.Version = reader.ReadUInt16();
- c.T1 = reader.ReadSingle(); c.T2 = reader.ReadSingle();
- c.T3 = reader.ReadSingle(); c.T4 = reader.ReadSingle();
- c.SpikeMax = reader.ReadSingle();
- c.Sea = reader.ReadSingle(); c.OrangeCeil = reader.ReadSingle();
- c.RedCeil = reader.ReadSingle(); c.PlateauLo = reader.ReadSingle();
- c.PlateauHi = reader.ReadSingle(); c.PeakCap = reader.ReadSingle();
- c.TailSlope = reader.ReadSingle();
- blueprint.TerrainCurve = c;
- return true;
- }
-
private static bool ParseRoadTier(BinaryReader reader, List into)
{
int pathCount = reader.ReadInt32();
diff --git a/Core/Scripts/README.md b/Core/Scripts/README.md
index d645894..31b56f8 100644
--- a/Core/Scripts/README.md
+++ b/Core/Scripts/README.md
@@ -39,11 +39,10 @@ rendering.
- **`BlueprintWriter.cs`** — writes a `WorldBlueprint` as a v2 file. Blueprint-typed on purpose:
the map generator and the round-trip harness are both just callers.
- **`MapDataParser.cs`** — decodes a `.dat` blueprint into a `WorldBlueprint` (heightmap, biome map,
- towns, four road tiers, and — v2 only — the embedded generation params, per-town highway-node
- flag, and the optional water-bodies data: `WaterBodyIds`, `WaterBodies` table, `WaterSurfaceQ`).
- Dispatches on the first byte: v2 tagged-section files get validation (version gate, size
+ towns, four road tiers, and — v2 only — the embedded generation params and per-town highway-node
+ flag). Dispatches on the first byte: v2 tagged-section files get validation (version gate, size
bounds, section-length and ordinal range checks); legacy v1 files still load, intact, with a
- deprecation warning. Nothing at runtime consumes the water data yet.
+ deprecation warning.
- **`ChunkData.cs`** — one chunk's density and block-ID fields, `+1` padded on every axis so the
mesher can reach into the neighbouring chunk, plus the per-column data the renderer needs.
- **`Constants.cs`** — chunk dimensions, `ISO_LEVEL`, `VOXEL_SCALE`, and the visual/road tunables
diff --git a/ServerConfig.json b/ServerConfig.json
index 7949049..b9af832 100644
--- a/ServerConfig.json
+++ b/ServerConfig.json
@@ -2,7 +2,5 @@
"WorldSeed": 1409879727,
"MapProfile": "8K",
"TownDensity": "Normal",
- "ChunkRadius": 32,
- "SeaLevelModel": "flat",
- "SeaLevelValue": 0.15
+ "ChunkRadius": 32
}
diff --git a/Tools/README.md b/Tools/README.md
index 0166168..05fa653 100644
--- a/Tools/README.md
+++ b/Tools/README.md
@@ -17,10 +17,6 @@ Builds the topographical and logistical foundation of the island before any 3D e
- **Topography:** FastNoiseLite plus distance-falloff maths for a guaranteed mainland island, a
mountain spine, dynamic temperature-driven sea level, and an impact crater pushed into the northern
coast (carved at 80 % of its radius so a landbridge always survives).
-- **Water bodies:** the classified water promoted into explicit blueprint data (ocean + labeled
- lakes with transitional per-body levels — `WBID`/`WBTB`/`WSRF` sections), plus priority-flood
- basin diagnostics. The `SkipRoads` config toggle turns a full ~26-min generation into a ~80 s
- road-less iteration cycle.
- **Logistics:** `AStarGrid2D` pathfinding producing a looping continental highway, a branch to the
mountain hub, and county roads daisy-chained outward with Prim's algorithm. Paths are decimated
(Ramer–Douglas–Peucker) then smoothed (4 Chaikin passes).
diff --git a/Tools/Scripts/HeightCurve.cs b/Tools/Scripts/HeightCurve.cs
deleted file mode 100644
index bced8b8..0000000
--- a/Tools/Scripts/HeightCurve.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using Godot;
-
-///
-/// The height-redistribution curve, v2 (terrain-water task 06) — a pure, static,
-/// monotonic piecewise map over raw blueprint heights (D-035: numbers in, numbers
-/// out; the per-seed spike maximum is an explicit PARAMETER, not hidden state).
-///
-/// v2 changes (developer's task-05 hillshade-gate verdict; everything below the
-/// plateau step is behaviorally byte-identical to v1):
-/// - PER-SEED SPIKE NORMALIZATION: the spike's input domain runs from t4 to the
-/// current seed's own raw pre-curve maximum (hMaxSeed), so every island's
-/// tallest pixel reaches the ceiling — v1 mapped against the pooled
-/// calibration max and mid-range seeds topped out at 110–175 m.
-/// - STIFFER SPIKE: ease-in 0.1u + 0.9·u⁴ (was 0.2u + 0.8·u³) — a wall, not a ramp.
-/// - PEAK CEILING 420 m above sea (was 220 m).
-///
-/// Lower knots/bands are v1's, calibrated 2026-08-07 from batch 04's ten flat-sea
-/// heightmaps (pooled above-sea land CDF, 340,618,126 samples): P75/P90/P93/P96.
-///
-/// Shape (strictly monotonic; every segment's normalized slope bounded below by a
-/// positive constant; asserted numerically per generation against the EFFECTIVE
-/// per-seed curve once hMaxSeed is known):
-/// h ≤ sea (0.15) identity — water and the below-sea world untouched
-/// sea → t1 smooth toe, ease-out blend (gentle rolling, never flat)
-/// t1 → t2 linear rise into the red band
-/// t2 → t3 smooth shoulder up to the plateau shelf
-/// t3 → t4 near-flat plateau step (small positive slope)
-/// t4 → spikeMax accelerating u⁴ spike to the 420 m peak cap (per-seed domain)
-/// h > spikeMax linear tail (strict monotonicity, no clamp; reachable only
-/// in the degenerate near-flat guard case)
-///
-public static class HeightCurve
-{
- public const ushort VERSION = 2;
-
- // Input knots — v1 calibration, unchanged (see class header).
- public const float T1 = 0.628736f; // P75 — orange coverage boundary
- public const float T2 = 0.819152f; // P90
- public const float T3 = 0.879340f; // P93
- public const float T4 = 0.962922f; // P96
-
- // Output bands — the storm ladder. Lower anchors unchanged from v1.
- public const float SEA = 0.15f;
- public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling
- public const float RED_CEIL = 0.27f; // biblical ceiling
- public const float PLATEAU_LO = SEA + 50f / 251f; // ≈ 0.34924 (50 m above sea)
- public const float PLATEAU_HI = PLATEAU_LO + 0.02f; // ≈ 0.36924 (~5 m step relief)
- public const float PEAK_CAP = SEA + 420f / 251f; // ≈ 1.82869 (420 m above sea; v1: 220 m)
- public const float TAIL_SLOPE = 0.25f; // above spikeMax (degenerate guard only)
-
- // Degenerate/near-flat guard: the spike domain is [T4, max(hMaxSeed, T4 + SPIKE_MIN_SPAN)],
- // so a pathological seed whose raw max sits at or below t4 still yields a positive,
- // monotonic domain (its cap is then simply never reached; heights above spikeMax — none in
- // practice — would ride the tail).
- public const float SPIKE_MIN_SPAN = 0.01f;
-
- /// The effective spike-domain top for a seed's raw maximum, guard applied.
- public static float EffectiveSpikeMax(float hMaxSeed)
- {
- return Mathf.Max(hMaxSeed, T4 + SPIKE_MIN_SPAN);
- }
-
- /// Raw pre-curve height.
- /// The seed's raw pre-curve maximum (post noise/falloff/Trench/spine,
- /// pre-carve) — the same field the curve consumes. Makes the map seed-dependent (v2).
- public static float Apply(float h, float hMaxSeed)
- {
- if (h <= SEA) return h;
-
- float u, s;
- if (h < T1)
- {
- u = (h - SEA) / (T1 - SEA);
- s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out, slope ≥ 0.3
- return SEA + s * (ORANGE_CEIL - SEA);
- }
- if (h < T2)
- {
- u = (h - T1) / (T2 - T1);
- return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear
- }
- if (h < T3)
- {
- u = (h - T2) / (T3 - T2);
- s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // smoothstep blend, slope ≥ 0.2
- return RED_CEIL + s * (PLATEAU_LO - RED_CEIL);
- }
- if (h < T4)
- {
- u = (h - T3) / (T4 - T3);
- return PLATEAU_LO + u * (PLATEAU_HI - PLATEAU_LO); // near-flat, small positive slope
- }
- float spikeMax = EffectiveSpikeMax(hMaxSeed);
- if (h < spikeMax)
- {
- u = (h - T4) / (spikeMax - T4);
- s = 0.1f * u + 0.9f * (u * u * u * u); // ease-in u⁴ wall, slope ≥ 0.1
- return PLATEAU_HI + s * (PEAK_CAP - PLATEAU_HI);
- }
- return PEAK_CAP + (h - spikeMax) * TAIL_SLOPE;
- }
-
- ///
- /// Numeric strict-monotonicity check of the EFFECTIVE per-seed curve — call once
- /// per generation after hMaxSeed is known, before the curve pass. A violation is
- /// a build bug, not a data condition — fail loudly and refuse to generate.
- ///
- public static void AssertMonotonic(float hMaxSeed)
- {
- float prevH = -7f;
- float prev = Apply(prevH, hMaxSeed);
-
- // Successive double samples can round to the SAME float32 — only strictly
- // increasing float samples are compared (task-05 incident fix, kept).
- void Check(double hd)
- {
- float h = (float)hd;
- if (h <= prevH) return;
- float v = Apply(h, hMaxSeed);
- if (v <= prev)
- throw new System.InvalidOperationException(
- $"[HeightCurve] MONOTONICITY VIOLATION at h={h} (hMaxSeed={hMaxSeed}): {v} <= {prev}. Refusing to generate.");
- prev = v;
- prevH = h;
- }
-
- // Coarse below the identity region, fine through every knot, out past the
- // per-seed spike top and the tail.
- double top = System.Math.Max(2.0, EffectiveSpikeMax(hMaxSeed) + 0.5);
- for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h);
- for (double h = 0.10; h <= top; h += 0.0001) Check(h);
- for (double h = top + 0.05; h <= top + 6.0; h += 0.05) Check(h);
- GD.Print($"[HeightCurve] Monotonicity assertion passed (v{VERSION}, effective spikeMax {EffectiveSpikeMax(hMaxSeed):F6}).");
- }
-}
diff --git a/Tools/Scripts/HeightCurve.cs.uid b/Tools/Scripts/HeightCurve.cs.uid
deleted file mode 100644
index c9c7559..0000000
--- a/Tools/Scripts/HeightCurve.cs.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ljv7x1fwm3os
diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs
index 5206a53..cd75452 100644
--- a/Tools/Scripts/MapGenerator.cs
+++ b/Tools/Scripts/MapGenerator.cs
@@ -26,34 +26,12 @@ public partial class MapGenerator : TextureRect
private float _impactRadius;
private float[,] _heightMap;
-
- // Classification heightmap (task 05): the UNCURVED heights (plus the crater
- // carve), i.e. exactly what the curve-off pipeline produces. Biome rules, the
- // two flood fills, and the shared water predicates read THIS map, so biome and
- // water output is identical with the curve on or off — the bit-identical-biomes
- // oracle holds by construction. Towns, roads, diagnostics, and the exported
- // heights use the curved _heightMap (they live in the 3D world). When the curve
- // is off this is the SAME array as _heightMap (aliased, no copy).
- private float[,] _heightMapClassify;
- private bool _curveOn;
-
- // The seed's raw pre-curve height maximum (post noise/falloff/Trench/spine,
- // pre-carve) — the v2 curve's per-seed spike normalizer. Computed in
- // GenerateTopography pass 1; recorded in TCRV (effective, guard applied).
- private float _hMaxSeed = float.MinValue;
-
private float[,] _tempMap;
private Biome[,] _biomeMap;
private bool[,] _isTrueOcean;
- private bool[,] _isMainland;
+ private bool[,] _isMainland;
internal List _towns = new List();
- // Water-bodies stage outputs (terrain-water task 03): 0 = no water, 1 = the
- // ocean, 2..N = lakes. Filled by IdentifyWaterBodies, serialized by the v2
- // writer, consumed by nothing at runtime yet.
- internal ushort[,] _waterBodyIds;
- internal List _waterBodies;
-
internal List _highwayPaths = new List();
internal List _branchPaths = new List();
internal List _ruggedPaths = new List();
@@ -75,10 +53,6 @@ public partial class MapGenerator : TextureRect
this.CustomMinimumSize = new Vector2(MapSize, MapSize);
_heightMap = new float[MapSize, MapSize];
- _curveOn = ConfigManager.TerrainCurve == "v2";
- // (The monotonicity assertion now runs inside GenerateTopography, against the
- // effective per-seed curve, once hMaxSeed is known.)
- _heightMapClassify = _curveOn ? new float[MapSize, MapSize] : _heightMap;
_tempMap = new float[MapSize, MapSize];
_biomeMap = new Biome[MapSize, MapSize];
_isTrueOcean = new bool[MapSize, MapSize];
@@ -104,19 +78,12 @@ public partial class MapGenerator : TextureRect
// A future water stage slots in as another CaptureStage call at its own boundary.
GenerateTopography();
- GD.Print($"{T()} Topography done (height + temperature). TerrainCurve: {(_curveOn ? "v1" : "off")}.");
-
- DrawHeightStageTexture();
- await CaptureStage("0_height");
+ GD.Print($"{T()} Topography done (height + temperature).");
CalculateTrueOcean();
CalculateMainland();
GD.Print($"{T()} Ocean and mainland masks done.");
- IdentifyWaterBodies();
- DrawWaterStageTexture();
- await CaptureStage("0_water");
-
AssignBiomesAndDraw();
GD.Print($"{T()} Biomes done.");
await CaptureStage("1_biomes");
@@ -125,28 +92,13 @@ public partial class MapGenerator : TextureRect
GD.Print($"{T()} Towns placed: {_towns.Count}.");
await CaptureStage("2_towns");
- if (ConfigManager.SkipRoads)
- {
- // Iteration toggle (terrain-water task 03): the road pass is ~25 min of a
- // ~26-min generation. Skipping it leaves all four road lists empty, so the
- // export writes present-but-empty road sections.
- GD.Print("==========================================================");
- GD.Print($"{T()} ⚠⚠ SKIPROADS IS ON — ROAD GENERATION SKIPPED ENTIRELY.");
- GD.Print($"{T()} ⚠⚠ This blueprint has NO ROADS. It is an ITERATION");
- GD.Print($"{T()} ⚠⚠ ARTIFACT, not a world. Do not judge or ship it.");
- GD.Print("==========================================================");
- }
- else
- {
- // NEW: We await the roads so the engine doesn't freeze!
- await GenerateRoadsAsync();
- }
+ // NEW: We await the roads so the engine doesn't freeze!
+ await GenerateRoadsAsync();
ExportMapData();
GD.Print($"{T()} Blueprint exported.");
- if (!ConfigManager.SkipRoads)
- await CaptureStage("3_roads");
+ await CaptureStage("3_roads");
GD.Print($"{T()} GENERATION COMPLETE.");
}
@@ -254,17 +206,6 @@ public partial class MapGenerator : TextureRect
BranchRoads = _branchPaths,
RuggedRoads = _ruggedPaths,
TrailRoads = _trailPaths,
- WaterBodyIds = _waterBodyIds,
- WaterBodies = _waterBodies ?? new List(),
- TerrainCurve = _curveOn ? new TerrainCurveInfo
- {
- Version = HeightCurve.VERSION,
- T1 = HeightCurve.T1, T2 = HeightCurve.T2, T3 = HeightCurve.T3, T4 = HeightCurve.T4,
- SpikeMax = HeightCurve.EffectiveSpikeMax(_hMaxSeed), // per-seed (v2)
- Sea = HeightCurve.SEA, OrangeCeil = HeightCurve.ORANGE_CEIL, RedCeil = HeightCurve.RED_CEIL,
- PlateauLo = HeightCurve.PLATEAU_LO, PlateauHi = HeightCurve.PLATEAU_HI,
- PeakCap = HeightCurve.PEAK_CAP, TailSlope = HeightCurve.TAIL_SLOPE
- } : null,
FormatVersion = 2,
Params = new BlueprintParams
{
@@ -407,51 +348,23 @@ public partial class MapGenerator : TextureRect
}
// --- 4. COMBINE HEIGHT ---
- // PASS 1 stores the RAW pre-curve height and tracks the seed maximum;
- // the curve (which is per-seed in v2 — its spike normalizes against
- // hMaxSeed) and the crater carve are applied in PASS 2 below.
float rawBase = (_noise.GetNoise2D(x, y) + 1.0f) / 2.0f;
float finalH = rawBase + mountainSpine - (finalFalloff * FalloffStrength);
- if (finalH > _hMaxSeed) _hMaxSeed = finalH;
- _heightMap[x, y] = finalH;
- }
- }
- // The v2 curve is SEED-DEPENDENT: its spike maps [t4, hMaxSeed] onto the peak
- // band, so the monotonicity assertion must run against the EFFECTIVE per-seed
- // curve — after hMaxSeed is known, before any pixel is curved.
- if (_curveOn) HeightCurve.AssertMonotonic(_hMaxSeed);
-
- // --- PASS 2: curve (task 05/06) + crater carve ---
- // Curve applied AFTER noise + falloff + Trench, BEFORE the crater carve, so
- // the carve cuts into curved terrain and the rim/bowl shape is untouched by
- // the curve. Identity at and below sea + this ordering preserve the
- // Trench/ocean-border guarantee and the crater by construction. classifyH
- // stays uncurved — see _heightMapClassify; hMaxSeed never touches it.
- float physicalCraterRadius = _impactRadius * 0.80f;
- for (int x = 0; x < MapSize; x++)
- {
- for (int y = 0; y < MapSize; y++)
- {
- float raw = _heightMap[x, y];
- float classifyH = raw;
- float curvedH = _curveOn ? HeightCurve.Apply(raw, _hMaxSeed) : raw;
// --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
-
+
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
+ float physicalCraterRadius = _impactRadius * 0.80f;
if (distToCrater < physicalCraterRadius)
{
- float craterDepth = 1.0f - (distToCrater / physicalCraterRadius);
+ float craterDepth = 1.0f - (distToCrater / physicalCraterRadius);
// Dialed back to -0.15f as per your excellent instinct!
- float carveTarget = GetSeaLevel(_tempMap[x, y]) - 0.15f;
- classifyH = Mathf.Lerp(classifyH, carveTarget, craterDepth * 0.9f);
- curvedH = Mathf.Lerp(curvedH, carveTarget, craterDepth * 0.9f);
+ finalH = Mathf.Lerp(finalH, GetSeaLevel(temperature) - 0.15f, craterDepth * 0.9f);
}
-
- _heightMapClassify[x, y] = classifyH;
- _heightMap[x, y] = curvedH;
+
+ _heightMap[x, y] = finalH;
}
}
}
@@ -472,7 +385,7 @@ public partial class MapGenerator : TextureRect
Vector2I neighbor = current + dir;
if (neighbor.X >= 0 && neighbor.X < MapSize && neighbor.Y >= 0 && neighbor.Y < MapSize)
{
- if (!_isTrueOcean[neighbor.X, neighbor.Y] && _heightMapClassify[neighbor.X, neighbor.Y] < GetSeaLevel(_tempMap[neighbor.X, neighbor.Y]))
+ if (!_isTrueOcean[neighbor.X, neighbor.Y] && _heightMap[neighbor.X, neighbor.Y] < GetSeaLevel(_tempMap[neighbor.X, neighbor.Y]))
{
_isTrueOcean[neighbor.X, neighbor.Y] = true;
queue.Enqueue(neighbor);
@@ -487,7 +400,7 @@ public partial class MapGenerator : TextureRect
Queue queue = new Queue();
Vector2I center = new Vector2I(MapSize / 2, MapSize / 2);
- if (_heightMapClassify[center.X, center.Y] >= GetSeaLevel(_tempMap[center.X, center.Y]))
+ if (_heightMap[center.X, center.Y] >= GetSeaLevel(_tempMap[center.X, center.Y]))
{
queue.Enqueue(center);
_isMainland[center.X, center.Y] = true;
@@ -503,7 +416,7 @@ public partial class MapGenerator : TextureRect
Vector2I neighbor = current + dir;
if (neighbor.X >= 0 && neighbor.X < MapSize && neighbor.Y >= 0 && neighbor.Y < MapSize)
{
- if (!_isMainland[neighbor.X, neighbor.Y] && _heightMapClassify[neighbor.X, neighbor.Y] >= GetSeaLevel(_tempMap[neighbor.X, neighbor.Y]))
+ if (!_isMainland[neighbor.X, neighbor.Y] && _heightMap[neighbor.X, neighbor.Y] >= GetSeaLevel(_tempMap[neighbor.X, neighbor.Y]))
{
_isMainland[neighbor.X, neighbor.Y] = true;
queue.Enqueue(neighbor);
@@ -513,358 +426,6 @@ public partial class MapGenerator : TextureRect
}
}
- // =====================================================================
- // SHARED WATER PREDICATES (terrain-water task 03)
- // The single source of per-pixel water truth. AssignBiomesAndDraw and
- // IdentifyWaterBodies both call these; correspondence between the biome
- // grid and the water-body grid holds BY CONSTRUCTION, not by parallel
- // implementations agreeing.
- // =====================================================================
- private bool IsWaterPixel(int x, int y) => _heightMapClassify[x, y] < GetSeaLevel(_tempMap[x, y]);
- private bool IsOceanPixel(int x, int y) => IsWaterPixel(x, y) && _isTrueOcean[x, y];
- private bool IsLakePixel(int x, int y) => IsWaterPixel(x, y) && !_isTrueOcean[x, y];
-
- ///
- /// The water-bodies stage (terrain-water task 03). Promotes the world's EXISTING
- /// water into explicit data: body 1 = the ocean (all IsOceanPixel pixels), bodies
- /// 2..N = lakes, connected-component labeled with the SAME 4-connectivity as
- /// CalculateTrueOcean's flood fill (Up/Down/Left/Right), in deterministic scan
- /// order (X outer, Y inner; a body's id is fixed by its first-encountered pixel).
- /// Membership comes only from the shared predicates — this stage groups pixels,
- /// it never adds or removes any.
- ///
- /// Each body carries ONE surface level: GetSeaLevel at the body's pixel centroid
- /// (ocean: at the map centre). This is the documented TRANSITIONAL rule — see
- /// BLUEPRINT_FORMAT.md (WBTB) — superseded when the flat-scalar sea model lands.
- ///
- private void IdentifyWaterBodies()
- {
- ulong t0 = Time.GetTicksMsec();
- _waterBodyIds = new ushort[MapSize, MapSize];
- _waterBodies = new List();
-
- // --- Body 1: the ocean, one body, first-class ---
- long oceanCount = 0;
- double oceanCx = 0, oceanCy = 0;
- for (int x = 0; x < MapSize; x++)
- {
- for (int y = 0; y < MapSize; y++)
- {
- if (IsOceanPixel(x, y))
- {
- _waterBodyIds[x, y] = 1;
- oceanCount++;
- oceanCx += x; oceanCy += y;
- }
- }
- }
- Vector2 oceanCentroid = oceanCount > 0
- ? new Vector2((float)(oceanCx / oceanCount), (float)(oceanCy / oceanCount))
- : Vector2.Zero;
- _waterBodies.Add(new WaterBodyInfo
- {
- Id = 1,
- Type = WaterBodyInfo.TYPE_OCEAN,
- Salinity = WaterBodyInfo.SALINITY_SALT, // provisional default
- SurfaceLevel = GetSeaLevel(_tempMap[MapSize / 2, MapSize / 2]), // ocean: map centre
- PixelCount = (int)oceanCount,
- Centroid = oceanCentroid
- });
-
- // --- Bodies 2..N: lakes, 4-connected like CalculateTrueOcean ---
- Vector2I[] directions = { Vector2I.Up, Vector2I.Down, Vector2I.Left, Vector2I.Right };
- Queue queue = new Queue();
- int nextId = 2;
- long lakePixels = 0;
-
- for (int x = 0; x < MapSize; x++)
- {
- for (int y = 0; y < MapSize; y++)
- {
- if (_waterBodyIds[x, y] != 0 || !IsLakePixel(x, y)) continue;
-
- if (nextId > ushort.MaxValue)
- {
- GD.PrintErr($"[WaterBodies] ⚠⚠ More than {ushort.MaxValue - 1} water bodies — u16 id space exhausted. Remaining lakes left unlabeled.");
- x = MapSize; break;
- }
-
- ushort id = (ushort)nextId++;
- long count = 0;
- double cx = 0, cy = 0;
-
- _waterBodyIds[x, y] = id;
- queue.Enqueue(new Vector2I(x, y));
- while (queue.Count > 0)
- {
- Vector2I current = queue.Dequeue();
- count++; cx += current.X; cy += current.Y;
- foreach (var dir in directions)
- {
- Vector2I nb = current + dir;
- if (nb.X < 0 || nb.X >= MapSize || nb.Y < 0 || nb.Y >= MapSize) continue;
- if (_waterBodyIds[nb.X, nb.Y] != 0 || !IsLakePixel(nb.X, nb.Y)) continue;
- _waterBodyIds[nb.X, nb.Y] = id;
- queue.Enqueue(nb);
- }
- }
-
- lakePixels += count;
- int centX = Mathf.Clamp((int)Mathf.Round((float)(cx / count)), 0, MapSize - 1);
- int centY = Mathf.Clamp((int)Mathf.Round((float)(cy / count)), 0, MapSize - 1);
- _waterBodies.Add(new WaterBodyInfo
- {
- Id = id,
- Type = WaterBodyInfo.TYPE_LAKE,
- Salinity = WaterBodyInfo.SALINITY_FRESH, // provisional default
- SurfaceLevel = GetSeaLevel(_tempMap[centX, centY]),
- PixelCount = (int)count,
- Centroid = new Vector2((float)(cx / count), (float)(cy / count))
- });
- }
- }
-
- double seconds = (Time.GetTicksMsec() - t0) / 1000.0;
- GD.Print($"{T()} [WaterBodies] {_waterBodies.Count} bodies in {seconds:F1}s: ocean {oceanCount} px, {_waterBodies.Count - 1} lakes totalling {lakePixels} px.");
-
- RunPriorityFloodDiagnostics();
- }
-
- ///
- /// Priority-flood pit-filling over the post-topography heightmap — VALIDATED
- /// DIAGNOSTICS ONLY. Serializes nothing (deliberately no BSIN section: basin data
- /// goes stale the moment coast smoothing changes terrain; the rivers stage
- /// recomputes fresh — see BLUEPRINT_FORMAT.md). Reports closed-basin statistics
- /// and asserts two invariants: (a) filled ≥ original everywhere; (b) on the filled
- /// surface every pixel has a non-ascending 8-neighbour path to the map border
- /// (checked in full via a reverse BFS, not a sample).
- ///
- private void RunPriorityFloodDiagnostics()
- {
- ulong t0 = Time.GetTicksMsec();
- int n = MapSize;
- int total = n * n;
-
- // 1-D row-major copies (idx = x * n + y) for speed.
- float[] original = new float[total];
- for (int x = 0; x < n; x++)
- for (int y = 0; y < n; y++)
- original[x * n + y] = _heightMap[x, y];
- float[] filled = (float[])original.Clone();
-
- // --- Priority-flood (Barnes et al. variant: heap + plain pit queue) ---
- bool[] visited = new bool[total];
- var heap = new PriorityQueue();
- var pit = new Queue();
-
- void Seed(int idx) { if (!visited[idx]) { visited[idx] = true; heap.Enqueue(idx, filled[idx]); } }
- for (int x = 0; x < n; x++) { Seed(x * n); Seed(x * n + (n - 1)); }
- for (int y = 0; y < n; y++) { Seed(y); Seed((n - 1) * n + y); }
-
- while (heap.Count > 0 || pit.Count > 0)
- {
- int c = pit.Count > 0 ? pit.Dequeue() : heap.Dequeue();
- float fc = filled[c];
- int cx = c / n, cy = c % n;
- for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- if (dx == 0 && dy == 0) continue;
- int nx = cx + dx, ny = cy + dy;
- if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
- int ni = nx * n + ny;
- if (visited[ni]) continue;
- visited[ni] = true;
- if (filled[ni] <= fc) { filled[ni] = fc; pit.Enqueue(ni); }
- else heap.Enqueue(ni, filled[ni]);
- }
- }
- }
- double floodSeconds = (Time.GetTicksMsec() - t0) / 1000.0;
-
- // --- Invariant (a): filled ≥ original everywhere ---
- long invariantAViolations = 0;
- for (int i = 0; i < total; i++)
- if (filled[i] < original[i]) invariantAViolations++;
-
- // --- Invariant (b): full reverse BFS from the border over non-descending
- // edges; a pixel is reachable iff it has a non-ascending 8-neighbour path
- // down to the border on the filled surface. ---
- bool[] reachable = new bool[total];
- var bfs = new Queue();
- void SeedB(int idx) { if (!reachable[idx]) { reachable[idx] = true; bfs.Enqueue(idx); } }
- for (int x = 0; x < n; x++) { SeedB(x * n); SeedB(x * n + (n - 1)); }
- for (int y = 0; y < n; y++) { SeedB(y); SeedB((n - 1) * n + y); }
- while (bfs.Count > 0)
- {
- int c = bfs.Dequeue();
- float fc = filled[c];
- int cx = c / n, cy = c % n;
- for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- if (dx == 0 && dy == 0) continue;
- int nx = cx + dx, ny = cy + dy;
- if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
- int ni = nx * n + ny;
- if (reachable[ni] || filled[ni] < fc) continue;
- reachable[ni] = true;
- bfs.Enqueue(ni);
- }
- }
- }
- long invariantBViolations = 0;
- for (int i = 0; i < total; i++)
- if (!reachable[i]) invariantBViolations++;
-
- if (invariantAViolations > 0)
- GD.PrintErr($"[PriorityFlood] ⚠⚠ INVARIANT (a) VIOLATED: {invariantAViolations} pixels have filled < original.");
- if (invariantBViolations > 0)
- GD.PrintErr($"[PriorityFlood] ⚠⚠ INVARIANT (b) VIOLATED: {invariantBViolations} pixels lack a non-ascending path to the border.");
-
- // --- Closed-basin statistics on LAND (shared predicate), 8-connected ---
- long landPixels = 0, basinLandPixels = 0;
- var basinAreas = new List();
- var basinDepths = new List();
- bool[] counted = new bool[total];
- var comp = new Queue();
- for (int x = 0; x < n; x++)
- {
- for (int y = 0; y < n; y++)
- {
- int i = x * n + y;
- bool land = !IsWaterPixel(x, y);
- if (land) landPixels++;
- if (!land || counted[i] || filled[i] <= original[i]) continue;
-
- long area = 0; float maxDepth = 0f;
- counted[i] = true;
- comp.Enqueue(i);
- while (comp.Count > 0)
- {
- int c = comp.Dequeue();
- area++;
- float d = filled[c] - original[c];
- if (d > maxDepth) maxDepth = d;
- int cx2 = c / n, cy2 = c % n;
- for (int dx = -1; dx <= 1; dx++)
- {
- for (int dy = -1; dy <= 1; dy++)
- {
- if (dx == 0 && dy == 0) continue;
- int nx = cx2 + dx, ny = cy2 + dy;
- if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
- int ni = nx * n + ny;
- if (counted[ni] || filled[ni] <= original[ni] || IsWaterPixel(nx, ny)) continue;
- counted[ni] = true;
- comp.Enqueue(ni);
- }
- }
- }
- basinLandPixels += area;
- basinAreas.Add(area);
- basinDepths.Add(maxDepth);
- }
- }
-
- basinAreas.Sort();
- basinDepths.Sort();
- long P(List s, double q) => s.Count == 0 ? 0 : s[Mathf.Clamp((int)(q * s.Count), 0, s.Count - 1)];
- float Pf(List s, double q) => s.Count == 0 ? 0 : s[Mathf.Clamp((int)(q * s.Count), 0, s.Count - 1)];
-
- double totalSeconds = (Time.GetTicksMsec() - t0) / 1000.0;
- GD.Print($"{T()} [PriorityFlood] flood {floodSeconds:F1}s, total (with invariants+stats) {totalSeconds:F1}s.");
- GD.Print($"{T()} [PriorityFlood] invariants: (a) {(invariantAViolations == 0 ? "PASS" : "FAIL")}, (b) {(invariantBViolations == 0 ? "PASS" : "FAIL")} (full check, no sampling).");
- GD.Print($"{T()} [PriorityFlood] closed basins on land: {basinAreas.Count}; land px in basins {basinLandPixels}/{landPixels} ({(landPixels > 0 ? 100.0 * basinLandPixels / landPixels : 0):F1}%).");
- GD.Print($"{T()} [PriorityFlood] area px: p50 {P(basinAreas, 0.5)}, p90 {P(basinAreas, 0.9)}, max {(basinAreas.Count > 0 ? basinAreas[basinAreas.Count - 1] : 0)}; " +
- $"count ≥100px {basinAreas.FindAll(a => a >= 100).Count}, ≥1000px {basinAreas.FindAll(a => a >= 1000).Count}, ≥10000px {basinAreas.FindAll(a => a >= 10000).Count}.");
- GD.Print($"{T()} [PriorityFlood] max depth (raw h): p50 {Pf(basinDepths, 0.5):F4}, p90 {Pf(basinDepths, 0.9):F4}, max {(basinDepths.Count > 0 ? basinDepths[basinDepths.Count - 1] : 0):F4}; " +
- $"deeper than 0.01: {basinDepths.FindAll(d => d > 0.01f).Count}, deeper than 0.05: {basinDepths.FindAll(d => d > 0.05f).Count}.");
- }
-
- ///
- /// Paints the height-stage snapshot (task 05): hypsometric tint by storm-ladder
- /// band × Lambert hillshade from the height gradient (NW light), over the CURVED
- /// heights — this is the snapshot that makes relief visible. Runs in both curve
- /// modes; with the curve off it shows the legacy profile under the same bands.
- /// Pure numeric pass over the heightmap.
- ///
- private void DrawHeightStageTexture()
- {
- Image img = Image.CreateEmpty(MapSize, MapSize, false, Image.Format.Rgba8);
- Vector3 light = new Vector3(-0.55f, -0.55f, 0.63f).Normalized(); // NW, ~39° up
-
- Color deepSea = new Color(0.07f, 0.15f, 0.32f);
- Color shallowSea = new Color(0.25f, 0.45f, 0.65f);
- Color green = new Color(0.44f, 0.62f, 0.36f); // orange band terrain: lowland green
- Color tan = new Color(0.76f, 0.70f, 0.46f); // red band: tan
- Color brown = new Color(0.55f, 0.41f, 0.28f); // shoulder + plateau: brown
- Color white = new Color(0.97f, 0.97f, 0.98f); // peaks
- Color grey = new Color(0.72f, 0.72f, 0.70f);
-
- int n = MapSize;
- for (int x = 0; x < n; x++)
- {
- for (int y = 0; y < n; y++)
- {
- float h = _heightMap[x, y];
- float sea = GetSeaLevel(_tempMap[x, y]);
-
- Color tint;
- if (h < sea)
- {
- float depth = Mathf.Clamp((sea - h) / 0.5f, 0f, 1f);
- tint = shallowSea.Lerp(deepSea, depth);
- }
- else if (h < HeightCurve.ORANGE_CEIL) tint = green;
- else if (h < HeightCurve.RED_CEIL) tint = tan;
- else if (h < HeightCurve.PLATEAU_HI) tint = brown;
- else
- {
- float t2 = Mathf.Clamp((h - HeightCurve.PLATEAU_HI) / (HeightCurve.PEAK_CAP - HeightCurve.PLATEAU_HI), 0f, 1f);
- tint = grey.Lerp(white, t2);
- }
-
- // Lambert hillshade on the world-scale gradient (1 px = 1 m, height ×251 m).
- int xm = x > 0 ? x - 1 : x, xp = x < n - 1 ? x + 1 : x;
- int ym = y > 0 ? y - 1 : y, yp = y < n - 1 ? y + 1 : y;
- float gx = (_heightMap[xp, y] - _heightMap[xm, y]) * 251f / (xp - xm == 0 ? 1 : xp - xm);
- float gy = (_heightMap[x, yp] - _heightMap[x, ym]) * 251f / (yp - ym == 0 ? 1 : yp - ym);
- Vector3 nrm = new Vector3(-gx, -gy, 1f).Normalized();
- float shade = Mathf.Clamp(nrm.Dot(light), 0f, 1f);
-
- Color c = tint * (0.35f + 0.65f * shade);
- c.A = 1f;
- img.SetPixel(x, y, c);
- }
- }
- Texture = ImageTexture.CreateFromImage(img);
- }
-
- ///
- /// Paints the water-stage snapshot from the stage's own outputs (the biome grid
- /// does not exist yet at this point in the pipeline): ocean deep blue, lakes a
- /// distinct lighter blue, land neutral grey.
- ///
- private void DrawWaterStageTexture()
- {
- Image img = Image.CreateEmpty(MapSize, MapSize, false, Image.Format.Rgba8);
- Color land = new Color(0.45f, 0.45f, 0.42f);
- Color ocean = new Color(0.05f, 0.2f, 0.45f);
- Color lake = new Color(0.35f, 0.7f, 0.9f);
- for (int x = 0; x < MapSize; x++)
- {
- for (int y = 0; y < MapSize; y++)
- {
- ushort id = _waterBodyIds[x, y];
- img.SetPixel(x, y, id == 0 ? land : (id == 1 ? ocean : lake));
- }
- }
- Texture = ImageTexture.CreateFromImage(img);
- }
-
private void AssignBiomesAndDraw()
{
Image mapImage = Image.CreateEmpty(MapSize, MapSize, false, Image.Format.Rgba8);
@@ -872,21 +433,13 @@ public partial class MapGenerator : TextureRect
{
for (int y = 0; y < MapSize; y++)
{
- // Biome rules classify against the UNCURVED heights (task 05) — the
- // bit-identical-biomes oracle. Beach/mountain/snow bands and the
- // water split all read the classify map.
- float h = _heightMapClassify[x, y];
+ float h = _heightMap[x, y];
float t = _tempMap[x, y];
Biome b;
-
+
float currentSeaLevel = GetSeaLevel(t);
- // Water classification comes from the SHARED predicates (task 03), so the
- // water-bodies stage and the biome classifier cannot disagree. Same rules
- // as before, verbatim: below local sea level -> Ocean if true-ocean
- // connected, else Lake.
- if (IsOceanPixel(x, y)) b = Biome.Ocean;
- else if (IsLakePixel(x, y)) b = Biome.Lake;
+ if (h < currentSeaLevel) b = _isTrueOcean[x, y] ? Biome.Ocean : Biome.Lake;
else
{
float baseDist = new Vector2(x, y).DistanceTo(_impactCenter);
@@ -1132,20 +685,12 @@ public partial class MapGenerator : TextureRect
if (t < minTemp || t > maxTemp) continue;
float centerH = _heightMap[rx, ry];
- // Sample slope a bit wider (10px) due to higher resolution.
- // Samples are clamped to map bounds (task 05): candidates spawn at
- // [20, MapSize-20] but slopeRadius is 40 at 8K, so the unclamped reads
- // were out of bounds near the border — previously unreachable only
- // because border land stayed underwater and failed the sea test first.
+ // Sample slope a bit wider (10px) due to higher resolution
int slopeRadius = (int)(MapSize * 0.005f); // Automatically scales!
- int sxHi = Mathf.Clamp(rx + slopeRadius, 0, MapSize - 1);
- int sxLo = Mathf.Clamp(rx - slopeRadius, 0, MapSize - 1);
- int syHi = Mathf.Clamp(ry + slopeRadius, 0, MapSize - 1);
- int syLo = Mathf.Clamp(ry - slopeRadius, 0, MapSize - 1);
- if (Mathf.Abs(_heightMap[sxHi, ry] - centerH) > maxSlope) continue;
- if (Mathf.Abs(_heightMap[sxLo, ry] - centerH) > maxSlope) continue;
- if (Mathf.Abs(_heightMap[rx, syHi] - centerH) > maxSlope) continue;
- if (Mathf.Abs(_heightMap[rx, syLo] - centerH) > maxSlope) continue;
+ if (Mathf.Abs(_heightMap[rx+slopeRadius, ry] - centerH) > maxSlope) continue;
+ if (Mathf.Abs(_heightMap[rx-slopeRadius, ry] - centerH) > maxSlope) continue;
+ if (Mathf.Abs(_heightMap[rx, ry+slopeRadius] - centerH) > maxSlope) continue;
+ if (Mathf.Abs(_heightMap[rx, ry-slopeRadius] - centerH) > maxSlope) continue;
bool nearTrueOcean = false;
bool nearAnyWater = false;
@@ -1342,13 +887,7 @@ public partial class MapGenerator : TextureRect
return astar.GetPointPath(from, to);
}
- // Sea-level model gate (D-033, task 04): "flat" returns the configured scalar;
- // "field" is the legacy latitude Lerp. Pure numeric mapping either way (D-035);
- // all nine call sites inherit whichever model config selects.
- private float GetSeaLevel(float t) =>
- ConfigManager.SeaLevelModel == "flat"
- ? ConfigManager.SeaLevelValue
- : Mathf.Lerp(0.26f, 0.15f, Mathf.Clamp(t, 0f, 1f));
+ private float GetSeaLevel(float t) => Mathf.Lerp(0.26f, 0.15f, Mathf.Clamp(t, 0f, 1f));
private Vector2I FindClosestPixel(Vector2 pos, HashSet set) {
Vector2I closest = new Vector2I(0,0); float min = float.MaxValue;
diff --git a/Tools/Scripts/README.md b/Tools/Scripts/README.md
index 0fb6401..deaf7c8 100644
--- a/Tools/Scripts/README.md
+++ b/Tools/Scripts/README.md
@@ -11,29 +11,14 @@ Generates the entire 2D blueprint. Roughly in order:
2. **Topography** — FastNoiseLite base height plus a mountain spine, minus a squircle distance
falloff, giving a guaranteed island. Noise frequency is divided by `scaleFactor`
(`MapSize / 1024f`) so terrain features stay the same real-world size at any map profile.
- When `TerrainCurve: "v2"` (the default), the calibrated height-redistribution curve
- (`HeightCurve.cs` — flat farmable lowlands, 50 m plateau shelf, and a per-seed-normalized
- spike to the 420 m peak cap: every island's tallest point reaches the ceiling) reshapes
- above-sea terrain after noise/falloff/Trench and before the crater carve; biome
- classification reads a retained uncurved map, so biomes are identical either way. Drops the
- `0_height` hillshade snapshot (hypsometric bands × NW hillshade) in both modes.
-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
- separates true ocean from inland lakes; a mainland fill guarantees one contiguous landmass.
+3. **Sea level and water** — temperature-driven sea level; flood fill separates true ocean from
+ inland lakes; a mainland fill guarantees one contiguous landmass.
4. **The crater** — placed along the northern coast and carved to below sea level, but only out to
**80 % of its radius**, which guarantees a landbridge rather than severing the island.
-5. **Water bodies** — promotes the classified water into explicit data: the ocean as body 1, each
- lake connected-component labeled (same 4-connectivity as the true-ocean fill), one transitional
- surface level per body. Classification comes from the shared `IsOceanPixel`/`IsLakePixel`
- predicates that the biome stage also uses, so the two can never disagree. A priority-flood
- pit-fill runs here as validated diagnostics (basin statistics to console; serializes nothing).
- Drops the `0_water` snapshot.
-6. **Biomes and towns** — biome zoning by height and temperature; tiered town placement (Capitol,
+5. **Biomes and towns** — biome zoning by height and temperature; tiered town placement (Capitol,
Hubs, Villages, Outposts, POIs) filtered by slope, water proximity and spacing.
-7. **Roads** — see below. Skipped entirely when the `SkipRoads` config toggle is on (the ~25-min
- A\* pass is the bottleneck; a skip run exports a road-less iteration blueprint in ~80 s with a
- loud console banner).
-8. **Export** — writes the `.dat` blueprint (dual-write: the v2 tagged container under the primary
+6. **Roads** — see below.
+7. **Export** — writes the `.dat` blueprint (dual-write: the v2 tagged container under the primary
seed name, plus the legacy v1 format beside it as `_v1.dat` — see
`Core/Scripts/BLUEPRINT_FORMAT.md`), then renders the PNG snapshot. The v2 file embeds the
resolved generation params (seed, MapSize, crater radius, density, impact centre, provenance).
@@ -70,11 +55,8 @@ Road colours on the snapshot, useful for identifying a road: **red** = Highway,
**dark brown** = Rugged, **light brown** = Trail.
### Outputs
-`MapData_Seed_.dat` (v2), `MapData_Seed__v1.dat` (legacy dual-write), and the staged
-snapshots `Map_Seed__{0_height,0_water,1_biomes,2_towns,3_roads}.png`, all to `user://`.
-(`0_height` is the hypsometric hillshade of the curved terrain; `0_water` is painted from the
-water stage's own outputs — ocean deep blue, lakes lighter blue, land neutral; `3_roads` is
-absent on a SkipRoads run.)
+`MapData_Seed_.dat` (v2), `MapData_Seed__v1.dat` (legacy dual-write), and
+`Map_Seed_.png`, all to `user://`.
## `RoundTripHarness.cs`
diff --git a/Tools/Scripts/RoundTripHarness.cs b/Tools/Scripts/RoundTripHarness.cs
index 7b2393a..bc0c6db 100644
--- a/Tools/Scripts/RoundTripHarness.cs
+++ b/Tools/Scripts/RoundTripHarness.cs
@@ -119,8 +119,6 @@ public partial class RoundTripHarness : Node
ok &= CompareRoads("Branch", a.BranchRoads, b.BranchRoads);
ok &= CompareRoads("Rugged", a.RuggedRoads, b.RuggedRoads);
ok &= CompareRoads("Trail", a.TrailRoads, b.TrailRoads);
- ok &= CompareWater(a, b);
- ok &= CompareTerrainCurve(a, b);
if (ok)
GD.Print($"[Harness] Semantic equality holds: {a.MapSize}x{a.MapSize} grid, " +
@@ -129,86 +127,6 @@ public partial class RoundTripHarness : Node
return ok;
}
- // Water sections (WBID/WBTB/WSRF) are compared whenever the SOURCE carries them —
- // the regression test grows with the format. A source without water (legacy v1)
- // must round-trip to a file without water.
- private bool CompareWater(WorldBlueprint a, WorldBlueprint b)
- {
- if (a.WaterBodyIds == null && b.WaterBodyIds == null)
- {
- GD.Print("[Harness] Water sections: absent in source — nothing to compare (and none reappeared).");
- return true;
- }
- if (a.WaterBodyIds == null || b.WaterBodyIds == null)
- {
- GD.PrintErr($"[Harness] Water sections presence mismatch: source {(a.WaterBodyIds != null ? "has" : "lacks")} them, reread {(b.WaterBodyIds != null ? "has" : "lacks")} them.");
- return false;
- }
-
- bool ok = true;
- long idDiffs = 0, surfDiffs = 0;
- int n = a.MapSize;
- for (int x = 0; x < n; x++)
- {
- for (int y = 0; y < n; y++)
- {
- if (a.WaterBodyIds[x, y] != b.WaterBodyIds[x, y]) idDiffs++;
- ushort sa = a.WaterSurfaceQ != null ? a.WaterSurfaceQ[x, y] : (ushort)0;
- ushort sb = b.WaterSurfaceQ != null ? b.WaterSurfaceQ[x, y] : (ushort)0;
- if (sa != sb) surfDiffs++;
- }
- }
- if (idDiffs > 0) { GD.PrintErr($"[Harness] {idDiffs} WBID pixels differ."); ok = false; }
- if (surfDiffs > 0) { GD.PrintErr($"[Harness] {surfDiffs} WSRF pixels differ."); ok = false; }
-
- if (a.WaterBodies.Count != b.WaterBodies.Count)
- {
- GD.PrintErr($"[Harness] Water body count mismatch: {a.WaterBodies.Count} vs {b.WaterBodies.Count}");
- ok = false;
- }
- else
- {
- for (int i = 0; i < a.WaterBodies.Count; i++)
- {
- var wa = a.WaterBodies[i];
- var wb = b.WaterBodies[i];
- if (wa.Id != wb.Id || wa.Type != wb.Type || wa.Salinity != wb.Salinity ||
- System.BitConverter.SingleToInt32Bits(wa.SurfaceLevel) != System.BitConverter.SingleToInt32Bits(wb.SurfaceLevel) ||
- wa.PixelCount != wb.PixelCount || wa.Centroid != wb.Centroid)
- {
- GD.PrintErr($"[Harness] Water body {i} mismatch (id {wa.Id} vs {wb.Id}).");
- ok = false;
- }
- }
- }
-
- if (ok) GD.Print($"[Harness] Water sections equal: {a.WaterBodies.Count} bodies, WBID+WSRF grids identical.");
- return ok;
- }
-
- private bool CompareTerrainCurve(WorldBlueprint a, WorldBlueprint b)
- {
- if (a.TerrainCurve == null && b.TerrainCurve == null)
- {
- GD.Print("[Harness] TCRV: absent in source — nothing to compare (and none reappeared).");
- return true;
- }
- if (a.TerrainCurve == null || b.TerrainCurve == null)
- {
- GD.PrintErr("[Harness] TCRV presence mismatch between source and reread.");
- return false;
- }
- var ca = a.TerrainCurve; var cb = b.TerrainCurve;
- bool same = ca.Version == cb.Version;
- float[] fa = { ca.T1, ca.T2, ca.T3, ca.T4, ca.SpikeMax, ca.Sea, ca.OrangeCeil, ca.RedCeil, ca.PlateauLo, ca.PlateauHi, ca.PeakCap, ca.TailSlope };
- float[] fb = { cb.T1, cb.T2, cb.T3, cb.T4, cb.SpikeMax, cb.Sea, cb.OrangeCeil, cb.RedCeil, cb.PlateauLo, cb.PlateauHi, cb.PeakCap, cb.TailSlope };
- for (int i = 0; i < fa.Length; i++)
- if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i])) same = false;
- if (!same) { GD.PrintErr("[Harness] TCRV fields differ."); return false; }
- GD.Print($"[Harness] TCRV equal (curve v{ca.Version}).");
- return true;
- }
-
private bool CompareRoads(string tier, List a, List b)
{
if (a.Count != b.Count)