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
179 lines
7.8 KiB
C#
179 lines
7.8 KiB
C#
using System.Collections.Generic;
|
|
using IslaApocalypse.Core;
|
|
|
|
namespace IslaApocalypse.Tools
|
|
{
|
|
/// <summary>
|
|
/// Everything pass 2 produces: THE TWO HEIGHT FIELDS. → <see cref="Shaping"/>.
|
|
///
|
|
/// ═══ ⭐⭐ THE TWO-FIELD SPLIT (D-046) — THE DISCIPLINE THIS CLASS EXISTS TO HOLD ═══
|
|
///
|
|
/// <see cref="HeightClassify"/> RAW. Uncurved, un-detailed. The ORACLE.
|
|
/// <see cref="Height"/> RENDER. Curved, detailed, and later eroded and carved.
|
|
///
|
|
/// Everything that CLASSIFIES the world — biomes, water bodies, the ocean flood fill — reads the
|
|
/// classify field. Everything that DRAWS or MESHES it reads the render field. The reference's
|
|
/// hardest-won lesson is that this split is what made five rounds of taste-iteration safe: the
|
|
/// biome and water maps stayed md5-identical across every shaping change, so correctness was
|
|
/// never being judged by eye. → `Design - Tooling - Iteration and Batching.md`,
|
|
/// "build the oracle before the taste-iteration".
|
|
///
|
|
/// ⚠ NOTHING CONSUMES THE CLASSIFY FIELD YET. No water, no biomes exist in this phase. The split
|
|
/// is established HERE, at the curve, because the curve is where the second field is BORN — and
|
|
/// retrofitting a classify path after three passes already ran on one array is how the two
|
|
/// silently diverge. The field is produced and asserted now so that when water lands it has
|
|
/// something correct to read.
|
|
///
|
|
/// ═══ ⚠ WHEN THE TWO FIELDS ARE THE SAME ARRAY ═══
|
|
///
|
|
/// With the curve OFF there is nothing to separate, so both properties reference ONE array —
|
|
/// exactly as the reference did (<c>_heightMapClassify = (_curveOn || _erosionOn) ? new float[…]
|
|
/// : _heightMap</c>). <see cref="FieldsAreAliased"/> says so out loud, because a later pass that
|
|
/// writes through one reference while reading the other MUST know: the reference's crater carve
|
|
/// reads both into locals before writing either for precisely this reason, and that is the trap
|
|
/// this flag is here to keep visible until the carve lands.
|
|
/// </summary>
|
|
public sealed class Pass2Result
|
|
{
|
|
/// <summary>Map side in columns.</summary>
|
|
public readonly int MapSize;
|
|
|
|
/// <summary>The resolved seed. Same seed, same two fields.</summary>
|
|
public readonly int Seed;
|
|
|
|
/// <summary>
|
|
/// ⭐ THE RENDER FIELD, <c>[x, y]</c> — curved and detailed. What gets drawn, dumped and
|
|
/// (later) eroded, carved and meshed.
|
|
/// </summary>
|
|
public readonly float[,] Height;
|
|
|
|
/// <summary>
|
|
/// ⭐ THE CLASSIFY FIELD, <c>[x, y]</c> — bit-for-bit the raw pre-curve pass-1 height.
|
|
///
|
|
/// ⚠ Nothing may write to this after pass 2 except the crater carve, which is the one pass
|
|
/// that legitimately moves both fields. Erosion, rivers and detail are render-only.
|
|
/// </summary>
|
|
public readonly float[,] HeightClassify;
|
|
|
|
/// <summary>Was the curve applied? The primary A/B gate.</summary>
|
|
public readonly bool CurveOn;
|
|
|
|
/// <summary>
|
|
/// Which curve shaped the render field: "off", "staircase", "continuous" or "lifted_WRONG".
|
|
/// A label, not logic — the oracle and the INDEX read it so a result can never be mistaken
|
|
/// for the wrong mode's.
|
|
/// </summary>
|
|
public readonly string CurveModeLabel;
|
|
|
|
/// <summary>
|
|
/// The batch variant this result belongs to, carried straight from
|
|
/// <see cref="TerrainGenConfig.VariantLabel"/>.
|
|
///
|
|
/// ⚠ CARRIED, NOT INFERRED. The first cut of the chat2/02 tool reconstructed this from the
|
|
/// knob values ("ceiling > 31 ⇒ hold_higher"), which works only for exactly today's
|
|
/// variant set: add a second variant sharing a knob value and two different runs silently
|
|
/// write to one folder. The label is an identity, so it travels with the result.
|
|
/// </summary>
|
|
public readonly string VariantLabel;
|
|
|
|
/// <summary>
|
|
/// The per-seed continuous spline, when <see cref="CurveModeLabel"/> is "continuous" — the
|
|
/// oracle samples its slopes (check e) and the INDEX prints its control points. Null for
|
|
/// every other mode.
|
|
/// </summary>
|
|
public readonly ContinuousCurve Continuous;
|
|
|
|
// ═══ ⭐ THE OFFSHORE TAG, CARRIED (chat2/05) ═══
|
|
//
|
|
// Pass 1 sets it; pass 2 carries it UNCHANGED beside the two height fields, because this is
|
|
// where the shaped-terrain result flows and where a downstream consumer would pick it up.
|
|
// The curve is identity at sea and monotone above, so a cell that was offshore-island LAND
|
|
// in pass 1 is still land in the render field — the tag stays valid for both fields without
|
|
// being recomputed (oracle: "classify/render coastline consistent").
|
|
//
|
|
// ⚠ NO LOGIC READS IT THIS PHASE. It is a data layer. A biome/fertility/placement pass reads
|
|
// it from here, checks for null (offshore off), and never re-derives island-land from
|
|
// geometry.
|
|
|
|
/// <summary>→ <see cref="Pass1Result.IsIsland"/>, the same array. Null when region labeling is off.</summary>
|
|
public readonly bool[,] IsIsland;
|
|
|
|
/// <summary>→ <see cref="Pass1Result.IslandHemisphere"/>, the same array. Null when region labeling is off.</summary>
|
|
public readonly byte[,] IslandHemisphere;
|
|
|
|
/// <summary>Was shelf detail applied? Requires <see cref="CurveOn"/> — it warps the curve's knots.</summary>
|
|
public readonly bool DetailOn;
|
|
|
|
/// <summary>The knot set used. Null when the curve is off.</summary>
|
|
public readonly CurveKnots Knots;
|
|
|
|
/// <summary>The output anchors used. Null when the curve is off.</summary>
|
|
public readonly CurveAnchors Anchors;
|
|
|
|
/// <summary>The per-seed spike input, carried through from pass 1.</summary>
|
|
public readonly float HMaxSeed;
|
|
|
|
/// <summary>The edge-warp amplitude actually APPLIED, raw units (post-clamp). Zero when detail is off.</summary>
|
|
public readonly float EdgeAmpRaw;
|
|
|
|
/// <summary>The knot set's safe warp bound, raw units — what <see cref="EdgeAmpRaw"/> was clamped to.</summary>
|
|
public readonly float MaxEdgeShiftRaw;
|
|
|
|
/// <summary>Render-field extremes after shaping. For the ramp and the report.</summary>
|
|
public readonly float HMin, HMax;
|
|
|
|
/// <summary>Wall-clock milliseconds pass 2 took.</summary>
|
|
public readonly ulong ElapsedMs;
|
|
|
|
/// <summary>
|
|
/// Lines worth printing: the monotonicity confirmation, any loud clamp. Collected rather than
|
|
/// printed inside the pass so the shaping code stays a pure function of its inputs and the
|
|
/// tool owns the console.
|
|
/// </summary>
|
|
public readonly List<string> Notes;
|
|
|
|
public Pass2Result(int mapSize, int seed, float[,] height, float[,] heightClassify,
|
|
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<string> notes, bool[,] isIsland = null, byte[,] islandHemisphere = null)
|
|
{
|
|
IsIsland = isIsland;
|
|
IslandHemisphere = islandHemisphere;
|
|
MapSize = mapSize;
|
|
Seed = seed;
|
|
Height = height;
|
|
HeightClassify = heightClassify;
|
|
CurveOn = curveOn;
|
|
DetailOn = detailOn;
|
|
CurveModeLabel = curveModeLabel;
|
|
VariantLabel = variantLabel;
|
|
Continuous = continuous;
|
|
Knots = knots;
|
|
Anchors = anchors;
|
|
HMaxSeed = hMaxSeed;
|
|
EdgeAmpRaw = edgeAmpRaw;
|
|
MaxEdgeShiftRaw = maxEdgeShiftRaw;
|
|
HMin = hMin;
|
|
HMax = hMax;
|
|
ElapsedMs = elapsedMs;
|
|
Notes = notes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⚠ True when the two fields ARE the same array (curve off). Any pass that writes one while
|
|
/// reading the other must read both into locals first. See the type header.
|
|
/// </summary>
|
|
public bool FieldsAreAliased => ReferenceEquals(Height, HeightClassify);
|
|
|
|
/// <summary>Fraction of the RENDER field at or above the sea threshold.</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);
|
|
}
|
|
}
|
|
}
|