diff --git a/Core/README.md b/Core/README.md
index b3e4cc5..8e656df 100644
--- a/Core/README.md
+++ b/Core/README.md
@@ -37,6 +37,7 @@ resolution and the file-safety rails. Constants and contracts.
| `Scripts/CurveKnots.cs` | The six INPUT knots — percentiles of the measured land CDF, plus the reference's for comparison. |
| `Scripts/CurveAnchors.cs` | The OUTPUT anchors — the storm-ladder elevations each band lands at. |
| `Scripts/TerrainDetailPass.cs` | Shelf micro-relief + the shelf-edge **knot warp**. Output-height only. |
+| `Scripts/RegionLabeling.cs` | ⭐⭐ **The region-labeling layer** (chat2/07) — shared infrastructure. 8-connected land components on the CLASSIFY field; mainland = the centre component; per component id / size / centroid / hemisphere (by centroid) / isMainland. Pure, engine-free, C++-candidate; a **contract** downstream phases consume (islands first; biomes, placement, rivers, the crater later). The hemisphere convention lives here. |
| `Scripts/ToolingPaths.cs` | Every tooling path, env-overridable, resolved in one place. |
| `Scripts/FileSafety.cs` | The permanent file-safety rules, as throws rather than sentences. |
@@ -56,9 +57,11 @@ resolution and the file-safety rails. Constants and contracts.
- **No water.** Water is an overlay over the columns (levels-not-cells), never a band.
- **No biomes.** Biomes are a later *classification* of finished shape, not an input to it (D-049).
-- **No algorithms** *beyond the height curve*. Phase 2 added `HeightCurve` and
+- **No algorithms** *beyond the height curve and the region layer*. Phase 2 added `HeightCurve` and
`TerrainDetailPass` here because they are pure, engine-free, C++-candidate math that defines the
- world's elevation profile — a contract, not a tool's dial. Stratigraphy, feature passes, meshing
+ world's elevation profile — a contract, not a tool's dial. chat2/07 added `RegionLabeling` on the
+ same grounds: region identity is a contract every later phase reads, and the flood fill is a hot
+ path. (The speck REVERT that uses it is a pass, and lives in `Tools/` — `RegionPass`.) Stratigraphy, feature passes, meshing
and run-splitting on dig are still later phases.
- **No erosion, rivers, water bodies, crater carve, coast shelf or offshore islets.** Later chat2
tasks; the curve is deliberately the only pass-2 element present.
diff --git a/Core/Scripts/RegionLabeling.cs b/Core/Scripts/RegionLabeling.cs
new file mode 100644
index 0000000..15467f9
--- /dev/null
+++ b/Core/Scripts/RegionLabeling.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+
+namespace IslaApocalypse.Core
+{
+ /// One maximal 8-connected component of land, as the region layer exposes it.
+ public sealed class LandRegion
+ {
+ /// 1-based, assigned in deterministic scan order (x outer, y inner) — stable per seed across runs.
+ public int Id;
+
+ /// Cells in the component.
+ public long SizeCells;
+
+ /// Centroid in map cells.
+ public double CentroidX, CentroidY;
+
+ ///
+ /// / , decided by the
+ /// CENTROID — one label per component; a straddler is decided by where its mass is, never per cell.
+ ///
+ public byte Hemisphere;
+
+ /// True for exactly one component: the one containing the map centre (or the flagged fallback).
+ public bool IsMainland;
+
+ /// Bounding box, inclusive. Convenience for overlays and guards; not part of the contract.
+ public int MinX, MinY, MaxX, MaxY;
+ }
+
+ /// The result of one labeling: the per-cell id map and the per-component table.
+ public sealed class RegionLabels
+ {
+ public int MapSize;
+
+ /// Per cell, x * MapSize + y: the component id, or 0 for water.
+ public int[] Id;
+
+ /// Every component, indexed by Id - 1, in id order.
+ public List Regions;
+
+ /// The mainland's id (0 only if there is no land at all).
+ public int MainlandId;
+
+ ///
+ /// ⚠ Whether the map-centre cell was land. Expected always true (the massif is centred and
+ /// stable). When false the mainland fell back to the LARGEST component and the caller must
+ /// report it loudly — the contract's mainland definition did not hold on this field.
+ ///
+ public bool CentreWasLand;
+
+ public long LandCells;
+ public int IslandCount => Regions.Count - (MainlandId > 0 ? 1 : 0);
+
+ public LandRegion Mainland => MainlandId > 0 ? Regions[MainlandId - 1] : null;
+ public LandRegion Of(int id) => Regions[id - 1];
+ public int IdAt(int x, int y) => Id[x * MapSize + y];
+ }
+
+ ///
+ /// ⭐⭐ THE REGION-LABELING LAYER — shared infrastructure (chat2/07). Flood-fills land into distinct
+ /// components, identifies mainland vs islands, and exposes per-component data. Islands are its first
+ /// consumer; later phases (biomes, placement, rivers, the crater) CONSUME this layer rather than
+ /// rebuild it. Engine-free; pure analysis over a float[,]; C++-candidate.
+ ///
+ /// ═══ THE CONTRACT — build to it exactly (recorded at graduation as the shared-infra contract) ═══
+ ///
+ /// FIELD It runs on the CLASSIFY (raw, uncurved) height — region identity partitions on the
+ /// same authoritative field as water bodies and biome regions (D-046), so islands /
+ /// water / biomes line up by construction. Raw is authoritative for region identity.
+ /// It does NOT run on the render field.
+ ///
+ /// CONNECTIVITY Land is 8-CONNECTED. Deliberately the complement of water's 4-connectivity —
+ /// foreground/background using opposite connectivity is the topologically sound
+ /// pairing (a diagonal isthmus reads as JOINED; the water on either side of it reads
+ /// as SEPARATE), not a conflict with the water model.
+ ///
+ /// COMPONENT A component = a maximal 8-connected set of land cells (land = classify height ≥ sea).
+ ///
+ /// MAINLAND The component containing the MAP CENTRE (the mountain/massif is always centred and
+ /// stable) — NOT merely the largest component, because a later fragmentation step
+ /// could make "largest" flip seed to seed. Every OTHER land component is an island.
+ /// ⚠ The crater is NOT central — it is a northern-coastline feature, unrelated to the
+ /// centre or the mountain, and plays no part here.
+ /// Defensively: if the centre cell is not land, the layer reports it ( = false) and falls back to the largest
+ /// component, FLAGGED — the caller asserts rather than assumes.
+ ///
+ /// PER COMPONENT id · sizeCells · centroid (x, y) · hemisphere (north / south, BY THE CENTROID —
+ /// one label per island; a straddler is decided by its centroid, never per cell) ·
+ /// isMainland.
+ ///
+ /// Ids are assigned in deterministic scan order (x outer, y inner, first-seen), so they are stable
+ /// per seed across runs. Nothing here knows about "offshore" or "stamped" — it labels land.
+ ///
+ /// ═══ THE HEMISPHERE CONVENTION — read from the code, not invented (chat2/05) ═══
+ ///
+ /// Pass 1's latitude scalar is y / MapSize; the spine's "southern fade" and the "southern
+ /// sinker" bite at high y. So y increases SOUTHWARD: NORTH = rows [0, MapSize/2), SOUTH = rows
+ /// [MapSize/2, MapSize). The clean row midline, never the wobbled latitude field.
+ ///
+ public static class RegionLabeling
+ {
+ public const byte HemiNone = 0;
+ public const byte HemiNorth = 1;
+ public const byte HemiSouth = 2;
+
+ /// The convention, in one place. Every consumer reads hemisphere through this.
+ public static byte HemisphereOfRow(int y, int mapSize) => y < mapSize / 2 ? HemiNorth : HemiSouth;
+
+ public static string HemisphereName(byte h) => h switch
+ {
+ HemiNorth => "north", HemiSouth => "south", _ => "none",
+ };
+
+ // 8-connectivity, fixed order (determinism: the fill order never changes).
+ private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 };
+ private static readonly int[] DY = { -1, 0, 1, -1, 1, -1, 0, 1 };
+
+ ///
+ /// Label every 8-connected land component of (land = height ≥
+ /// ). Pure: the field is read, never written.
+ ///
+ public static RegionLabels Label(float[,] classify, int mapSize, float sea)
+ {
+ int n = mapSize;
+ var id = new int[n * n];
+ var regions = new List();
+ var stack = new Stack();
+ long landCells = 0;
+
+ for (int sx = 0; sx < n; sx++)
+ {
+ for (int sy = 0; sy < n; sy++)
+ {
+ if (classify[sx, sy] < sea || id[sx * n + sy] != 0) continue;
+
+ var r = new LandRegion { Id = regions.Count + 1, MinX = sx, MaxX = sx, MinY = sy, MaxY = sy };
+ double sumX = 0, sumY = 0;
+ id[sx * n + sy] = r.Id;
+ stack.Push(sx * n + sy);
+
+ while (stack.Count > 0)
+ {
+ int cur = stack.Pop();
+ int cx = cur / n, cy = cur % n;
+ r.SizeCells++; sumX += cx; sumY += cy;
+ if (cx < r.MinX) r.MinX = cx; if (cx > r.MaxX) r.MaxX = cx;
+ if (cy < r.MinY) r.MinY = cy; if (cy > r.MaxY) r.MaxY = cy;
+
+ for (int k = 0; k < 8; k++)
+ {
+ int nx = cx + DX[k], ny = cy + DY[k];
+ if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
+ int ni = nx * n + ny;
+ if (id[ni] != 0 || classify[nx, ny] < sea) continue;
+ id[ni] = r.Id;
+ stack.Push(ni);
+ }
+ }
+
+ r.CentroidX = sumX / r.SizeCells;
+ r.CentroidY = sumY / r.SizeCells;
+ r.Hemisphere = HemisphereOfRow((int)Math.Round(r.CentroidY), n);
+ landCells += r.SizeCells;
+ regions.Add(r);
+ }
+ }
+
+ var labels = new RegionLabels { MapSize = n, Id = id, Regions = regions, LandCells = landCells };
+
+ // ⭐ MAINLAND = the component containing the map centre. Asserted by the caller; the
+ // fallback (largest) exists so a run can finish and REPORT the violation rather than crash.
+ int centre = (n / 2) * n + (n / 2);
+ labels.CentreWasLand = id[centre] != 0;
+ if (labels.CentreWasLand) labels.MainlandId = id[centre];
+ else
+ {
+ long best = -1;
+ foreach (var r in regions) if (r.SizeCells > best) { best = r.SizeCells; labels.MainlandId = r.Id; }
+ }
+ if (labels.MainlandId > 0) regions[labels.MainlandId - 1].IsMainland = true;
+ return labels;
+ }
+
+ ///
+ /// Size statistics over the islands (non-mainland components): count, min / median / mean /
+ /// max cells, and a log-spaced histogram — the instrument that turns "nice pieces vs shattered
+ /// gravel" into numbers.
+ ///
+ public static (int count, long min, long median, double mean, long max, int[] histogram)
+ IslandSizes(RegionLabels labels)
+ {
+ var sizes = new List();
+ foreach (var r in labels.Regions) if (!r.IsMainland) sizes.Add(r.SizeCells);
+ var hist = new int[HistogramEdges.Length + 1];
+ if (sizes.Count == 0) return (0, 0, 0, 0.0, 0, hist);
+ sizes.Sort();
+ double sum = 0;
+ foreach (long s in sizes) { sum += s; hist[HistogramBin(s)]++; }
+ return (sizes.Count, sizes[0], sizes[sizes.Count / 2], sum / sizes.Count, sizes[sizes.Count - 1], hist);
+ }
+
+ /// Histogram bin edges (cells): [0,64) [64,256) [256,1024) [1024,4096) [4096,16384) [16384,∞).
+ public static readonly long[] HistogramEdges = { 64, 256, 1024, 4096, 16384 };
+
+ public static int HistogramBin(long cells)
+ {
+ for (int i = 0; i < HistogramEdges.Length; i++) if (cells < HistogramEdges[i]) return i;
+ return HistogramEdges.Length;
+ }
+
+ public static string HistogramLabel(int bin) => bin == 0 ? $"<{HistogramEdges[0]}"
+ : bin < HistogramEdges.Length ? $"{HistogramEdges[bin - 1]}–{HistogramEdges[bin] - 1}"
+ : $"≥{HistogramEdges[^1]}";
+
+ /// Island counts per hemisphere (non-mainland components, by centroid).
+ public static (int north, int south) IslandsByHemisphere(RegionLabels labels)
+ {
+ int nN = 0, nS = 0;
+ foreach (var r in labels.Regions)
+ {
+ if (r.IsMainland) continue;
+ if (r.Hemisphere == HemiNorth) nN++; else if (r.Hemisphere == HemiSouth) nS++;
+ }
+ return (nN, nS);
+ }
+ }
+}
diff --git a/Core/Scripts/RegionLabeling.cs.uid b/Core/Scripts/RegionLabeling.cs.uid
new file mode 100644
index 0000000..824714a
--- /dev/null
+++ b/Core/Scripts/RegionLabeling.cs.uid
@@ -0,0 +1 @@
+uid://bsb27ghrajnhc
diff --git a/Tools/README.md b/Tools/README.md
index 94c3b77..fac50a7 100644
--- a/Tools/README.md
+++ b/Tools/README.md
@@ -36,6 +36,9 @@ constants, carried over verbatim — not re-derived from a design summary** (→
| `Scripts/OffshoreAnalysis.cs` | Island components, N/S counts, the moat check, the separation-guard geometry, **the hemisphere convention** |
| `Scripts/OffshoreDiagnosis.cs` | The per-hemisphere **measurement** — valid-zone area, binding gate, noise peaks over threshold (chat2/06 §2) |
| `Scripts/TagOverlayRenderer.cs` | The island-tag / hemisphere debug overlay |
+| `Scripts/RegionPass.cs` | ⭐ **Pass 1c** (chat2/07) — `Core.RegionLabeling` over the classify field, the **origin-blind speck revert** (lower-only + component-only asserted, mainland never), the island tag **by construction** |
+| `Scripts/RegionOverlayRenderer.cs` | The labeled-regions overlay: mainland one tint, each island its own colour, reverted specks dark red |
+| `Scripts/RegionLabelingTool.cs` + `Scenes/RegionLabelingTool.tscn` | The chat2/07 batch — 3 revert thresholds + a second seed, the count/size instrument |
| `Scripts/OffshoreIslandsTool.cs` + `Scenes/OffshoreIslandsTool.tscn` | The offshore batch — chat2/06: 4 plates + the count table + the diagnosis (the chat2/05 version is at `3b96e06`) |
| `Scripts/Pass1Result.cs` | The height field **and the Phase-2 seams** |
| `Scripts/TerrainGenConfig.cs` | Config + the per-element ablation toggles |
@@ -221,13 +224,23 @@ the per-hemisphere counts are a statistical outcome of the tuning, read off the
> once. Batch tools turn them on explicitly. **Flipping the defaults is the act that retires the
> Phase-1 dumps — do it in a task that re-baselines the oracles.** → `TerrainGenConfig`.
-> ### ⭐ The island tag — data, set here, read by nothing yet.
+**Pass 1c — region labeling + the speck revert + the island tag (chat2/07).** `RegionPass` runs
+`Core.RegionLabeling` over the finished classify field — **land 8-connected, mainland = the component
+containing the map centre, every other component an island** — then the config-gated **speck revert**
+(`TerrainGenConfig.SpeckRevert` / `MinLandComponentFrac`): every non-mainland component below the
+threshold is lowered to its ring's seabed. Origin-blind (a natural nub goes like an offshore dot),
+**lower-only and component-only, asserted**, mainland never a candidate. Then the island tag, by
+construction. ⚠ `SpeckRevert` defaults OFF in the bare config for the same reason as the shelf/islets
+(it would move the calibration pool and every regression dump); the batch turns it on.
+
+> ### ⭐ The island tag — data, set BY THE REGION LAYER, read by nothing yet.
>
-> `Pass1Result.IsOffshoreIsland` / `IslandHemisphere` (carried through `Pass2Result`) mark every cell
-> the islet layer lifted above sea, with its hemisphere. **NORTH = rows `[0, MapSize/2)`, SOUTH =
-> `[MapSize/2, MapSize)`** — y runs south, as the spine's southern fade and the southern sinker
-> already encode. A biome / fertility / placement pass reads it from there and never re-derives
-> island-land from geometry. Null when offshore is off.
+> `Pass1Result.IsIsland` / `IslandHemisphere` (carried through `Pass2Result`) mark every cell of every
+> non-mainland land component — natural detached masses and offshore-pass islands alike — with the
+> COMPONENT's hemisphere (by centroid). **NORTH = rows `[0, MapSize/2)`, SOUTH = `[MapSize/2,
+> MapSize)`** — y runs south. `Pass1Result.Regions` carries the full per-component table. A biome /
+> fertility / placement pass reads these and never re-derives island-land from geometry. Null when
+> region labeling is off. (chat2/05–06 tagged only what the offshore pass raised — fixed in 07.)
> ### ⚠ The one honest coupling: islets TURN WATER INTO LAND.
>
diff --git a/Tools/Scenes/RegionLabelingTool.tscn b/Tools/Scenes/RegionLabelingTool.tscn
new file mode 100644
index 0000000..c4fd7d8
--- /dev/null
+++ b/Tools/Scenes/RegionLabelingTool.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cregions07isla"]
+
+[ext_resource type="Script" path="res://Tools/Scripts/RegionLabelingTool.cs" id="1_rlt"]
+
+[node name="RegionLabelingTool" type="Node"]
+script = ExtResource("1_rlt")
diff --git a/Tools/Scripts/OffshoreAnalysis.cs b/Tools/Scripts/OffshoreAnalysis.cs
index 79e3de4..210eb03 100644
--- a/Tools/Scripts/OffshoreAnalysis.cs
+++ b/Tools/Scripts/OffshoreAnalysis.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
@@ -44,17 +45,15 @@ namespace IslaApocalypse.Tools
///
public static class OffshoreAnalysis
{
- public const byte HemiNone = 0;
- public const byte HemiNorth = 1;
- public const byte HemiSouth = 2;
+ // The convention now lives in Core (RegionLabeling, chat2/07) — one definition; these are aliases.
+ public const byte HemiNone = RegionLabeling.HemiNone;
+ public const byte HemiNorth = RegionLabeling.HemiNorth;
+ public const byte HemiSouth = RegionLabeling.HemiSouth;
- /// The convention, in one place. Every consumer of the tag reads hemisphere through this.
- public static byte HemisphereOfRow(int y, int mapSize) => y < mapSize / 2 ? HemiNorth : HemiSouth;
+ /// → .
+ public static byte HemisphereOfRow(int y, int mapSize) => RegionLabeling.HemisphereOfRow(y, mapSize);
- public static string HemisphereName(byte h) => h switch
- {
- HemiNorth => "north", HemiSouth => "south", _ => "none",
- };
+ public static string HemisphereName(byte h) => RegionLabeling.HemisphereName(h);
// 8-connectivity, fixed order.
private static readonly int[] DX = { -1, -1, -1, 0, 0, 1, 1, 1 };
diff --git a/Tools/Scripts/OffshoreIslandsTool.cs b/Tools/Scripts/OffshoreIslandsTool.cs
index f84c755..5417d50 100644
--- a/Tools/Scripts/OffshoreIslandsTool.cs
+++ b/Tools/Scripts/OffshoreIslandsTool.cs
@@ -12,6 +12,10 @@ namespace IslaApocalypse.Tools
/// NO FORCED COUNT. (chat2/05's version of this tool — faithful / floor_only / hybrid / dense —
/// is in git history at 3b96e06; the forced floor it batched was reverted out on look.)
///
+ /// ⚠ Since chat2/07 the island tag is set BY THE REGION LAYER (every non-mainland land component,
+ /// natural islets included), so this tool's counts now include natural islands; the chat2/06 batch
+ /// of record (offshore-pass islands only) was produced at e8571b2.
+ ///
/// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + a count table + a diagnosis ═══
///
/// PLATES (4, at ISLA_MAPSIZE, grayscale + .f32 + relief + tags overlay):
@@ -253,7 +257,7 @@ namespace IslaApocalypse.Tools
Pass2Result p2 = Shaping.Shape(p1, cfg);
if (!notesShown) { foreach (string n in p1.Notes) GD.Print(" " + n); notesShown = true; }
- var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, tableSize);
+ var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, tableSize);
var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
var checks = new List
@@ -300,7 +304,7 @@ namespace IslaApocalypse.Tools
cfg.CoastShelf = true; cfg.Offshore = lv.Settings.Clone();
Pass1Result p1 = Topography.Generate(cfg);
Pass2Result p2 = Shaping.Shape(p1, cfg);
- var comps = OffshoreAnalysis.Components(p1.IsOffshoreIsland, p1.Height, sea, mapSize);
+ var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, mapSize);
var (cn, cs) = OffshoreAnalysis.CountByHemisphere(comps);
var moat = ShapingOracle.MoatIntact(p1, comps); moat.Name += $" [plate {lv.Label} {seed}]";
var tag = ShapingOracle.TagCoastlineConsistent(p2, sea); tag.Name += $" [plate {lv.Label} {seed}]";
@@ -463,7 +467,7 @@ namespace IslaApocalypse.Tools
.SavePng(Path.Combine(dir, "relief.png"));
// ⭐ The tag overlay — the one artifact that shows the DATA this pass set.
- TagOverlayRenderer.SavePng(p2.Height, p2.IsOffshoreIsland, p2.IslandHemisphere, p2.MapSize, sea,
+ TagOverlayRenderer.SavePng(p2.Height, p2.IsIsland, p2.IslandHemisphere, p2.MapSize, sea,
countN, countS, Path.Combine(dir, "tags.png"));
}
diff --git a/Tools/Scripts/Pass1Result.cs b/Tools/Scripts/Pass1Result.cs
index 27647ec..8dabf47 100644
--- a/Tools/Scripts/Pass1Result.cs
+++ b/Tools/Scripts/Pass1Result.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
@@ -70,27 +71,34 @@ namespace IslaApocalypse.Tools
///
public readonly float HMaxSeedBeforeOffshore;
- // ═══ ⭐ THE OFFSHORE TAG — chat2/05's one forward-looking piece ═══
+ // ═══ ⭐ THE ISLAND TAG — BY CONSTRUCTION from the region layer (chat2/07; chat2/05's tag, fixed) ═══
//
// DATA, set here, carried downstream, READ BY NOTHING IN THIS PHASE. It exists so a later
- // pass (biome, fertility, placement) can find offshore-island land without re-deriving it
- // from geometry. Downstream that ignores it is unaffected; downstream that reads it gets a
- // clean flag. Both arrays are NULL when the offshore layer is off — a consumer checks for
- // null, not for all-false.
+ // pass (biome, fertility, placement) can find island land without re-deriving it from
+ // geometry. Since chat2/07 it is a CONSEQUENCE OF LABELING: every cell of every non-mainland
+ // land component is tagged — the big organic detached masses the same as an offshore-pass
+ // dot (chat2/05–06 tagged only what the offshore pass raised; that was the bug). Both arrays
+ // are NULL when region labeling is off — a consumer checks for null, not for all-false.
+
+ /// Per column: is this land an island (any non-mainland land component)? Null when labeling is off.
+ public readonly bool[,] IsIsland;
///
- /// Per column: is this land an offshore island (as opposed to mainland)? Set for every cell
- /// the islet layer lifted from below sea to at-or-above sea. Null when offshore is off.
- ///
- public readonly bool[,] IsOffshoreIsland;
-
- ///
- /// Per column: /
- /// for tagged cells, otherwise. The convention is the
- /// row midline — see . Null when offshore is off.
+ /// Per column: /
+ /// for tagged cells (the COMPONENT's hemisphere, by centroid),
+ /// otherwise. Null when labeling is off.
///
public readonly byte[,] IslandHemisphere;
+ /// ⭐ The region layer's output for this field: id map + per-component table (post-revert). Null when labeling is off.
+ public readonly RegionLabels Regions;
+
+ /// The labeling BEFORE the speck revert (== when the revert is off or removed nothing). For the overlay's "where a speck was". Null when labeling is off.
+ public readonly RegionLabels RegionsPre;
+
+ /// The region pass's numbers — pre/post-revert counts and sizes, what was reverted. Null when labeling is off.
+ public readonly RegionLedger RegionLedger;
+
/// Cells the islet layer lifted above sea. The reference printed this too.
public readonly long OffshoreLiftedCells;
@@ -106,8 +114,9 @@ namespace IslaApocalypse.Tools
public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs,
float hMaxSeedBeforeOffshore = float.NaN,
- bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null,
- long offshoreLiftedCells = 0, IReadOnlyList notes = null, OffshoreLedger offshoreLedger = null)
+ bool[,] isIsland = null, byte[,] islandHemisphere = null,
+ long offshoreLiftedCells = 0, IReadOnlyList notes = null, OffshoreLedger offshoreLedger = null,
+ RegionLabels regions = null, RegionLedger regionLedger = null, RegionLabels regionsPre = null)
{
MapSize = mapSize;
Seed = seed;
@@ -118,15 +127,18 @@ namespace IslaApocalypse.Tools
HMinSeed = hMinSeed;
ElapsedMs = elapsedMs;
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
- IsOffshoreIsland = isOffshoreIsland;
+ IsIsland = isIsland;
IslandHemisphere = islandHemisphere;
+ Regions = regions;
+ RegionsPre = regionsPre;
+ RegionLedger = regionLedger;
OffshoreLiftedCells = offshoreLiftedCells;
OffshoreLedger = offshoreLedger;
Notes = notes ?? System.Array.Empty();
}
- /// Whether the offshore layer ran on this field (the tag arrays are present).
- public bool HasOffshoreTag => IsOffshoreIsland != null;
+ /// Whether the region layer ran on this field (the tag arrays are present).
+ public bool HasIslandTag => IsIsland != null;
/// Fraction of the map at or above the sea threshold. A cheap shape sanity number.
public float LandFraction(float seaLevel)
diff --git a/Tools/Scripts/Pass2Result.cs b/Tools/Scripts/Pass2Result.cs
index ecc6b4a..6b8db64 100644
--- a/Tools/Scripts/Pass2Result.cs
+++ b/Tools/Scripts/Pass2Result.cs
@@ -95,10 +95,10 @@ namespace IslaApocalypse.Tools
// it from here, checks for null (offshore off), and never re-derives island-land from
// geometry.
- /// → , the same array. Null when offshore is off.
- public readonly bool[,] IsOffshoreIsland;
+ /// → , the same array. Null when region labeling is off.
+ public readonly bool[,] IsIsland;
- /// → , the same array. Null when offshore is off.
+ /// → , the same array. Null when region labeling is off.
public readonly byte[,] IslandHemisphere;
/// Was shelf detail applied? Requires — it warps the curve's knots.
@@ -136,9 +136,9 @@ namespace IslaApocalypse.Tools
bool curveOn, bool detailOn, string curveModeLabel, string variantLabel,
ContinuousCurve continuous, CurveKnots knots, CurveAnchors anchors, float hMaxSeed,
float edgeAmpRaw, float maxEdgeShiftRaw, float hMin, float hMax, ulong elapsedMs,
- List notes, bool[,] isOffshoreIsland = null, byte[,] islandHemisphere = null)
+ List notes, bool[,] isIsland = null, byte[,] islandHemisphere = null)
{
- IsOffshoreIsland = isOffshoreIsland;
+ IsIsland = isIsland;
IslandHemisphere = islandHemisphere;
MapSize = mapSize;
Seed = seed;
diff --git a/Tools/Scripts/RegionLabelingTool.cs b/Tools/Scripts/RegionLabelingTool.cs
new file mode 100644
index 0000000..2c44186
--- /dev/null
+++ b/Tools/Scripts/RegionLabelingTool.cs
@@ -0,0 +1,611 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// ⭐ THE REGION-LABELING BATCH (chat2/07) — the general region layer on the current terrain, the
+ /// island tag fixed by construction, and the tunable speck revert swept.
+ ///
+ /// ═══ WHAT IT PRODUCES — a fixed budget: 4 plates + the count/size table ═══
+ ///
+ /// PLATES (4 fields, each grayscale + .f32 + relief + the LABELED-REGIONS overlay + the tag overlay):
+ /// {plate}_threshold_low / _mid / _high three revert thresholds on ONE seed — the developer
+ /// dials "where too-small-to-keep sits" by eye.
+ /// {second}_threshold_mid the preset on a second seed — the table seed with the
+ /// most NATURAL islands (offshore off), auto-picked or
+ /// ISLA_SECOND_SEED — labeling + threshold are not
+ /// seed-specific; the big organic masses tag correctly.
+ ///
+ /// THE COUNT/SIZE TABLE (data): per seed, natural islands (offshore off), pre-revert islands, and
+ /// post-revert islands at each threshold, with size min / median / mean / max and a log-spaced
+ /// size histogram — the instrument the later southern-stretch step tunes against.
+ ///
+ /// Every "current terrain" field = shelf ON + the chat2/06 organic preset (`density_mid`) + region
+ /// labeling ON; the revert is the variable. The curve is the tagged curve, unchanged.
+ ///
+ /// ═══ RUNNING IT ═══
+ ///
+ /// xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 \
+ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/RegionLabelingTool.tscn
+ ///
+ /// ISLA_TASK / ISLA_BATCH / ISLA_SKIP_RAW / ISLA_OUTPUT_DIR
+ /// ISLA_MAPSIZE plate + table size (default 4096)
+ /// ISLA_TABLE_SIZE count-table size (default = ISLA_MAPSIZE; a probe may drop it)
+ /// ISLA_CALIB_SIZE curve calibration size (default 2048, task 01's)
+ /// ISLA_TABLE_SEEDS the table seeds (default 8 below)
+ /// ISLA_PLATE_SEED the three-threshold seed (default 1063685222)
+ /// ISLA_SECOND_SEED the second plate seed (default 0 = auto: most natural islands)
+ /// ISLA_THR_LOW / ISLA_THR_MID / ISLA_THR_HIGH thresholds, fraction of map area (probe overrides)
+ /// ISLA_TABLE_ONLY=1 probe: table only (no regressions, no plates)
+ /// ISLA_SKIP_8K=1 skip the 8192 regression (a4)
+ /// ISLA_PHASE1_SOURCE / ISLA_T03_SOURCE / ISLA_T04_SOURCE / ISLA_T06_SOURCE the regression dumps' batches
+ ///
+ public partial class RegionLabelingTool : Node
+ {
+ private static readonly int[] DefaultTableSeeds =
+ {
+ 1063685222, 20260821, 8675309, 123456789, 271828182, 999999937, 90210, 424242,
+ };
+
+ /// ⚠ Task 01's pool, verbatim — the curve's identity.
+ private static readonly int[] CalibrationSeeds = { 1063685222, 20260819, 777001, 424242, 90210, 31337 };
+
+ private const int DefaultMapSize = 4096;
+ private const int DefaultCalibSize = 2048;
+ private const int GallerySize = 8192;
+
+ public override void _Ready()
+ {
+ try { Run(); }
+ catch (Exception e)
+ {
+ GD.PrintErr("==================================================================");
+ GD.PrintErr($" REFUSED: {e.Message}");
+ GD.PrintErr(e.StackTrace);
+ GD.PrintErr("==================================================================");
+ GetTree().Quit(2);
+ }
+ }
+
+ private sealed class Level { public string Label; public float Frac; }
+
+ private sealed class Row
+ {
+ public string Level; public int Seed; public long ThresholdCells;
+ public int Natural, NaturalN, NaturalS; // offshore OFF, revert OFF
+ public int Pre, PreN, PreS; // offshore ON, revert OFF
+ public int Post, PostN, PostS; // offshore ON, revert at this level
+ public int RevertedComps; public long RevertedCells;
+ public long PreMin, PreMed, PreMax, PostMin, PostMed, PostMax; public double PreMean, PostMean;
+ public int[] PreHist, PostHist;
+ public long MainlandCells; public bool Ok; public ulong Ms;
+ }
+
+ private void Run()
+ {
+ ToolingPaths.Configure(OS.GetUserDataDir());
+
+ int task = EnvInt("ISLA_TASK", 7);
+ string descr = EnvStr("ISLA_BATCH", "region_labeling");
+ int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
+ int tableSize = EnvInt("ISLA_TABLE_SIZE", mapSize);
+ int calibSize = EnvInt("ISLA_CALIB_SIZE", DefaultCalibSize);
+ int[] tableSeeds = EnvSeeds("ISLA_TABLE_SEEDS", DefaultTableSeeds);
+ int plateSeed = EnvInt("ISLA_PLATE_SEED", 1063685222);
+ int secondEnv = EnvInt("ISLA_SECOND_SEED", 0);
+ string p1Source = EnvStr("ISLA_PHASE1_SOURCE", "02_pass1_port");
+ string t03Source = EnvStr("ISLA_T03_SOURCE", "03_mountain_restore");
+ string t04Source = EnvStr("ISLA_T04_SOURCE", "04_seed_gallery");
+ string t06Source = EnvStr("ISLA_T06_SOURCE", "06_offshore_organic_tune");
+ bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
+ bool tableOnly = EnvStr("ISLA_TABLE_ONLY", "0") == "1";
+ bool skip8k = EnvStr("ISLA_SKIP_8K", "0") == "1";
+
+ var levels = new List
+ {
+ new() { Label = "threshold_low", Frac = EnvFloat("ISLA_THR_LOW", RegionPass.ThresholdLowFrac) },
+ new() { Label = "threshold_mid", Frac = EnvFloat("ISLA_THR_MID", RegionPass.ThresholdMidFrac) },
+ new() { Label = "threshold_high", Frac = EnvFloat("ISLA_THR_HIGH", RegionPass.ThresholdHighFrac) },
+ };
+ Level mid = levels[1];
+
+ string batchRoot = ToolingPaths.BatchRoot(task, descr);
+ DirAccess.MakeDirRecursiveAbsolute(batchRoot);
+ DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot));
+
+ var anchors = CurveAnchors.Default;
+ float sea = 0.15f;
+
+ GD.Print("==================================================================");
+ GD.Print(" REGION LABELING (chat2/07) — label all land, fix the tag, tunable speck revert");
+ GD.Print("==================================================================");
+ GD.Print($"MapSize : {mapSize} (plates) table at {tableSize} curve calibrated at {calibSize} (offshore OFF)");
+ GD.Print($"table : {string.Join(", ", tableSeeds)}");
+ GD.Print($"plate seed: {plateSeed} second seed: {(secondEnv > 0 ? secondEnv.ToString() : "auto (most natural islands)")}");
+ foreach (var l in levels) GD.Print($" {l.Label,-15} {l.Frac:G3} of map area = {Cells(l.Frac, mapSize):N0} cells at {mapSize} ({Cells(l.Frac, tableSize):N0} at {tableSize})");
+ GD.Print($"terrain : shelf ON + offshore {OffshoreSettings.Organic().Describe()}");
+ GD.Print($"contract : classify field · land 8-connected · mainland = centre component · id/size/centroid/hemisphere(centroid)/isMainland");
+ GD.Print($"batch : {batchRoot}{(tableOnly ? " ⚠ ISLA_TABLE_ONLY — a probe, not the batch of record" : "")}");
+ GD.Print("==================================================================");
+
+ // ═══ 0. THE CURVE ═══
+ GD.Print($"\n--- 0. CURVE (task-01 pool at {calibSize}, offshore off) ---");
+ var (knots, calibration) = CalibrateCurve(calibSize, sea, anchors);
+ GD.Print($" {knots}");
+ GD.Print($" {calibration.Describe()}");
+
+ TerrainGenConfig Cfg(int size, int seed, string label, bool offshoreOn, bool revertOn, float frac)
+ {
+ var c = BaseConfig(size, seed, knots, anchors, calibration, label);
+ if (offshoreOn) { c.CoastShelf = true; c.Offshore = OffshoreSettings.Organic(); }
+ c.RegionLabeling = true;
+ c.SpeckRevert = revertOn;
+ c.MinLandComponentFrac = frac;
+ return c;
+ }
+
+ // ═══ 1. REGRESSIONS ═══
+ var hard = new List();
+ if (!tableOnly)
+ {
+ GD.Print($"\n--- 1. REGRESSIONS at {calibSize}, seed {plateSeed} ---");
+ var offCfg = Cfg(calibSize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac);
+ Pass1Result p1 = Topography.Generate(offCfg);
+
+ var curveOff = offCfg.Clone(); curveOff.Curve = false;
+ Pass2Result pOff = Shaping.Shape(p1, curveOff);
+ string p1Dump = Path.Combine(ToolingPaths.BatchesRoot, p1Source, $"{plateSeed}_full", "height.f32");
+ hard.Add(ShapingOracle.DumpRegression("a1", "curve OFF, offshore OFF, revert OFF (labeling on) == Phase-1 .f32 dump",
+ pOff.Height, HeightField.Load(p1Dump, calibSize), calibSize, p1Dump));
+
+ Pass2Result pRest = Shaping.Shape(p1, offCfg);
+ string t03Dump = Path.Combine(ToolingPaths.BatchesRoot, t03Source, $"{plateSeed}_continuous_restored", "height.f32");
+ float[,] t03 = HeightField.Load(t03Dump, calibSize);
+ hard.Add(ShapingOracle.DumpRegression("a3", "continuous_restored, offshore OFF, revert OFF (labeling on) == task-03 .f32 dump",
+ pRest.Height, t03, calibSize, t03Dump));
+
+ // Informational: offshore OFF, revert ON — how many NATURAL speck cells the revert removes
+ // from the bare field. Allowed to differ (the revert may change terrain); reported, not asserted.
+ var revCfg = Cfg(calibSize, plateSeed, "off_revert", offshoreOn: false, revertOn: true, mid.Frac);
+ Pass1Result p1Rev = Topography.Generate(revCfg);
+ Pass2Result pRev = Shaping.Shape(p1Rev, revCfg);
+ var info = ShapingOracle.DumpRegression("a3r", "(informational) offshore OFF, revert ON at threshold_mid vs task-03 dump — the natural specks removed", pRev.Height, t03, calibSize, t03Dump);
+ info.Detail = (info.Passed ? "no natural speck below the threshold on this seed — " : "") + info.Detail +
+ $" · reverted {p1Rev.RegionLedger.RevertedComponents} natural components / {p1Rev.RegionLedger.RevertedCells:N0} cells";
+ info.Passed = true;
+ hard.Add(info);
+
+ var shelfCfg = offCfg.Clone(); shelfCfg.CoastShelf = true; shelfCfg.VariantLabel = "shelf_only";
+ Pass1Result p1Shelf = Topography.Generate(shelfCfg);
+ var j0 = ShapingOracle.MainlandUnmoved(p1, p1Shelf, sea);
+ j0.Name = "shelf alone: every land cell bit-identical (shelf is below-sea only)";
+ hard.Add(j0);
+ hard.Add(ShapingOracle.CentreIsLand(p1));
+ foreach (var c in hard) GD.Print(" " + c);
+
+ if (!skip8k)
+ {
+ string t04Dump = Path.Combine(ToolingPaths.BatchesRoot, t04Source, $"{plateSeed}", "height.f32");
+ if (File.Exists(t04Dump))
+ {
+ GD.Print($" a4: generating {plateSeed} at {GallerySize}, offshore OFF, revert OFF …");
+ var gCfg = Cfg(GallerySize, plateSeed, "off", offshoreOn: false, revertOn: false, mid.Frac);
+ Pass2Result pG = Shaping.Shape(Topography.Generate(gCfg), gCfg);
+ var a4 = ShapingOracle.DumpRegression("a4", $"offshore OFF, revert OFF at {GallerySize} == terrain-curve-v1's 04 gallery .f32 dump",
+ pG.Height, HeightField.Load(t04Dump, GallerySize), GallerySize, t04Dump);
+ hard.Add(a4); GD.Print(" " + a4);
+ }
+ else GD.Print($" a4: ⚠ skipped — no 04 gallery dump at {t04Dump}");
+ }
+ else GD.Print(" a4: skipped (ISLA_SKIP_8K)");
+
+ // ⭐ a6 — labeling ON, revert OFF, on the chat2/06 preset: bit-identical to the 06 batch's
+ // render field. Labeling is pure analysis; only the revert may change terrain.
+ string t06Dump = Path.Combine(ToolingPaths.BatchesRoot, t06Source, $"{plateSeed}_density_mid", "height.f32");
+ if (File.Exists(t06Dump) && mapSize == 4096)
+ {
+ var c6 = Cfg(mapSize, plateSeed, "density_mid", offshoreOn: true, revertOn: false, mid.Frac);
+ Pass2Result p6 = Shaping.Shape(Topography.Generate(c6), c6);
+ var a6 = ShapingOracle.DumpRegression("a6", "offshore density_mid ON, labeling ON, revert OFF == task-06 .f32 dump (labeling is pure analysis)",
+ p6.Height, HeightField.Load(t06Dump, mapSize), mapSize, t06Dump);
+ hard.Add(a6); GD.Print(" " + a6);
+ }
+ else GD.Print($" a6: ⚠ skipped — {(mapSize != 4096 ? "map size is not the 06 batch's 4096" : $"no 06 dump at {t06Dump}")}");
+ }
+
+ // ═══ 2. DETERMINISM ═══
+ GD.Print($"\n--- 2. DETERMINISM at {tableSize}, seed {plateSeed}, {mid.Label} ---");
+ var perFieldChecks = new List();
+ {
+ var cA = Cfg(tableSize, plateSeed, mid.Label, true, true, mid.Frac);
+ var cB = Cfg(tableSize, plateSeed, mid.Label, true, true, mid.Frac);
+ var det = ShapingOracle.LabelsDeterministic(Topography.Generate(cA), Topography.Generate(cB));
+ det.Name += $" [{plateSeed}]";
+ perFieldChecks.Add(det); GD.Print(" " + det);
+ }
+
+ // ═══ 3. THE COUNT/SIZE TABLE ═══
+ GD.Print($"\n--- 3. COUNT/SIZE TABLE at {tableSize} ---");
+ var rows = new List();
+ var naturalCount = new Dictionary();
+ bool notesShown = false;
+ foreach (int seed in tableSeeds)
+ {
+ // natural: offshore OFF, revert OFF
+ Pass1Result pNat = Topography.Generate(Cfg(tableSize, seed, "natural", false, false, mid.Frac));
+ var (natN, natS) = RegionLabeling.IslandsByHemisphere(pNat.Regions);
+ naturalCount[seed] = pNat.Regions.IslandCount;
+ // pre: offshore ON, revert OFF
+ var cPre = Cfg(tableSize, seed, "pre", true, false, mid.Frac);
+ Pass1Result pPre = Topography.Generate(cPre);
+ {
+ var cj = ShapingOracle.MainlandUnmoved(pNat, pPre, sea); cj.Name += $" [offshore on vs off, {seed}]"; perFieldChecks.Add(cj);
+ var cm = ShapingOracle.CentreIsLand(pPre); cm.Name += $" [pre {seed}]"; perFieldChecks.Add(cm);
+ }
+ if (!notesShown) { foreach (string n in pPre.Notes) GD.Print(" " + n); }
+ GD.Print($" seed {seed,-11} natural islands {pNat.Regions.IslandCount,3} (N {natN} / S {natS}) pre-revert {pPre.Regions.IslandCount,3} (N {pPre.RegionLedger.PostNorth} / S {pPre.RegionLedger.PostSouth}) mainland {pPre.Regions.Mainland.SizeCells:N0} cells");
+
+ foreach (var lv in levels)
+ {
+ var cfg = Cfg(tableSize, seed, lv.Label, true, true, lv.Frac);
+ Pass1Result p1 = Topography.Generate(cfg);
+ Pass2Result p2 = Shaping.Shape(p1, cfg);
+ if (!notesShown) { foreach (string n in p1.Notes) if (n.StartsWith("[Regions]")) GD.Print(" " + n); notesShown = true; }
+ var led = p1.RegionLedger;
+ long thr = led.ThresholdCells;
+ var comps = OffshoreAnalysis.Components(p1.IsIsland, p1.Height, sea, tableSize);
+ var checks = new List
+ {
+ ShapingOracle.CentreIsLand(p1),
+ ShapingOracle.RevertGuards(pPre, p1, sea, thr),
+ ShapingOracle.MoatIntact(p1, comps),
+ ShapingOracle.TagCoastlineConsistent(p2, sea),
+ ShapingOracle.HMaxAfterOffshore(p1),
+ ShapingOracle.ClassifyFidelity(p1, p2),
+ };
+ foreach (var c in checks) { c.Name += $" [{lv.Label} {seed}]"; perFieldChecks.Add(c); }
+ bool ok = checks.TrueForAll(c => c.Passed);
+ var row = new Row
+ {
+ Level = lv.Label, Seed = seed, ThresholdCells = thr,
+ Natural = pNat.Regions.IslandCount, NaturalN = natN, NaturalS = natS,
+ Pre = led.PreIslands, PreN = led.PreNorth, PreS = led.PreSouth,
+ Post = led.PostIslands, PostN = led.PostNorth, PostS = led.PostSouth,
+ RevertedComps = led.RevertedComponents, RevertedCells = led.RevertedCells,
+ PreMin = led.PreMin, PreMed = led.PreMedian, PreMean = led.PreMean, PreMax = led.PreMax,
+ PostMin = led.PostMin, PostMed = led.PostMedian, PostMean = led.PostMean, PostMax = led.PostMax,
+ PreHist = led.PreHistogram, PostHist = led.PostHistogram,
+ MainlandCells = p1.Regions.Mainland.SizeCells, Ok = ok, Ms = p1.ElapsedMs,
+ };
+ rows.Add(row);
+ GD.Print($" {lv.Label,-15} seed {seed,-11} thr {thr,5} pre {row.Pre,3} → post {row.Post,3} (N {row.PostN,2} / S {row.PostS,2}) reverted {row.RevertedComps,3} comps / {row.RevertedCells,7:N0} cells " +
+ $"post size min {row.PostMin,5} med {row.PostMed,5} max {row.PostMax,6} {(ok ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
+ }
+ }
+
+ // ═══ 4. THE PLATES ═══
+ int secondSeed = secondEnv > 0 ? secondEnv : PickSecondSeed(naturalCount, tableSeeds, plateSeed);
+ GD.Print($"\n second seed: {secondSeed}{(secondEnv > 0 ? " (ISLA_SECOND_SEED)" : $" (auto: most natural islands among the table seeds — {naturalCount.GetValueOrDefault(secondSeed)})")}");
+ var plateRows = new List();
+ if (!tableOnly)
+ {
+ GD.Print($"\n--- 4. PLATES at {mapSize} ---");
+ var plates = new List<(int seed, Level lv)> { (plateSeed, levels[0]), (plateSeed, levels[1]), (plateSeed, levels[2]), (secondSeed, mid) };
+ foreach (var (seed, lv) in plates)
+ {
+ var cfg = Cfg(mapSize, seed, lv.Label, true, true, lv.Frac);
+ Pass1Result p1 = Topography.Generate(cfg);
+ Pass2Result p2 = Shaping.Shape(p1, cfg);
+ var led = p1.RegionLedger;
+ var cm = ShapingOracle.CentreIsLand(p1); cm.Name += $" [plate {lv.Label} {seed}]";
+ var ck = ShapingOracle.TagCoastlineConsistent(p2, sea); ck.Name += $" [plate {lv.Label} {seed}]";
+ perFieldChecks.Add(cm); perFieldChecks.Add(ck);
+ WritePlate(batchRoot, p1, p2, sea, anchors, skipRaw);
+ plateRows.Add(new Row
+ {
+ Level = lv.Label, Seed = seed, ThresholdCells = led.ThresholdCells,
+ Pre = led.PreIslands, PreN = led.PreNorth, PreS = led.PreSouth, Post = led.PostIslands, PostN = led.PostNorth, PostS = led.PostSouth,
+ RevertedComps = led.RevertedComponents, RevertedCells = led.RevertedCells,
+ PostMin = led.PostMin, PostMed = led.PostMedian, PostMean = led.PostMean, PostMax = led.PostMax,
+ MainlandCells = p1.Regions.Mainland.SizeCells, Ok = cm.Passed && ck.Passed, Ms = p1.ElapsedMs,
+ });
+ GD.Print($" plate {seed}_{lv.Label}: pre {led.PreIslands} → post {led.PostIslands} (N {led.PostNorth} / S {led.PostSouth}), reverted {led.RevertedComponents} comps {(cm.Passed && ck.Passed ? "ok" : "⚠ CHECK FAILED")} {p1.ElapsedMs} ms");
+ }
+ }
+
+ bool allOk = hard.TrueForAll(c => c.Passed) && perFieldChecks.TrueForAll(c => c.Passed);
+ GD.Print($"\n ORACLE: {(allOk ? "ALL HARD CHECKS PASS" : "*** FAILURES ***")}");
+ foreach (var c in perFieldChecks) if (!c.Passed) GD.PrintErr(" " + c);
+
+ WriteTable(batchRoot, tableSize, levels, rows);
+ WriteIndex(batchRoot, mapSize, tableSize, calibSize, plateSeed, secondSeed, tableSeeds, levels, rows, plateRows, hard, perFieldChecks, allOk, tableOnly);
+
+ GD.Print("\n==================================================================");
+ GD.Print($" DONE — {batchRoot}");
+ GD.Print($" ORACLE {(allOk ? "HARD CHECKS ALL PASS" : "*** FAILURES — see the table ***")}");
+ GD.Print("==================================================================");
+ GetTree().Quit(allOk ? 0 : 3);
+ }
+
+ private static long Cells(float frac, int size) => Math.Max(1L, (long)Math.Round(frac * (double)size * size));
+
+ private static int PickSecondSeed(Dictionary natural, int[] seeds, int plateSeed)
+ {
+ int best = 0, bestN = -1;
+ foreach (int s in seeds)
+ {
+ if (s == plateSeed) continue;
+ int n = natural.GetValueOrDefault(s);
+ if (n > bestN) { best = s; bestN = n; }
+ }
+ return best == 0 ? plateSeed : best;
+ }
+
+ // ---- the curve, measured exactly as tasks 03–06 did --------------------
+
+ private static (CurveKnots, ClimbCalibration) CalibrateCurve(int calibSize, float sea, CurveAnchors anchors)
+ {
+ var rawPool = new LandHistogram(sea);
+ var pass1 = new Dictionary();
+ foreach (int s in CalibrationSeeds)
+ {
+ var p1 = Topography.Generate(new TerrainGenConfig { MapSize = calibSize, Seed = s }); // offshore OFF, revert OFF by default
+ pass1[s] = p1;
+ rawPool.Accumulate(p1.Height, calibSize);
+ }
+ var knots = new CurveKnots(2, "v2_balanced",
+ rawPool.Quantile(CurveKnots.Percentiles[0]), rawPool.Quantile(CurveKnots.Percentiles[1]),
+ rawPool.Quantile(CurveKnots.Percentiles[2]), rawPool.Quantile(CurveKnots.Percentiles[3]),
+ rawPool.Quantile(CurveKnots.Percentiles[4]), rawPool.Quantile(CurveKnots.Percentiles[5]));
+
+ float ceilingRaw = knots.K2;
+ var rawAbove = new LandHistogram(sea);
+ var outAbove = new LandHistogram(sea);
+ foreach (int s in CalibrationSeeds)
+ {
+ var scfg = new TerrainGenConfig
+ {
+ MapSize = calibSize, Seed = s, Curve = true, ShelfDetail = true,
+ CurveMode = CurveModeKind.Staircase, Knots = knots, Anchors = anchors, VariantLabel = "staircase",
+ };
+ Pass2Result st = Shaping.Shape(pass1[s], scfg);
+ rawAbove.AccumulateWhere(pass1[s].Height, pass1[s].Height, calibSize, ceilingRaw);
+ outAbove.AccumulateWhere(st.Height, pass1[s].Height, calibSize, ceilingRaw);
+ }
+ var pcts = ClimbCalibration.DefaultPercentiles;
+ var rawQ = new float[pcts.Length];
+ var outQ = new float[pcts.Length];
+ for (int i = 0; i < pcts.Length; i++) { rawQ[i] = rawAbove.Quantile(pcts[i]); outQ[i] = outAbove.Quantile(pcts[i]); }
+
+ var cal = ClimbCalibration.FromPercentiles(pcts, rawQ, outQ, ceilingRaw,
+ HeightCurve.EffectiveSpikeMax(pass1[CalibrationSeeds[0]].HMaxSeed, knots, anchors),
+ anchors.RedCeil, anchors.PeakCap, mountainLift: 1.0f, peakSharpness: 1.0f);
+ return (knots, cal);
+ }
+
+ private static TerrainGenConfig BaseConfig(int mapSize, int seed, CurveKnots k, CurveAnchors a,
+ ClimbCalibration cal, string label) => new TerrainGenConfig
+ {
+ MapSize = mapSize, Seed = seed, VariantLabel = label,
+ Curve = true, ShelfDetail = false, CurveMode = CurveModeKind.Continuous,
+ Knots = k, Anchors = a, ClimbCalibration = cal, LowlandCeilingM = 30f,
+ CoastShelf = false, Offshore = new OffshoreSettings(), // OFF unless the variant turns it on
+ };
+
+ // ---- output -----------------------------------------------------------
+
+ private static void WritePlate(string batchRoot, Pass1Result p1, Pass2Result p2, float sea, CurveAnchors anchors, bool skipRaw)
+ {
+ string dir = Path.Combine(batchRoot, $"{p2.Seed}_{p2.VariantLabel}");
+ DirAccess.MakeDirRecursiveAbsolute(dir);
+
+ GrayscaleRenderer.SavePng(p2.Height, p2.MapSize, Path.Combine(dir, "grayscale.png"));
+ if (!skipRaw) HeightField.Save(p2.Height, p2.MapSize, Path.Combine(dir, "height.f32"));
+
+ var look = new LookConfig
+ {
+ Name = "hillshade_even", Palette = ReliefPalette.Kind.ProvisionalEven,
+ ZExaggeration = 18f, LightAzimuth = 315f, LightAltitude = 45f,
+ HillshadeStrength = 0.30f, SeaLevel = sea,
+ };
+ Image map = ReliefRenderer.Render(p2.Height, p2.MapSize, look);
+ LegendRenderer.WithLegend(map, look.Palette, sea, anchors.PeakCap, $"{p2.VariantLabel.ToUpperInvariant()} {p2.Seed}")
+ .SavePng(Path.Combine(dir, "relief.png"));
+
+ // ⭐ The labeled-regions overlay — the point of this task.
+ var led = p1.RegionLedger;
+ RegionOverlayRenderer.SavePng(p1.Regions, led.RevertOn ? p1.RegionsPre : null, p1.MapSize,
+ led.RevertedComponents, led.ThresholdCells, Path.Combine(dir, "regions.png"));
+
+ // The hemisphere tag overlay (chat2/05's), now showing the tag by construction.
+ var (n, s) = RegionLabeling.IslandsByHemisphere(p1.Regions);
+ TagOverlayRenderer.SavePng(p2.Height, p2.IsIsland, p2.IslandHemisphere, p2.MapSize, sea, n, s, Path.Combine(dir, "tags.png"));
+ }
+
+ private static string HistRow(int[] h)
+ {
+ if (h == null) return "—";
+ var sb = new StringBuilder();
+ for (int i = 0; i < h.Length; i++) { if (i > 0) sb.Append(" · "); sb.Append(h[i]); }
+ return sb.ToString();
+ }
+
+ private static string TableMarkdown(List levels, List rows, int tableSize)
+ {
+ var sb = new StringBuilder();
+ var histHead = new StringBuilder();
+ for (int i = 0; i <= RegionLabeling.HistogramEdges.Length; i++) { if (i > 0) histHead.Append(" · "); histHead.Append(RegionLabeling.HistogramLabel(i)); }
+ sb.AppendLine($"| Level | Seed | threshold (cells) | natural islands (offshore off) N / S | pre-revert islands N / S | **post-revert islands N / S** | reverted comps / cells | pre size min / med / mean / max | **post size min / med / mean / max** | post histogram ({histHead}) | mainland cells | oracle |");
+ sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|---|");
+ foreach (var lv in levels)
+ foreach (var r in rows)
+ {
+ if (r.Level != lv.Label) continue;
+ sb.AppendLine($"| `{r.Level}` | `{r.Seed}` | {r.ThresholdCells:N0} | {r.Natural} ({r.NaturalN} / {r.NaturalS}) | {r.Pre} ({r.PreN} / {r.PreS}) | **{r.Post} ({r.PostN} / {r.PostS})** | {r.RevertedComps} / {r.RevertedCells:N0} | " +
+ $"{r.PreMin} / {r.PreMed} / {r.PreMean:F0} / {r.PreMax} | **{r.PostMin} / {r.PostMed} / {r.PostMean:F0} / {r.PostMax}** | {HistRow(r.PostHist)} | {r.MainlandCells:N0} | {(r.Ok ? "pass" : "**FAIL**")} |");
+ }
+ sb.AppendLine();
+ sb.AppendLine("**Per level (over the seeds):**");
+ sb.AppendLine();
+ sb.AppendLine("| Level | threshold | post islands min / mean / max | post N min / mean / max | post S min / mean / max | reverted comps (total) | reverted cells (total) | post median island (median over seeds) | smallest surviving island |");
+ sb.AppendLine("|---|---|---|---|---|---|---|---|---|");
+ foreach (var lv in levels)
+ {
+ int cnt = 0, minP = int.MaxValue, maxP = 0, minN = int.MaxValue, maxN = 0, minS = int.MaxValue, maxS = 0; double sumP = 0, sumN = 0, sumS = 0;
+ long revC = 0, revCells = 0, smallest = long.MaxValue; var meds = new List(); long thr = 0;
+ foreach (var r in rows)
+ {
+ if (r.Level != lv.Label) continue;
+ cnt++; thr = r.ThresholdCells;
+ minP = Math.Min(minP, r.Post); maxP = Math.Max(maxP, r.Post); sumP += r.Post;
+ minN = Math.Min(minN, r.PostN); maxN = Math.Max(maxN, r.PostN); sumN += r.PostN;
+ minS = Math.Min(minS, r.PostS); maxS = Math.Max(maxS, r.PostS); sumS += r.PostS;
+ revC += r.RevertedComps; revCells += r.RevertedCells; meds.Add(r.PostMed);
+ if (r.Post > 0) smallest = Math.Min(smallest, r.PostMin);
+ }
+ if (cnt == 0) continue;
+ meds.Sort();
+ sb.AppendLine($"| `{lv.Label}` | {lv.Frac:G3} = {thr:N0} cells | {minP} / {sumP / cnt:F1} / {maxP} | {minN} / {sumN / cnt:F1} / {maxN} | {minS} / {sumS / cnt:F1} / {maxS} | {revC} | {revCells:N0} | {meds[meds.Count / 2]} | {(smallest == long.MaxValue ? 0 : smallest)} |");
+ }
+ return sb.ToString();
+ }
+
+ private static void WriteTable(string batchRoot, int tableSize, List levels, List rows)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"# The count/size table — {rows.Count / Math.Max(1, levels.Count)} seeds × {levels.Count} revert thresholds at {tableSize}");
+ sb.AppendLine();
+ sb.AppendLine("Islands = non-mainland 8-connected land components of the CLASSIFY field (mainland = the centre component).");
+ sb.AppendLine("*natural* = offshore off, revert off; *pre-revert* = offshore `density_mid` on, revert off; *post-revert* = the same with");
+ sb.AppendLine("the speck revert on at the level's threshold. Sizes in cells. Histogram bins are cells, log-spaced.");
+ sb.AppendLine();
+ sb.Append(TableMarkdown(levels, rows, tableSize));
+ WriteText(Path.Combine(batchRoot, "count_size_table.md"), sb.ToString());
+
+ var csv = new StringBuilder();
+ csv.AppendLine("level,seed,threshold_cells,natural,natural_n,natural_s,pre,pre_n,pre_s,post,post_n,post_s,reverted_comps,reverted_cells,pre_min,pre_median,pre_mean,pre_max,post_min,post_median,post_mean,post_max,post_hist,mainland_cells,oracle,ms");
+ var ic = System.Globalization.CultureInfo.InvariantCulture;
+ foreach (var r in rows)
+ csv.AppendLine(string.Join(",", r.Level, r.Seed, r.ThresholdCells, r.Natural, r.NaturalN, r.NaturalS, r.Pre, r.PreN, r.PreS, r.Post, r.PostN, r.PostS,
+ r.RevertedComps, r.RevertedCells, r.PreMin, r.PreMed, r.PreMean.ToString("F1", ic), r.PreMax, r.PostMin, r.PostMed, r.PostMean.ToString("F1", ic), r.PostMax,
+ "\"" + HistRow(r.PostHist) + "\"", r.MainlandCells, r.Ok ? "pass" : "FAIL", r.Ms));
+ WriteText(Path.Combine(batchRoot, "count_size_table.csv"), csv.ToString());
+ }
+
+ private static void WriteIndex(string batchRoot, int mapSize, int tableSize, int calibSize, int plateSeed, int secondSeed,
+ int[] tableSeeds, List levels, List rows, List plateRows,
+ List hard, List perField, bool allOk, bool tableOnly)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("# Batch 07 — region labeling: label all land, fix the tag, tunable speck revert");
+ sb.AppendLine();
+ sb.AppendLine("The **region-labeling layer** (`Core.RegionLabeling`) flood-fills the CLASSIFY field's land into 8-connected");
+ sb.AppendLine("components, names the **centre component** the mainland, and exposes id / size / centroid / hemisphere (by");
+ sb.AppendLine("centroid) / isMainland. The **island tag is now a consequence of labeling** — every non-mainland component,");
+ sb.AppendLine("natural detached masses included. The **speck revert** (origin-blind, lower-only, component-only, mainland never)");
+ sb.AppendLine("lowers sub-threshold islands to their ring's seabed; the threshold is the dial swept here.");
+ sb.AppendLine();
+ if (tableOnly) sb.AppendLine("> ⚠ **ISLA_TABLE_ONLY** — a probe run: table only, no regressions, no plates. Not the batch of record.\n");
+ sb.AppendLine("## ⭐ Open this first");
+ sb.AppendLine();
+ sb.AppendLine($"1. **`{plateSeed}_threshold_mid/regions.png`** — the labeled-regions overlay: grey = mainland (the centre component),");
+ sb.AppendLine(" every island its own colour, dark red = where a reverted speck was. Then `relief.png` for the clean ocean.");
+ sb.AppendLine($"2. **`{plateSeed}_threshold_low/`** and **`{plateSeed}_threshold_high/`** beside it — same seed, lower / higher cutoff.");
+ sb.AppendLine($"3. **`{secondSeed}_threshold_mid/regions.png`** + `tags.png` — the second seed (most natural islands): the big organic");
+ sb.AppendLine(" detached masses are labeled and TAGGED (cyan / orange), which task 06's overlay left grey.");
+ sb.AppendLine("4. Then the count/size table — the instrument for the later southern-stretch step.");
+ sb.AppendLine();
+ sb.AppendLine("**The contract (verbatim):** field = classify (raw, uncurved) · land 8-connected (the complement of water's 4) ·");
+ sb.AppendLine("component = maximal 8-connected set of land cells (classify ≥ sea) · mainland = the component containing the map");
+ sb.AppendLine("centre (not merely the largest; the crater is NOT central) · per component: id, sizeCells, centroid, hemisphere");
+ sb.AppendLine($"(by centroid), isMainland. NORTH = rows `[0, {mapSize / 2})`, SOUTH = rows `[{mapSize / 2}, {mapSize})`; y runs south.");
+ sb.AppendLine();
+ sb.AppendLine("## The four plates");
+ sb.AppendLine();
+ sb.AppendLine("| Plate | threshold (cells) | pre-revert islands N / S | **post-revert islands N / S** | reverted comps / cells | post size min / med / mean / max | mainland cells | oracle |");
+ sb.AppendLine("|---|---|---|---|---|---|---|---|");
+ foreach (var r in plateRows)
+ sb.AppendLine($"| `{r.Seed}_{r.Level}/` | {r.ThresholdCells:N0} | {r.Pre} ({r.PreN} / {r.PreS}) | **{r.Post} ({r.PostN} / {r.PostS})** | {r.RevertedComps} / {r.RevertedCells:N0} | {r.PostMin} / {r.PostMed} / {r.PostMean:F0} / {r.PostMax} | {r.MainlandCells:N0} | {(r.Ok ? "pass" : "**FAIL**")} |");
+ if (plateRows.Count == 0) sb.AppendLine("| *(no plates — probe run)* | | | | | | | |");
+ sb.AppendLine();
+ sb.AppendLine($"## ⭐ The count/size table — {tableSeeds.Length} seeds × 3 thresholds at {tableSize}");
+ sb.AppendLine();
+ sb.Append(TableMarkdown(levels, rows, tableSize));
+ sb.AppendLine();
+ sb.AppendLine("Also as plain data: `count_size_table.md` / `.csv`.");
+ sb.AppendLine();
+ sb.AppendLine("## The levels");
+ sb.AppendLine();
+ sb.AppendLine("| Level | `MinLandComponentFrac` | cells at the plate size | cells at 8192 |");
+ sb.AppendLine("|---|---|---|---|");
+ foreach (var lv in levels) sb.AppendLine($"| `{lv.Label}`{(lv.Label == "threshold_mid" ? " ⭐ config default" : "")} | {lv.Frac:G3} | {Cells(lv.Frac, mapSize):N0} | {Cells(lv.Frac, 8192):N0} |");
+ sb.AppendLine();
+ sb.AppendLine($"Every field: coast shelf ON + offshore `{OffshoreSettings.Organic().Describe()}` + region labeling ON. The revert is the variable.");
+ sb.AppendLine("The revert is **origin-blind**: it removes small natural nubs as well as offshore-pass dots (fewer / bigger, intended). An offshore");
+ sb.AppendLine("island it removes leaves its submerged skirt (not this component — component-only) as a shoal.");
+ sb.AppendLine();
+ sb.AppendLine("## ⚠ The palette is PROVISIONAL");
+ sb.AppendLine();
+ sb.AppendLine("`ProvisionalEven`, flagged. The individually-coloured scheme is ONLY the `regions.png` overlay.");
+ sb.AppendLine();
+ sb.AppendLine("## The oracle");
+ sb.AppendLine();
+ sb.AppendLine("Regressions (offshore OFF + revert OFF must be bit-identical to Phase 1, task 03 and the `terrain-curve-v1` gallery dump; labeling ON + revert OFF bit-identical to the task-06 dump):");
+ sb.AppendLine();
+ sb.AppendLine(hard.Count == 0 ? "*(skipped — probe run)*\n" : ShapingOracle.ToMarkdownTable(hard));
+ sb.AppendLine("Per field (centre-is-land m · revert guards n · determinism o · moat i · mainland unmoved j · tag/coastline k · HMaxSeed l · classify b):");
+ sb.AppendLine();
+ sb.AppendLine(ShapingOracle.ToMarkdownTable(perField));
+ sb.AppendLine($"**{(allOk ? "ALL HARD CHECKS PASS" : "⚠⚠ FAILURES — do not judge this batch")}**");
+ sb.AppendLine();
+ sb.AppendLine("## Disposability");
+ sb.AppendLine();
+ sb.AppendLine("| Artifact | Keep? |");
+ sb.AppendLine("|---|---|");
+ sb.AppendLine("| `regions.png`, `tags.png`, `relief.png`, `INDEX.md`, `count_size_table.md` / `.csv` | **keep** |");
+ sb.AppendLine("| `grayscale.png` | ♻ regenerable from the `.f32` |");
+ sb.AppendLine("| `height.f32` | ♻ regenerable from seed + code — large, clear freely |");
+ sb.AppendLine("| `scratch/` | persistent by rule; never cleaned |");
+ sb.AppendLine();
+ sb.AppendLine($"Plates at {mapSize}, table at {tableSize}, curve calibrated at {calibSize} with offshore off. {WorldScale.Describe()}.");
+ WriteText(Path.Combine(batchRoot, "INDEX.md"), sb.ToString());
+ }
+
+ private static void WriteText(string path, string text)
+ {
+ using var f = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Write);
+ if (f == null) { GD.PrintErr($"could not write {path}"); return; }
+ f.StoreString(text);
+ }
+
+ // ---- env helpers --------------------------------------------------------
+
+ private static string EnvStr(string k, string fallback)
+ {
+ string v = System.Environment.GetEnvironmentVariable(k);
+ return string.IsNullOrWhiteSpace(v) ? fallback : v;
+ }
+
+ private static int EnvInt(string k, int fallback)
+ => int.TryParse(EnvStr(k, null) ?? "", out int v) ? v : fallback;
+
+ private static float EnvFloat(string k, float fallback)
+ => float.TryParse(EnvStr(k, null) ?? "", System.Globalization.NumberStyles.Float,
+ System.Globalization.CultureInfo.InvariantCulture, out float v) ? v : fallback;
+
+ private static int[] EnvSeeds(string k, int[] fallback)
+ {
+ string v = EnvStr(k, null);
+ if (v == null) return fallback;
+ var outp = new List();
+ foreach (string part in v.Split(',', StringSplitOptions.RemoveEmptyEntries))
+ if (int.TryParse(part.Trim(), out int s) && s > 0) outp.Add(s);
+ return outp.Count > 0 ? outp.ToArray() : fallback;
+ }
+ }
+}
diff --git a/Tools/Scripts/RegionLabelingTool.cs.uid b/Tools/Scripts/RegionLabelingTool.cs.uid
new file mode 100644
index 0000000..2e877df
--- /dev/null
+++ b/Tools/Scripts/RegionLabelingTool.cs.uid
@@ -0,0 +1 @@
+uid://ceeyncymgtjpm
diff --git a/Tools/Scripts/RegionOverlayRenderer.cs b/Tools/Scripts/RegionOverlayRenderer.cs
new file mode 100644
index 0000000..71adea0
--- /dev/null
+++ b/Tools/Scripts/RegionOverlayRenderer.cs
@@ -0,0 +1,77 @@
+using System;
+using Godot;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ ///
+ /// THE LABELED-REGIONS OVERLAY (chat2/07): the mainland one tint, EACH island component an
+ /// individually distinct colour, the components the speck revert removed in a dim red, the midline
+ /// drawn — so the developer can SEE that components are identified correctly and catch an
+ /// 8-connectivity mislabel (two touching blobs coloured as one, one mass coloured as two).
+ ///
+ /// ⚠ A DIAGNOSTIC, NOT A MAP. It draws the region layer's id map, which is DATA. No hypsometry, no
+ /// hillshade — flat tints on purpose. The relief / grayscale plates stay on the provisional palette.
+ /// Presentation only: it is handed arrays and returns a PNG.
+ ///
+ public static class RegionOverlayRenderer
+ {
+ private static readonly Color Sea = new(0.055f, 0.110f, 0.235f);
+ private static readonly Color Mainland = new(0.340f, 0.380f, 0.330f);
+ private static readonly Color Reverted = new(0.420f, 0.080f, 0.080f); // where a reverted speck WAS (now sea)
+ private static readonly Color Midline = new(0.700f, 0.720f, 0.760f);
+ private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
+
+ /// A distinct, saturated colour per island id — golden-angle hue walk, three value steps.
+ public static Color IslandColor(int id)
+ {
+ float hue = (id * 137.508f) % 360f / 360f;
+ float val = 0.70f + 0.15f * (id % 3);
+ float sat = 0.85f - 0.15f * ((id / 3) % 2);
+ return Color.FromHsv(hue, sat, val);
+ }
+
+ /// The finished field's labeling (post-revert).
+ /// The pre-revert labeling, or null — its reverted components are painted .
+ public static void SavePng(RegionLabels labels, RegionLabels labelsPre, int mapSize, int revertedCount, long thresholdCells,
+ string absolutePath)
+ {
+ var img = Image.CreateEmpty(mapSize, mapSize, false, Image.Format.Rgb8);
+ int n = mapSize;
+ for (int x = 0; x < n; x++)
+ {
+ for (int y = 0; y < n; y++)
+ {
+ int id = labels.Id[x * n + y];
+ Color c;
+ if (id == 0)
+ {
+ c = Sea;
+ if (labelsPre != null)
+ {
+ int pid = labelsPre.Id[x * n + y];
+ if (pid != 0 && pid != labelsPre.MainlandId) c = Reverted; // was land, was not mainland, is sea now
+ }
+ }
+ else if (id == labels.MainlandId) c = Mainland;
+ else c = IslandColor(id);
+ img.SetPixel(x, y, c);
+ }
+ }
+
+ int mid = n / 2;
+ for (int x = 0; x < n; x += 3) img.SetPixel(x, mid, Midline);
+
+ var (north, south) = RegionLabeling.IslandsByHemisphere(labels);
+ int s = n >= 4096 ? 4 : 3;
+ int lh = TinyFont.Height(s) + 6;
+ TinyFont.Draw(img, "LABELED REGIONS - CLASSIFY FIELD, LAND 8-CONNECTED", 12, 12, s, Ink);
+ TinyFont.Draw(img, $"GREY: MAINLAND (CENTRE COMPONENT{(labels.CentreWasLand ? "" : " - FALLBACK, CENTRE NOT LAND")}) EACH ISLAND: ITS OWN COLOUR", 12, 12 + lh, s, Ink);
+ TinyFont.Draw(img, $"ISLANDS: {labels.IslandCount} ({north} N / {south} S BY CENTROID) DARK RED: {revertedCount} REVERTED < {thresholdCells} CELLS", 12, 12 + lh * 2, s, Ink);
+ TinyFont.Draw(img, "N ABOVE THE LINE - S BELOW - Y RUNS SOUTH", 12, 12 + lh * 3, s, Ink);
+
+ Error err = img.SavePng(absolutePath);
+ if (err != Error.Ok) GD.PrintErr($"[RegionOverlayRenderer] SavePng failed ({err}) for {absolutePath}");
+ }
+ }
+}
diff --git a/Tools/Scripts/RegionOverlayRenderer.cs.uid b/Tools/Scripts/RegionOverlayRenderer.cs.uid
new file mode 100644
index 0000000..91edb93
--- /dev/null
+++ b/Tools/Scripts/RegionOverlayRenderer.cs.uid
@@ -0,0 +1 @@
+uid://c05blbudak0as
diff --git a/Tools/Scripts/RegionPass.cs b/Tools/Scripts/RegionPass.cs
new file mode 100644
index 0000000..b700823
--- /dev/null
+++ b/Tools/Scripts/RegionPass.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Collections.Generic;
+using IslaApocalypse.Core;
+
+namespace IslaApocalypse.Tools
+{
+ /// The region pass's numbers, carried on Pass1Result for the report.
+ public sealed class RegionLedger
+ {
+ public bool RevertOn;
+ public long ThresholdCells;
+ public int PreIslands, PreNorth, PreSouth; // before the speck revert
+ public int PostIslands, PostNorth, PostSouth; // after
+ public int RevertedComponents; public long RevertedCells;
+ public bool CentreWasLand = true, CentreWasLandPre = true;
+ public long PreMin, PreMedian, PreMax, PostMin, PostMedian, PostMax; public double PreMean, PostMean;
+ public int[] PreHistogram, PostHistogram;
+ public List<(int id, long cells, byte hemi, float newHeight)> Reverted = new();
+ }
+
+ ///
+ /// ⭐ PASS 1c — REGION LABELING + THE SPECK REVERT + THE ISLAND TAG (chat2/07). Runs over the
+ /// finished pass-1/1b classify field, IN PLACE, after the shelf/islets and before HMaxSeed
+ /// is retaken and anything classifies.
+ ///
+ /// ═══ WHAT IT DOES, IN ORDER ═══
+ ///
+ /// 1. LABEL over the classify field — the general layer.
+ /// Pure analysis: no height changes. (The pre-revert table is kept for the instrument.)
+ /// 2. REVERT (config-gated: TerrainGenConfig.SpeckRevert; threshold
+ /// MinLandComponentFrac × map area) — every NON-MAINLAND component below the
+ /// threshold is lowered to seabed. ORIGIN-BLIND: it judges components by size, not by
+ /// who made them — a small natural nub goes the same way as an offshore-pass dot.
+ /// Fewer, bigger. Two guards, ASSERTED per component, hard failure on violation:
+ /// LOWER-ONLY — every touched cell goes DOWN (land → below sea), never up;
+ /// COMPONENT-ONLY — only cells of the sub-threshold component are touched, never a
+ /// neighbour (the submerged skirt an offshore island leaves behind
+ /// is NOT this component and stays — a shoal, by the rule).
+ /// Together they make it impossible for "revert" to move the mainland coast.
+ /// MAINLAND IS NEVER A CANDIDATE (asserted), however small a pathological seed made it.
+ /// The seabed a cell is lowered to is the MEAN height of the component's adjacent sea
+ /// cells (its ring), held strictly below sea by BitDecrement — a flat shoal at the
+ /// local depth, not a pit and not a reef.
+ /// 3. RELABEL after a revert the layer is run again, so the exposed table and ids are those of
+ /// the finished field.
+ /// 4. TAG BY CONSTRUCTION: every cell of every non-mainland component is an island cell,
+ /// hemisphere from its component's centroid. The big organic detached masses are
+ /// tagged the same as an offshore-pass dot. Nothing here knows which pass made a cell.
+ ///
+ /// The classify field IS the pass-1 array; pass 2 derives the render field from it and the curve is
+ /// identity at and below sea, so a reverted cell is seabed in both — asserted downstream by oracle (k).
+ ///
+ public static class RegionPass
+ {
+ public sealed class Result
+ {
+ public RegionLabels LabelsPre; // before the revert (== Labels when the revert is off or reverted nothing)
+ public RegionLabels Labels; // the finished field's labeling
+ public bool[,] IsIsland; // the tag, by construction
+ public byte[,] IslandHemisphere;
+ public RegionLedger Ledger = new();
+ public List Notes = new();
+ }
+
+ /// The three thresholds of the chat2/07 batch, fractions of the map's area; Mid is the config default.
+ public const float ThresholdLowFrac = 1e-5f; // 168 cells at 4096 — only the smallest natural specks
+ public const float ThresholdMidFrac = 3e-5f; // 503 cells at 4096 — the offshore pass's own speck guard, applied to all land
+ public const float ThresholdHighFrac = 1e-4f; // 1,678 cells at 4096 — "fewer, bigger": takes small offshore islands too
+
+ public static Result Apply(float[,] height, int mapSize, float sea, TerrainGenConfig cfg)
+ {
+ var r = new Result();
+ var pre = RegionLabeling.Label(height, mapSize, sea);
+ r.LabelsPre = pre;
+ var led = r.Ledger;
+ led.CentreWasLandPre = pre.CentreWasLand;
+ FillPre(led, pre);
+ r.Notes.Add($"[Regions] labeled {pre.Regions.Count} land components ({pre.LandCells:N0} land cells): mainland id {pre.MainlandId} " +
+ $"({(pre.Mainland == null ? 0 : pre.Mainland.SizeCells):N0} cells, centre {(pre.CentreWasLand ? "is land" : "⚠ NOT LAND — fell back to the largest component")}), " +
+ $"{pre.IslandCount} islands (N {led.PreNorth} / S {led.PreSouth}); island cells min {led.PreMin} median {led.PreMedian} mean {led.PreMean:F0} max {led.PreMax}.");
+
+ RegionLabels final = pre;
+ led.RevertOn = cfg.SpeckRevert;
+ if (cfg.SpeckRevert)
+ {
+ long threshold = Math.Max(1L, (long)Math.Round(cfg.MinLandComponentFrac * (double)mapSize * mapSize));
+ led.ThresholdCells = threshold;
+ float strictlyBelowSea = MathF.BitDecrement(sea);
+ int n = mapSize;
+
+ // Which components go: non-mainland, below the threshold. Mainland is never a candidate.
+ var revert = new Dictionary();
+ foreach (var c in pre.Regions)
+ if (!c.IsMainland && c.SizeCells < threshold) revert[c.Id] = c;
+ if (pre.Mainland != null && revert.ContainsKey(pre.MainlandId))
+ throw new InvalidOperationException("[RegionPass] the mainland was selected for revert. Refusing.");
+
+ if (revert.Count > 0)
+ {
+ // The ring: the mean height of each doomed component's adjacent SEA cells.
+ var ringSum = new Dictionary();
+ var ringCnt = new Dictionary();
+ foreach (int id in revert.Keys) { ringSum[id] = 0; ringCnt[id] = 0; }
+ for (int x = 0; x < n; x++)
+ {
+ for (int y = 0; y < n; y++)
+ {
+ int id = pre.Id[x * n + y];
+ if (id == 0 || !revert.ContainsKey(id)) continue;
+ for (int dx = -1; dx <= 1; dx++)
+ {
+ int nx = x + dx; if (nx < 0 || nx >= n) continue;
+ for (int dy = -1; dy <= 1; dy++)
+ {
+ int ny = y + dy; if (ny < 0 || ny >= n || (dx == 0 && dy == 0)) continue;
+ if (pre.Id[nx * n + ny] != 0) continue; // land (this or another component)
+ ringSum[id] += height[nx, ny]; ringCnt[id]++;
+ }
+ }
+ }
+ }
+
+ var target = new Dictionary();
+ foreach (var (id, c) in revert)
+ {
+ float t = ringCnt[id] > 0 ? (float)(ringSum[id] / ringCnt[id]) : strictlyBelowSea;
+ target[id] = MathF.Min(t, strictlyBelowSea); // strictly below sea, whatever the ring says
+ }
+
+ // The revert, with both guards asserted cell by cell.
+ var touched = new Dictionary();
+ foreach (int id in revert.Keys) touched[id] = 0;
+ long cells = 0;
+ for (int x = 0; x < n; x++)
+ {
+ for (int y = 0; y < n; y++)
+ {
+ int id = pre.Id[x * n + y];
+ if (id == 0 || !target.TryGetValue(id, out float t)) continue; // COMPONENT-ONLY: nothing else is ever touched
+ float h = height[x, y];
+ if (h < sea)
+ throw new InvalidOperationException($"[RegionPass] component {id} cell ({x},{y}) is not land (h {h:G9} < sea {sea:G9}) — the labeling and the field disagree. Refusing.");
+ if (t >= h)
+ throw new InvalidOperationException($"[RegionPass] LOWER-ONLY violated at ({x},{y}): {h:G9} → {t:G9}. Refusing.");
+ height[x, y] = t;
+ touched[id]++; cells++;
+ }
+ }
+ foreach (var (id, c) in revert)
+ {
+ if (touched[id] != c.SizeCells)
+ throw new InvalidOperationException($"[RegionPass] COMPONENT-ONLY violated: component {id} has {c.SizeCells} cells, {touched[id]} touched. Refusing.");
+ led.Reverted.Add((id, c.SizeCells, c.Hemisphere, target[id]));
+ }
+ led.RevertedComponents = revert.Count;
+ led.RevertedCells = cells;
+
+ final = RegionLabeling.Label(height, mapSize, sea);
+ if (final.Mainland == null || pre.Mainland == null || final.Mainland.SizeCells != pre.Mainland.SizeCells)
+ throw new InvalidOperationException("[RegionPass] the mainland's size changed across the revert. Refusing.");
+ }
+ r.Notes.Add($"[Regions] speck revert ON (threshold {threshold:N0} cells = {cfg.MinLandComponentFrac:G2} of the map): " +
+ $"{led.RevertedComponents} sub-threshold non-mainland components ({led.RevertedCells:N0} cells) lowered to their ring's mean seabed; " +
+ $"lower-only and component-only asserted; mainland untouched by definition.");
+ }
+ else r.Notes.Add("[Regions] speck revert OFF.");
+
+ r.Labels = final;
+ led.CentreWasLand = final.CentreWasLand;
+ FillPost(led, final);
+
+ // ═══ THE TAG, BY CONSTRUCTION ═══
+ var tag = new bool[mapSize, mapSize];
+ var hemi = new byte[mapSize, mapSize];
+ for (int x = 0; x < mapSize; x++)
+ for (int y = 0; y < mapSize; y++)
+ {
+ int id = final.Id[x * mapSize + y];
+ if (id == 0 || id == final.MainlandId) continue;
+ tag[x, y] = true;
+ hemi[x, y] = final.Regions[id - 1].Hemisphere;
+ }
+ r.IsIsland = tag; r.IslandHemisphere = hemi;
+ r.Notes.Add($"[Regions] tag by construction: {final.IslandCount} islands (N {led.PostNorth} / S {led.PostSouth}), " +
+ $"{final.LandCells - (final.Mainland?.SizeCells ?? 0):N0} island cells tagged; island cells min {led.PostMin} median {led.PostMedian} mean {led.PostMean:F0} max {led.PostMax}.");
+ return r;
+ }
+
+ private static void FillPre(RegionLedger l, RegionLabels lab)
+ {
+ (l.PreNorth, l.PreSouth) = RegionLabeling.IslandsByHemisphere(lab);
+ var (c, mn, med, mean, mx, h) = RegionLabeling.IslandSizes(lab);
+ l.PreIslands = c; l.PreMin = mn; l.PreMedian = med; l.PreMean = mean; l.PreMax = mx; l.PreHistogram = h;
+ }
+
+ private static void FillPost(RegionLedger l, RegionLabels lab)
+ {
+ (l.PostNorth, l.PostSouth) = RegionLabeling.IslandsByHemisphere(lab);
+ var (c, mn, med, mean, mx, h) = RegionLabeling.IslandSizes(lab);
+ l.PostIslands = c; l.PostMin = mn; l.PostMedian = med; l.PostMean = mean; l.PostMax = mx; l.PostHistogram = h;
+ }
+ }
+}
diff --git a/Tools/Scripts/RegionPass.cs.uid b/Tools/Scripts/RegionPass.cs.uid
new file mode 100644
index 0000000..35b45d9
--- /dev/null
+++ b/Tools/Scripts/RegionPass.cs.uid
@@ -0,0 +1 @@
+uid://btgwrycrjllr4
diff --git a/Tools/Scripts/Shaping.cs b/Tools/Scripts/Shaping.cs
index 8dbbc91..500f74f 100644
--- a/Tools/Scripts/Shaping.cs
+++ b/Tools/Scripts/Shaping.cs
@@ -75,7 +75,7 @@ namespace IslaApocalypse.Tools
knots: null, anchors: null, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: p1.HMinSeed, hMax: p1.HMaxSeed,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
- isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere);
+ isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
}
// ═══ WHICH CURVE (chat2/02) ═══
@@ -212,7 +212,7 @@ namespace IslaApocalypse.Tools
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: edgeAmpRaw, maxEdgeShiftRaw: maxEdgeShiftRaw, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
- isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere);
+ isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
}
///
@@ -283,7 +283,7 @@ namespace IslaApocalypse.Tools
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
- isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere);
+ isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
}
///
@@ -337,7 +337,7 @@ namespace IslaApocalypse.Tools
knots: knots, anchors: anchors, hMaxSeed: p1.HMaxSeed,
edgeAmpRaw: 0f, maxEdgeShiftRaw: 0f, hMin: hMin, hMax: hMax,
elapsedMs: Time.GetTicksMsec() - t0, notes: notes,
- isOffshoreIsland: p1.IsOffshoreIsland, islandHemisphere: p1.IslandHemisphere);
+ isIsland: p1.IsIsland, islandHemisphere: p1.IslandHemisphere);
}
}
}
diff --git a/Tools/Scripts/ShapingOracle.cs b/Tools/Scripts/ShapingOracle.cs
index 9fa73df..8f9d88f 100644
--- a/Tools/Scripts/ShapingOracle.cs
+++ b/Tools/Scripts/ShapingOracle.cs
@@ -414,8 +414,8 @@ namespace IslaApocalypse.Tools
{
var c = new Check { Id = "i", Name = "moat intact — no island touches the mainland" };
int bridged = OffshoreAnalysis.BridgedCount(comps);
- c.Passed = p1.HasOffshoreTag && bridged == 0;
- c.Detail = !p1.HasOffshoreTag ? "no offshore tag — nothing to check"
+ c.Passed = p1.HasIslandTag && bridged == 0;
+ c.Detail = !p1.HasIslandTag ? "no island tag — nothing to check"
: bridged == 0 ? $"all {comps.Count} islands are separated from mainland by water"
: $"{bridged} island(s) BRIDGE to mainland land";
return c;
@@ -476,7 +476,7 @@ namespace IslaApocalypse.Tools
bool cl = p2.HeightClassify[x, y] >= sea;
bool rl = p2.Height[x, y] >= sea;
if (cl != rl) mismatch++;
- if (p2.IsOffshoreIsland != null && p2.IsOffshoreIsland[x, y])
+ if (p2.IsIsland != null && p2.IsIsland[x, y])
{
tagged++;
if (!cl || !rl) tagNotLand++;
@@ -505,6 +505,80 @@ namespace IslaApocalypse.Tools
};
}
+ // ═══ chat2/07 — the region checks ═══
+
+ /// (m) CENTRE IS LAND — the mainland definition held (the massif is centred); the fallback was not needed.
+ public static Check CentreIsLand(Pass1Result p1)
+ {
+ var c = new Check { Id = "m", Name = "mainland = centre component (centre cell is land, no fallback)" };
+ if (p1.Regions == null) { c.Passed = false; c.Detail = "no region labeling on this field"; return c; }
+ var m = p1.Regions.Mainland;
+ c.Passed = p1.Regions.CentreWasLand && m != null && (p1.RegionLedger == null || p1.RegionLedger.CentreWasLandPre);
+ c.Detail = c.Passed
+ ? $"centre is land; mainland id {p1.Regions.MainlandId}, {m.SizeCells:N0} cells, centroid ({m.CentroidX:F0},{m.CentroidY:F0}); {p1.Regions.IslandCount} islands"
+ : "⚠ CENTRE CELL IS NOT LAND — fell back to the largest component";
+ return c;
+ }
+
+ ///
+ /// (n) ⭐ THE REVERT GUARDS, RE-PROVEN ON THE FIELDS — filter OFF vs ON: every cell of the OFF
+ /// field's MAINLAND component is bit-identical; every cell that changed was land in a
+ /// sub-threshold NON-MAINLAND component of the OFF labeling (component-only) and went DOWN, to
+ /// below sea (lower-only); nothing else moved. The pass asserted this as it ran; this is the
+ /// independent proof on the finished fields.
+ ///
+ public static Check RevertGuards(Pass1Result off, Pass1Result on, float sea, long thresholdCells)
+ {
+ var c = new Check { Id = "n", Name = "speck revert: mainland bit-identical, every change is in a sub-threshold island and lower-only" };
+ if (off.Regions == null) { c.Passed = false; c.Detail = "the OFF field has no region labeling"; return c; }
+ int n = off.MapSize;
+ var lab = off.Regions;
+ long mainland = 0, mainlandDiff = 0, changed = 0, notIsland = 0, notSmall = 0, raised = 0, notSea = 0;
+ string first = null;
+ for (int x = 0; x < n; x++)
+ {
+ for (int y = 0; y < n; y++)
+ {
+ float a = off.Height[x, y], b = on.Height[x, y];
+ int id = lab.Id[x * n + y];
+ bool isMain = id != 0 && id == lab.MainlandId;
+ if (isMain) mainland++;
+ if (BitConverter.SingleToInt32Bits(a) == BitConverter.SingleToInt32Bits(b)) continue;
+ changed++;
+ if (isMain) { mainlandDiff++; first ??= $"mainland cell [{x},{y}] {a:G9} → {b:G9}"; continue; }
+ if (id == 0) { notIsland++; first ??= $"sea cell [{x},{y}] changed {a:G9} → {b:G9}"; continue; }
+ if (lab.Regions[id - 1].SizeCells >= thresholdCells) { notSmall++; first ??= $"cell [{x},{y}] of component {id} ({lab.Regions[id - 1].SizeCells} cells ≥ {thresholdCells}) changed"; }
+ if (b >= a) { raised++; first ??= $"cell [{x},{y}] RAISED {a:G9} → {b:G9}"; }
+ if (b >= sea) { notSea++; first ??= $"cell [{x},{y}] still land after revert ({b:G9})"; }
+ }
+ }
+ c.Passed = mainlandDiff == 0 && notIsland == 0 && notSmall == 0 && raised == 0 && notSea == 0;
+ c.Detail = c.Passed
+ ? $"all {mainland:N0} mainland cells bit-identical; {changed:N0} cells changed, every one in a sub-threshold island, lowered below sea"
+ : $"VIOLATION — mainland {mainlandDiff:N0} / non-island {notIsland:N0} / over-threshold {notSmall:N0} / raised {raised:N0} / still land {notSea:N0} — {first}";
+ return c;
+ }
+
+ /// (o) LABELS DETERMINISTIC — two generations of the same seed: id maps and component tables identical.
+ public static Check LabelsDeterministic(Pass1Result a, Pass1Result b)
+ {
+ var c = new Check { Id = "o", Name = "region ids deterministic per seed (two runs, id map + table identical)" };
+ if (a.Regions == null || b.Regions == null) { c.Passed = false; c.Detail = "no region labeling"; return c; }
+ long diff = 0; int n = a.MapSize;
+ for (int i = 0; i < n * n; i++) if (a.Regions.Id[i] != b.Regions.Id[i]) diff++;
+ bool table = a.Regions.Regions.Count == b.Regions.Regions.Count && a.Regions.MainlandId == b.Regions.MainlandId;
+ if (table)
+ for (int i = 0; i < a.Regions.Regions.Count; i++)
+ {
+ var ra = a.Regions.Regions[i]; var rb = b.Regions.Regions[i];
+ if (ra.SizeCells != rb.SizeCells || ra.CentroidX != rb.CentroidX || ra.CentroidY != rb.CentroidY || ra.Hemisphere != rb.Hemisphere || ra.IsMainland != rb.IsMainland) { table = false; break; }
+ }
+ c.Passed = diff == 0 && table;
+ c.Detail = c.Passed ? $"{a.Regions.Regions.Count} components, id map identical over {(long)n * n:N0} cells, tables identical"
+ : $"{diff:N0} id cells differ; tables {(table ? "identical" : "DIFFER")}";
+ return c;
+ }
+
/// Render the whole oracle as a markdown table for the INDEX and the report.
public static string ToMarkdownTable(IEnumerable checks)
{
diff --git a/Tools/Scripts/TerrainGenConfig.cs b/Tools/Scripts/TerrainGenConfig.cs
index 03a3d2d..eb91120 100644
--- a/Tools/Scripts/TerrainGenConfig.cs
+++ b/Tools/Scripts/TerrainGenConfig.cs
@@ -274,6 +274,32 @@ namespace IslaApocalypse.Tools
///
public OffshoreSettings Offshore = new OffshoreSettings();
+ // ---- PASS 1c — region labeling + the speck revert (chat2/07) ----------
+
+ ///
+ /// ⭐ THE REGION-LABELING LAYER (Core.RegionLabeling, via RegionPass): 8-connected
+ /// land components on the classify field, mainland = the centre component, the island tag BY
+ /// CONSTRUCTION. Pure analysis — it changes no height — so it is ON by default without touching
+ /// any regression anchor. Off ⇒ no tag, no region table (the tag arrays are null).
+ ///
+ public bool RegionLabeling = true;
+
+ ///
+ /// ⭐ THE SPECK REVERT — lower every non-mainland land component smaller than
+ /// of the map to seabed. Origin-blind; lower-only and
+ /// component-only, asserted; mainland never a candidate.
+ ///
+ /// ⚠ DEFAULT OFF IN THE BARE CONFIG, for exactly the reason the shelf and the islets are: the
+ /// raw field has small natural nubs, so with this ON the calibration pool's land histogram, the
+ /// curve knots and every Phase-1 / task-03 / task-04 regression dump would move at once.
+ /// The region batch turns it on explicitly (its preset is ON); flipping the bare default is
+ /// the act that re-baselines the oracles — own task, not a side effect.
+ ///
+ public bool SpeckRevert = false;
+
+ /// The revert threshold, as a fraction of the map's AREA (scale-free). → .
+ public float MinLandComponentFrac = RegionPass.ThresholdMidFrac;
+
/// A short label for this variant, used in output filenames. E.g. "full", "base_only".
public string VariantLabel = "full";
diff --git a/Tools/Scripts/Topography.cs b/Tools/Scripts/Topography.cs
index 36166fe..edbcc2f 100644
--- a/Tools/Scripts/Topography.cs
+++ b/Tools/Scripts/Topography.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
@@ -299,7 +300,18 @@ namespace IslaApocalypse.Tools
// ~290 m). Both values are carried so the report states whether it moved, not guesses.
float hMaxBeforeOffshore = hMax;
OffshorePass.Result offshore = OffshorePass.Apply(height, preTrenchFalloff, mapSize, cfg.Seed, cfg.SeaLevel, cfg);
- if (offshore != null)
+
+ // ═══ PASS 1c — REGION LABELING + THE SPECK REVERT + THE ISLAND TAG (chat2/07) ═══
+ //
+ // The general region layer over the classify field (this array), then the origin-blind
+ // speck revert (config-gated, lower-only, component-only), then the island tag BY
+ // CONSTRUCTION from the finished labeling. Labeling alone changes nothing; only the revert
+ // may, and only downward, and only inside a sub-threshold non-mainland component. → RegionPass.
+ RegionPass.Result regions = cfg.RegionLabeling
+ ? RegionPass.Apply(height, mapSize, cfg.SeaLevel, cfg)
+ : null;
+
+ if (offshore != null || regions != null)
{
hMax = float.MinValue;
hMin = float.MaxValue;
@@ -312,14 +324,21 @@ namespace IslaApocalypse.Tools
}
}
+ var notes = new List();
+ if (offshore != null) notes.AddRange(offshore.Notes);
+ if (regions != null) notes.AddRange(regions.Notes);
+
return new Pass1Result(mapSize, cfg.Seed, height, preTrenchFalloff, latitudeField,
hMax, hMin, Time.GetTicksMsec() - t0,
hMaxSeedBeforeOffshore: hMaxBeforeOffshore,
- isOffshoreIsland: offshore?.Tag,
- islandHemisphere: offshore?.Hemi,
+ isIsland: regions?.IsIsland,
+ islandHemisphere: regions?.IslandHemisphere,
offshoreLiftedCells: offshore == null ? 0 : offshore.LiftedOrganic - offshore.LiftedReverted,
- notes: offshore?.Notes,
- offshoreLedger: offshore?.ToLedger());
+ notes: notes,
+ offshoreLedger: offshore?.ToLedger(),
+ regions: regions?.Labels,
+ regionLedger: regions?.Ledger,
+ regionsPre: regions?.LabelsPre);
}
}
}