Core/Scripts/RegionLabeling.cs is the shared-infra contract, built to the letter: it runs on the CLASSIFY (raw) field; land is 8-connected, the deliberate complement of water's 4 (a diagonal isthmus joins; the water either side stays separate); a component is a maximal 8-connected set of land cells; the MAINLAND is the component containing the map centre — not merely the largest, which a later fragmentation step could flip — with a flagged fallback to the largest if the centre were ever water (asserted, never needed: oracle m); every other component is an island; per component id / sizeCells / centroid / hemisphere (by centroid, one label per island) / isMainland. Ids come from a fixed scan order and are proven stable across two generations (oracle o, 16.8M cells). It knows nothing about offshore or stamped. Engine-free, in Core as C++-candidate math; the hemisphere convention moved there with it, OffshoreAnalysis aliases it. Tools/Scripts/RegionPass.cs is pass 1c: label, revert, relabel, tag. The island tag (renamed IsIsland; IslandHemisphere from the component's centroid; Pass1Result.Regions carries the whole table) is now a CONSEQUENCE of labeling — every non-mainland component. That is the fix for the chat2/06 overlay, which tagged only what the offshore pass raised: 1063685222 has 11 natural islands including a 94,511-cell detached mass, 20260821 has 19, all grey in 06's tags.png and all coloured now. The offshore pass itself is untouched; its internal Tag stays for its own guards and is no longer exported. The speck revert (TerrainGenConfig.SpeckRevert / MinLandComponentFrac) lowers every non-mainland component below the threshold to the mean of its ring of adjacent sea cells, held strictly below sea. Origin-blind: a natural nub goes the same way as an offshore dot (6 natural components / 273 cells on the bare 1063685222 field at threshold_mid — reported as a3r, informational). Lower-only and component-only are asserted cell by cell in the pass and re-proven on the finished fields by oracle n (mainland bit-identical filter OFF vs ON; every changed cell in a sub-threshold island, lowered below sea); the mainland is never a candidate and its size is asserted unchanged across the revert. A reverted offshore island leaves its submerged skirt as a shoal — not this component, by the rule. Classify/render consistency is by construction (pass 1, curve identity at sea) and asserted by oracle k. Deliberately OFF in the bare TerrainGenConfig for the reason the shelf and islets are: the raw field has natural specks, so default-ON would move the calibration pool and every regression dump; the batch turns it on. Thresholds swept on 8 seeds at 4096 (1e-5 / 3e-5 / 1e-4 of the map = 168 / 503 / 1,678 cells): low removes 0–6 nubs per seed, mid (the config default, equal to the offshore guard) 2–11, high 26–36 — most of the offshore islands, the "fewer, bigger" bookend. The count/size table carries natural / pre / post counts per hemisphere, min/median/mean/max and a log-spaced size histogram — the instrument for the southern-stretch step. Oracle, all passing: a1, a3, a4 (8192, 67M cells) with labeling ON + revert OFF; a6 NEW — labeling ON + revert OFF on the 06 preset bit-identical to the 06 batch's render field (labeling is pure analysis); j0; m, n, o, i, j, k, l, b per field. Batch: BatchRoot(7, "region_labeling") — exactly 4 plates (three thresholds on 1063685222, threshold_mid on 20260821, the table's most-natural-islands seed), each with grayscale / .f32 / relief / the labeled-regions overlay / the tag overlay, plus count_size_table.md/.csv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
153 lines
7.2 KiB
C#
153 lines
7.2 KiB
C#
using System.Collections.Generic;
|
||
using IslaApocalypse.Core;
|
||
|
||
namespace IslaApocalypse.Tools
|
||
{
|
||
/// <summary>
|
||
/// Everything pass 1 produces. → <see cref="Topography"/>.
|
||
///
|
||
/// ⭐ THREE OF THESE FIELDS ARE THE PHASE-2 SEAM. The reference computed them in pass 1 and
|
||
/// consumed them in pass 2; exposing them now is what keeps the deferred ports clean, and it
|
||
/// costs nothing to carry.
|
||
/// </summary>
|
||
public sealed class Pass1Result
|
||
{
|
||
/// <summary>Map side in columns.</summary>
|
||
public readonly int MapSize;
|
||
|
||
/// <summary>The resolved seed this was generated with. Always positive, always recorded.</summary>
|
||
public readonly int Seed;
|
||
|
||
/// <summary>
|
||
/// The raw pass-1 height field, <c>[x, y]</c>. Pre-curve, pre-erosion, pre-crater.
|
||
/// The reference's <c>finalH</c> (~:619) written to <c>_heightMap</c> (~:666).
|
||
/// </summary>
|
||
public readonly float[,] Height;
|
||
|
||
/// <summary>
|
||
/// ⭐ PHASE-2 SEAM — the falloff value captured BEFORE the <c>Pow(·, 2.5f)</c> and BEFORE the
|
||
/// Trench wall (reference ~:591, <c>preTrenchFalloff</c>).
|
||
///
|
||
/// It is the "how far past the island body are we" number, uncontaminated by the power's
|
||
/// curvature and by the Trench's additive ramp. The DEFERRED offshore-islet layer reads it
|
||
/// as its distance mask (<c>OffshoreZoneWeight</c>), which is the only reason it is captured
|
||
/// mid-computation rather than recomputed.
|
||
///
|
||
/// ⚠ A port that "tidies" the falloff chain into one expression destroys this capture point,
|
||
/// and the loss would not show up until Phase 2's offshore mask moved. It is exposed here so
|
||
/// that cannot happen quietly.
|
||
/// </summary>
|
||
public readonly float[,] PreTrenchFalloff;
|
||
|
||
/// <summary>
|
||
/// ⭐ PHASE-2 SEAM — the wobbled latitude scalar, <c>[x, y]</c>.
|
||
///
|
||
/// ⚠ THIS IS NOT A CLIMATE TEMPERATURE MAP. See <see cref="Topography"/> for the full note.
|
||
/// It is a stage-1-local latitude field that terrain GEOMETRY reads. Phase 2 needs it if the
|
||
/// latitude sea-level model is ever switched on (<c>SeaLevelModel = "field"</c>); under the
|
||
/// shipped flat model it has no consumer beyond the spine.
|
||
/// </summary>
|
||
public readonly float[,] LatitudeField;
|
||
|
||
/// <summary>
|
||
/// ⭐ PHASE-2 SEAM — the maximum height over the whole map (reference <c>_hMaxSeed</c>, ~:665).
|
||
///
|
||
/// The reference's v2+ redistribution curve is SEED-DEPENDENT: its spike maps
|
||
/// <c>[t4, hMaxSeed]</c> onto the peak band, so pass 2 cannot start until pass 1 has scanned
|
||
/// every pixel. That is precisely why the pass-1/pass-2 boundary is a hard one and not an
|
||
/// interleave. Carried here so Phase 2 inherits it rather than rediscovering the dependency.
|
||
/// </summary>
|
||
public readonly float HMaxSeed;
|
||
|
||
/// <summary>The minimum height. Not a reference field — carried for the renderer's ramp and the report.</summary>
|
||
public readonly float HMinSeed;
|
||
|
||
/// <summary>
|
||
/// ⚠ The map-wide max BEFORE the coast shelf and offshore islets ran (chat2/05). The
|
||
/// reference takes <c>_hMaxSeed</c> AFTER both, inside the same loop; v2 now does too —
|
||
/// <see cref="HMaxSeed"/> is the post-shelf/offshore value the curve normalizes against.
|
||
/// This one is carried so the report can state whether the two differed (expected: no, an
|
||
/// islet crest of ~34 m is far below any peak — but "expected" is measured, not assumed).
|
||
/// </summary>
|
||
public readonly float HMaxSeedBeforeOffshore;
|
||
|
||
// ═══ ⭐ 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 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.
|
||
|
||
/// <summary>Per column: is this land an island (any non-mainland land component)? Null when labeling is off.</summary>
|
||
public readonly bool[,] IsIsland;
|
||
|
||
/// <summary>
|
||
/// Per column: <see cref="RegionLabeling.HemiNorth"/> / <see cref="RegionLabeling.HemiSouth"/>
|
||
/// for tagged cells (the COMPONENT's hemisphere, by centroid), <see cref="RegionLabeling.HemiNone"/>
|
||
/// otherwise. Null when labeling is off.
|
||
/// </summary>
|
||
public readonly byte[,] IslandHemisphere;
|
||
|
||
/// <summary>⭐ The region layer's output for this field: id map + per-component table (post-revert). Null when labeling is off.</summary>
|
||
public readonly RegionLabels Regions;
|
||
|
||
/// <summary>The labeling BEFORE the speck revert (== <see cref="Regions"/> when the revert is off or removed nothing). For the overlay's "where a speck was". Null when labeling is off.</summary>
|
||
public readonly RegionLabels RegionsPre;
|
||
|
||
/// <summary>The region pass's numbers — pre/post-revert counts and sizes, what was reverted. Null when labeling is off.</summary>
|
||
public readonly RegionLedger RegionLedger;
|
||
|
||
/// <summary>Cells the islet layer lifted above sea. The reference printed this too.</summary>
|
||
public readonly long OffshoreLiftedCells;
|
||
|
||
/// <summary>The islet layer's numbers — thresholds, pre-guard count, guard ledger, final count. Null when offshore is off.</summary>
|
||
public readonly OffshoreLedger OffshoreLedger;
|
||
|
||
/// <summary>Lines worth printing from the shelf/offshore pass: thresholds, guard ledger, lifted counts.</summary>
|
||
public readonly IReadOnlyList<string> Notes;
|
||
|
||
/// <summary>Wall-clock milliseconds the pass took.</summary>
|
||
public readonly ulong ElapsedMs;
|
||
|
||
public Pass1Result(int mapSize, int seed, float[,] height, float[,] preTrenchFalloff,
|
||
float[,] latitudeField, float hMaxSeed, float hMinSeed, ulong elapsedMs,
|
||
float hMaxSeedBeforeOffshore = float.NaN,
|
||
bool[,] isIsland = null, byte[,] islandHemisphere = null,
|
||
long offshoreLiftedCells = 0, IReadOnlyList<string> notes = null, OffshoreLedger offshoreLedger = null,
|
||
RegionLabels regions = null, RegionLedger regionLedger = null, RegionLabels regionsPre = null)
|
||
{
|
||
MapSize = mapSize;
|
||
Seed = seed;
|
||
Height = height;
|
||
PreTrenchFalloff = preTrenchFalloff;
|
||
LatitudeField = latitudeField;
|
||
HMaxSeed = hMaxSeed;
|
||
HMinSeed = hMinSeed;
|
||
ElapsedMs = elapsedMs;
|
||
HMaxSeedBeforeOffshore = float.IsNaN(hMaxSeedBeforeOffshore) ? hMaxSeed : hMaxSeedBeforeOffshore;
|
||
IsIsland = isIsland;
|
||
IslandHemisphere = islandHemisphere;
|
||
Regions = regions;
|
||
RegionsPre = regionsPre;
|
||
RegionLedger = regionLedger;
|
||
OffshoreLiftedCells = offshoreLiftedCells;
|
||
OffshoreLedger = offshoreLedger;
|
||
Notes = notes ?? System.Array.Empty<string>();
|
||
}
|
||
|
||
/// <summary>Whether the region layer ran on this field (the tag arrays are present).</summary>
|
||
public bool HasIslandTag => IsIsland != null;
|
||
|
||
/// <summary>Fraction of the map at or above the sea threshold. A cheap shape sanity number.</summary>
|
||
public float LandFraction(float seaLevel)
|
||
{
|
||
long land = 0;
|
||
for (int x = 0; x < MapSize; x++)
|
||
for (int y = 0; y < MapSize; y++)
|
||
if (Height[x, y] >= seaLevel) land++;
|
||
return land / (float)((long)MapSize * MapSize);
|
||
}
|
||
}
|
||
}
|