diff --git a/Core/Scripts/BlueprintFormat.cs b/Core/Scripts/BlueprintFormat.cs
index 5aad3c7..162bdd2 100644
--- a/Core/Scripts/BlueprintFormat.cs
+++ b/Core/Scripts/BlueprintFormat.cs
@@ -35,6 +35,13 @@ namespace IslaApocalypse.Core
public const uint TAG_TERRAIN_CURVE = 0x56524354; // "TCRV"
public const uint TAG_TERRAIN_DETAIL = 0x4C544454; // "TDTL"
+ // TDTL body version. 1 = shelf micro-relief + D8 drainage incision (the
+ // task-10 draft; the incision was reverted, and the only v1 payloads in
+ // existence are in that batch's tree). 2 = the current body. A reader that
+ // meets an unrecognised body version skips it rather than misreading a
+ // differently shaped payload into plausible-looking nonsense.
+ public const ushort TDTL_VERSION = 2;
+
// 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
diff --git a/Core/Scripts/BlueprintWriter.cs b/Core/Scripts/BlueprintWriter.cs
index 27fe859..e7ff721 100644
--- a/Core/Scripts/BlueprintWriter.cs
+++ b/Core/Scripts/BlueprintWriter.cs
@@ -155,12 +155,9 @@ namespace IslaApocalypse.Core
private static void WriteTerrainDetail(BinaryWriter writer, TerrainDetailInfo d)
{
- writer.Write(d.Version);
- writer.Write(d.ReliefAmpM); writer.Write(d.ReliefFreqIslands);
- writer.Write(d.IncK); writer.Write(d.IncP); writer.Write(d.IncCapM);
- writer.Write(d.SeaClampRaw); writer.Write(d.CraterExclFactor);
- writer.Write(d.ShelfIncWeight);
- writer.Write(d.ReliefSeedOffset);
+ writer.Write(d.Version); // u16
+ writer.Write(d.ReliefAmpM); writer.Write(d.ReliefFreqIslands); // 2 × f32
+ writer.Write(d.ReliefSeedOffset); // i32
}
private static void WriteWaterBodyIds(BinaryWriter writer, WorldBlueprint bp)
diff --git a/Core/Scripts/ConfigManager.cs b/Core/Scripts/ConfigManager.cs
index 14e233e..d0387b5 100644
--- a/Core/Scripts/ConfigManager.cs
+++ b/Core/Scripts/ConfigManager.cs
@@ -33,10 +33,9 @@ namespace IslaApocalypse.Core // Change this if your namespace is different
// are retired. Default: v5.
public static string TerrainCurve = "v5";
- // Terrain detail passes (task 10): "v1" = shelf micro-relief + drainage
- // incision as one judged unit (requires the curve; no-op when it is off);
- // "off" disables both. ShelfReliefAmp is the micro-relief amplitude in
- // metres. Defaults: v1, 3 m.
+ // Terrain detail passes (task 10): "v1" = shelf micro-relief (requires the
+ // curve; no-op when it is off), "off" disables it. ShelfReliefAmp is the
+ // micro-relief amplitude in metres. Defaults: v1, 3 m.
public static string TerrainDetail = "v1";
public static float ShelfReliefAmp = 3.0f;
diff --git a/Core/Scripts/MapDataParser.cs b/Core/Scripts/MapDataParser.cs
index 35e4695..c5f7754 100644
--- a/Core/Scripts/MapDataParser.cs
+++ b/Core/Scripts/MapDataParser.cs
@@ -89,15 +89,15 @@ namespace IslaApocalypse.Core
///
/// The terrain detail passes that shaped this blueprint's HGTS (v2 TDTL section,
- /// terrain-water task 10): shelf micro-relief + drainage incision parameters.
- /// Null when detail was off. Metadata only — heights are already detailed.
+ /// terrain-water task 10): the shelf micro-relief parameters. Null when detail
+ /// was off. Metadata only — heights are already detailed. Body version 1 (the
+ /// reverted relief + D8-incision layout) is longer and differently shaped; the
+ /// parser refuses it rather than misreading it.
///
public class TerrainDetailInfo
{
public ushort Version;
public float ReliefAmpM, ReliefFreqIslands;
- public float IncK, IncP, IncCapM;
- public float SeaClampRaw, CraterExclFactor, ShelfIncWeight;
public int ReliefSeedOffset;
}
@@ -254,7 +254,7 @@ namespace IslaApocalypse.Core
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 if (tag == BlueprintFormat.TAG_TERRAIN_DETAIL) sectionOk = ParseTerrainDetail(reader, blueprint);
+ else if (tag == BlueprintFormat.TAG_TERRAIN_DETAIL) sectionOk = ParseTerrainDetail(reader, blueprint, payloadLength);
else
{
// The property the redesign exists to buy: future sections (water,
@@ -448,14 +448,19 @@ namespace IslaApocalypse.Core
return true;
}
- private static bool ParseTerrainDetail(BinaryReader reader, WorldBlueprint blueprint)
+ private static bool ParseTerrainDetail(BinaryReader reader, WorldBlueprint blueprint, ulong payloadLength)
{
var d = new TerrainDetailInfo();
d.Version = reader.ReadUInt16();
+ if (d.Version != BlueprintFormat.TDTL_VERSION)
+ {
+ // Skip the body and leave TerrainDetail null. The heights are still
+ // whatever they are — we simply refuse to describe them wrongly.
+ GD.PrintErr($"[MapDataParser] ⚠ TDTL body version {d.Version} is not the current {BlueprintFormat.TDTL_VERSION} — section skipped, detail metadata unavailable.");
+ reader.BaseStream.Seek((long)payloadLength - 2L, SeekOrigin.Current);
+ return true;
+ }
d.ReliefAmpM = reader.ReadSingle(); d.ReliefFreqIslands = reader.ReadSingle();
- d.IncK = reader.ReadSingle(); d.IncP = reader.ReadSingle(); d.IncCapM = reader.ReadSingle();
- d.SeaClampRaw = reader.ReadSingle(); d.CraterExclFactor = reader.ReadSingle();
- d.ShelfIncWeight = reader.ReadSingle();
d.ReliefSeedOffset = reader.ReadInt32();
blueprint.TerrainDetail = d;
return true;
diff --git a/Tools/Scripts/MapGenerator.cs b/Tools/Scripts/MapGenerator.cs
index 40528cb..e4aecb1 100644
--- a/Tools/Scripts/MapGenerator.cs
+++ b/Tools/Scripts/MapGenerator.cs
@@ -49,9 +49,9 @@ public partial class MapGenerator : TextureRect
// v5: the selected knot preset; null = curve off.
private CurveKnots _curveKnots;
- // Task-10 detail passes (shelf micro-relief + drainage incision): gated by
- // TerrainDetail, active only with the curve on (the masks are curve-band
- // defined). The relief noise seeds from resolvedSeed + 7409.
+ // Task-10 detail pass (shelf micro-relief): gated by TerrainDetail, active only
+ // with the curve on (the mask is curve-band defined). The relief noise seeds
+ // from resolvedSeed + 7409.
private bool _detailOn;
private FastNoiseLite _reliefNoise;
@@ -314,11 +314,6 @@ public partial class MapGenerator : TextureRect
Version = TerrainDetailPass.VERSION,
ReliefAmpM = ConfigManager.ShelfReliefAmp,
ReliefFreqIslands = TerrainDetailPass.RELIEF_FREQ_ISLANDS,
- IncK = TerrainDetailPass.INC_K, IncP = TerrainDetailPass.INC_P,
- IncCapM = TerrainDetailPass.INC_CAP_M,
- SeaClampRaw = TerrainDetailPass.SEA_CLAMP,
- CraterExclFactor = TerrainDetailPass.CRATER_EXCL_FACTOR,
- ShelfIncWeight = TerrainDetailPass.SHELF_INC_WEIGHT,
ReliefSeedOffset = TerrainDetailPass.RELIEF_SEED_OFFSET
} : null,
FormatVersion = 2,
@@ -484,15 +479,16 @@ public partial class MapGenerator : TextureRect
// 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.
- // --- PASS 2a: curve + shelf micro-relief (task 10 pass A) ---
// classify stays RAW; curved gets the v5 curve plus, when TerrainDetail is on,
// the shelf-ness-weighted noise skin (risers and peaks untouched).
float reliefAmpRaw = ConfigManager.ShelfReliefAmp / 251f;
+ 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;
if (_curveOn)
{
@@ -515,23 +511,11 @@ public partial class MapGenerator : TextureRect
curvedH = raw;
}
- _heightMapClassify[x, y] = raw; // uncurved; carve joins in pass 2c
- _heightMap[x, y] = curvedH;
- }
- }
-
- // --- PASS 2b: drainage incision (task 10 pass B) ---
- if (_curveOn && _detailOn)
- RunIncisionPass();
-
- // --- PASS 2c: THE CRATER CARVE (The Flooded Bay & Landbridge Fix!) ---
- // The carve remains the FINAL authority on its own terrain: applied after
- // curve/relief/incision, to both maps, with the original expression.
- float physicalCraterRadius = _impactRadius * 0.80f;
- for (int x = 0; x < MapSize; x++)
- {
- for (int y = 0; y < MapSize; y++)
- {
+ // --- 5. CARVE THE CRATER (The Flooded Bay & Landbridge Fix!) ---
+ // Both maps are carved from LOCALS and written once each, so the
+ // curve-off aliasing (classify IS the height array) cannot double-carve
+ // — the failure mode that the separate carve loop introduced and that
+ // the task-10 continuity oracle caught (fix 1b98fb5, now structural).
float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
// We only carve the physical hole at 80% of the radius to guarantee a landbridge!
@@ -540,82 +524,16 @@ public partial class MapGenerator : TextureRect
float craterDepth = 1.0f - (distToCrater / physicalCraterRadius);
// Dialed back to -0.15f as per your excellent instinct!
float carveTarget = GetSeaLevel(_tempMap[x, y]) - 0.15f;
- // Read BOTH before writing EITHER: with the curve off the two maps
- // alias the same array, and a sequential read-modify-write carved
- // the crater twice (caught by the task-10 continuity oracle).
- float preClassify = _heightMapClassify[x, y];
- float preCurved = _heightMap[x, y];
- _heightMapClassify[x, y] = Mathf.Lerp(preClassify, carveTarget, craterDepth * 0.9f);
- _heightMap[x, y] = Mathf.Lerp(preCurved, carveTarget, craterDepth * 0.9f);
+ classifyH = Mathf.Lerp(classifyH, carveTarget, craterDepth * 0.9f);
+ curvedH = Mathf.Lerp(curvedH, carveTarget, craterDepth * 0.9f);
}
+
+ _heightMapClassify[x, y] = classifyH;
+ _heightMap[x, y] = curvedH;
}
}
}
- ///
- /// Task-10 pass B: D8 flow accumulation over the curved+relieved land, then
- /// depth = K · accum^p · slope, masked to the risers (shelves feathered to 30 %,
- /// toe and peaks zero, crater excluded), capped, and clamped to sea + 1 m.
- /// Prints its own MEASURED depth distribution — the tuning/report source.
- ///
- private void RunIncisionPass()
- {
- ulong t0 = Time.GetTicksMsec();
- int n = MapSize;
- int total = n * n;
-
- float[] flat = new float[total];
- for (int x = 0; x < n; x++)
- for (int y = 0; y < n; y++)
- flat[x * n + y] = _heightMap[x, y];
-
- int[] accum = TerrainDetailPass.FlowAccumulation(flat, n, out float[] drop);
- double accumSeconds = (Time.GetTicksMsec() - t0) / 1000.0;
-
- float capRaw = TerrainDetailPass.INC_CAP_M / 251f;
- float exclR = _impactRadius * TerrainDetailPass.CRATER_EXCL_FACTOR;
- float featherR = _impactRadius * TerrainDetailPass.CRATER_FEATHER_FACTOR;
- long incised = 0, clampHits = 0;
- var depthsM = new System.Collections.Generic.List(1 << 20);
-
- for (int x = 0; x < n; x++)
- {
- for (int y = 0; y < n; y++)
- {
- float raw = _heightMapClassify[x, y];
- float w = TerrainDetailPass.IncisionWeight(raw, _curveKnots);
- if (w <= 0f) continue;
-
- float distToCrater = new Vector2(x, y).DistanceTo(_impactCenter);
- if (distToCrater < exclR) continue;
- if (distToCrater < featherR)
- w *= (distToCrater - exclR) / (featherR - exclR);
-
- int i = x * n + y;
- float depth = TerrainDetailPass.INC_K
- * Mathf.Pow(accum[i], TerrainDetailPass.INC_P) * drop[i];
- depth = Mathf.Min(depth, capRaw) * w;
- if (depth <= 0f) continue;
-
- float nh = _heightMap[x, y] - depth;
- if (nh < TerrainDetailPass.SEA_CLAMP)
- {
- nh = TerrainDetailPass.SEA_CLAMP;
- clampHits++;
- }
- float realized = _heightMap[x, y] - nh;
- if (realized * 251f >= 0.5f) { incised++; depthsM.Add(realized * 251f); }
- _heightMap[x, y] = nh;
- }
- }
-
- depthsM.Sort();
- float P(double q) => depthsM.Count == 0 ? 0 : depthsM[Mathf.Clamp((int)(q * depthsM.Count), 0, depthsM.Count - 1)];
- double totalSeconds = (Time.GetTicksMsec() - t0) / 1000.0;
- GD.Print($"{T()} [Incision] accumulation {accumSeconds:F1}s, total {totalSeconds:F1}s.");
- GD.Print($"{T()} [Incision] incised cells (≥0.5 m): {incised}; depth m: p50 {P(0.5):F1}, p90 {P(0.9):F1}, p99 {P(0.99):F1}, max {(depthsM.Count > 0 ? depthsM[depthsM.Count - 1] : 0):F1}; sea-clamp hits {clampHits}.");
- }
-
private void CalculateTrueOcean()
{
Queue queue = new Queue();
diff --git a/Tools/Scripts/RoundTripHarness.cs b/Tools/Scripts/RoundTripHarness.cs
index 10128cb..803a16c 100644
--- a/Tools/Scripts/RoundTripHarness.cs
+++ b/Tools/Scripts/RoundTripHarness.cs
@@ -229,8 +229,8 @@ public partial class RoundTripHarness : Node
return false;
}
var da = a.TerrainDetail; var db = b.TerrainDetail;
- float[] fa = { da.Version, da.ReliefAmpM, da.ReliefFreqIslands, da.IncK, da.IncP, da.IncCapM, da.SeaClampRaw, da.CraterExclFactor, da.ShelfIncWeight, da.ReliefSeedOffset };
- float[] fb = { db.Version, db.ReliefAmpM, db.ReliefFreqIslands, db.IncK, db.IncP, db.IncCapM, db.SeaClampRaw, db.CraterExclFactor, db.ShelfIncWeight, db.ReliefSeedOffset };
+ float[] fa = { da.Version, da.ReliefAmpM, da.ReliefFreqIslands, da.ReliefSeedOffset };
+ float[] fb = { db.Version, db.ReliefAmpM, db.ReliefFreqIslands, db.ReliefSeedOffset };
for (int i = 0; i < fa.Length; i++)
if (System.BitConverter.SingleToInt32Bits(fa[i]) != System.BitConverter.SingleToInt32Bits(fb[i]))
{
diff --git a/Tools/Scripts/TerrainDetailPass.cs b/Tools/Scripts/TerrainDetailPass.cs
index b7cf84f..4a9d70c 100644
--- a/Tools/Scripts/TerrainDetailPass.cs
+++ b/Tools/Scripts/TerrainDetailPass.cs
@@ -1,43 +1,34 @@
using Godot;
-using System;
///
-/// The terrain DETAIL passes (terrain-water task 10) — pure numeric array machinery
+/// The terrain DETAIL passes (terrain-water task 10) — pure numeric functions
/// (D-035; a named future C++ candidate, kept standalone):
///
/// PASS A — shelf micro-relief: a medium-frequency noise skin (±ShelfReliefAmp,
/// default 3 m) weighted by shelf-ness, so the compressed shelves get their
/// rolling texture back while risers and peaks stay untouched.
///
-/// PASS B — drainage incision: D8 steepest-descent flow routing + accumulation
-/// over the curved terrain; depth = K · accum^p · localSlope (capped), masked to
-/// the risers (feathered ~30 % onto shelves, zero on the toe and above the
-/// plateau top, zero near the crater), clamped so carved terrain never drops
-/// below sea + 1 m. The channels double as the future river routes (Phase C).
+/// Output-height only; the classify map never sees it.
///
-/// Ordering (enforced by the caller): curve → micro-relief → incision → crater
-/// carve. The classify map never sees any of it.
+/// NOTE — what this file deliberately no longer contains: the task-10 draft's D8
+/// steepest-descent flow routing, accumulation and drainage incision. It shipped,
+/// and it produced the canonical grid artifact — thousands of straight,
+/// disconnected, pooling scratches running along the eight D8 neighbour
+/// directions, because per-cell steepest descent on a regular grid can only ever
+/// route along those eight headings. It is reverted whole. Rivers and erosion are
+/// Phase C work: a hydraulic-erosion pass over FINAL terrain, not a routing carve
+/// on a grid mid-pipeline.
///
public static class TerrainDetailPass
{
- public const ushort VERSION = 1;
+ // The TDTL body version is owned by the format (Core); the pass just stamps it.
+ public const ushort VERSION = IslaApocalypse.Core.BlueprintFormat.TDTL_VERSION;
// Pass A — micro-relief.
public const float RELIEF_AMP_DEFAULT_M = 3f; // config dial: ShelfReliefAmp (metres)
public const float RELIEF_FREQ_ISLANDS = 40f; // ~40 undulations per island width (~200 m features)
public const int RELIEF_SEED_OFFSET = 7409;
- // Pass B — incision. K/p tuned against the depth targets (gullies 8–15 m,
- // trunks ~25 m, cap 30 m); the tuning run's achieved distribution is in the
- // task-10 report.
- public const float INC_K = 1.40f;
- public const float INC_P = 0.45f; // concave: many fingers, few deep trunks
- public const float INC_CAP_M = 30f; // IncisionMax
- public const float SEA_CLAMP = 0.15f + 1f / 251f; // carved height ≥ sea + 1 m
- public const float SHELF_INC_WEIGHT = 0.3f; // shelves get washes, not gorges
- public const float CRATER_EXCL_FACTOR = 1.2f; // zero incision inside this × CraterRadius
- public const float CRATER_FEATHER_FACTOR = 1.4f; // ...feathering to full by this × CraterRadius
-
///
/// Shelf-ness weight from the RAW input height: 1 mid-shelf, feathering to 0
/// through the risers (feather extends 30 % of the band half-width past each
@@ -55,89 +46,4 @@ public static class TerrainDetailPass
// full inside 60 % of the band, linear feather to zero at 130 %
return Mathf.Clamp(1f - (t - 0.6f) / 0.7f, 0f, 1f);
}
-
- ///
- /// Incision mask from the RAW input height: 0 below the red-ceiling input (K2)
- /// and above the plateau top (K6); 1 on the riser bands; SHELF_INC_WEIGHT on the
- /// shelf bands; smooth feathers (15 % of the local band width) at every boundary.
- ///
- public static float IncisionWeight(float raw, CurveKnots k)
- {
- if (raw <= k.K2 || raw >= k.K6) return 0f;
- if (raw < k.K3) // foothill riser: feather in from K2, feather toward shelf weight at K3
- return EdgeBlend(raw, k.K2, k.K3, 0f, 1f, SHELF_INC_WEIGHT);
- if (raw < k.K4) // bench
- return SHELF_INC_WEIGHT;
- if (raw < k.K5) // mid riser
- return EdgeBlend(raw, k.K4, k.K5, SHELF_INC_WEIGHT, 1f, SHELF_INC_WEIGHT);
- // plateau band: shelf weight, feathering to zero at K6
- float w = (k.K6 - raw) / ((k.K6 - k.K5) * 0.15f);
- return Mathf.Min(SHELF_INC_WEIGHT, Mathf.Clamp(w, 0f, 1f) * SHELF_INC_WEIGHT);
- }
-
- private static float EdgeBlend(float h, float lo, float hi, float wIn, float wMid, float wOut)
- {
- float f = (hi - lo) * 0.15f;
- if (h < lo + f) return Mathf.Lerp(wIn, wMid, (h - lo) / f);
- if (h > hi - f) return Mathf.Lerp(wMid, wOut, (h - (hi - f)) / f);
- return wMid;
- }
-
- ///
- /// D8 flow accumulation over a height field (row-major idx = x·n + y).
- /// Steepest-descent routing (drop / distance, diagonals ÷√2), deterministic
- /// tie-break (fixed neighbour order, first winner). Cells with no lower
- /// neighbour are pits/outlets (no outflow). accum = upslope contributing cells
- /// including self; steepestDrop = drop per pixel toward the chosen neighbour.
- ///
- public static int[] FlowAccumulation(float[] h, int n, out float[] steepestDrop)
- {
- int total = n * n;
- int[] downstream = new int[total];
- steepestDrop = new float[total];
- int[] dx = { 1, -1, 0, 0, 1, 1, -1, -1 };
- int[] dy = { 0, 0, 1, -1, 1, -1, 1, -1 };
- float[] invDist = { 1f, 1f, 1f, 1f, 0.7071068f, 0.7071068f, 0.7071068f, 0.7071068f };
-
- for (int x = 0; x < n; x++)
- {
- for (int y = 0; y < n; y++)
- {
- int i = x * n + y;
- float hc = h[i];
- float best = 0f;
- int bestIdx = -1;
- for (int d = 0; d < 8; d++)
- {
- int nx = x + dx[d], ny = y + dy[d];
- if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
- int ni = nx * n + ny;
- float grade = (hc - h[ni]) * invDist[d];
- if (grade > best)
- {
- best = grade;
- bestIdx = ni;
- }
- }
- downstream[i] = bestIdx;
- steepestDrop[i] = best;
- }
- }
-
- // Height-descending order: each cell pushes its accumulated count downstream.
- float[] keys = (float[])h.Clone();
- int[] order = new int[total];
- for (int i = 0; i < total; i++) order[i] = i;
- Array.Sort(keys, order); // ascending
-
- int[] accum = new int[total];
- for (int i = 0; i < total; i++) accum[i] = 1;
- for (int i = total - 1; i >= 0; i--)
- {
- int c = order[i];
- int d = downstream[c];
- if (d >= 0) accum[d] += accum[c];
- }
- return accum;
- }
}