islaApocalypse-v2/Tools/Scripts/OffshorePass.cs
beezm e8571b2778 chat2/06: offshore islands — revert the forced floor, tune the organic layer for coverage
The seeded floor from chat2/05 — FloorNorth/FloorSouth, the PCG32 placement, StampWeight,
StampRadiusFrac/StampCoreFrac/StampEdgeJitter, the centre separation and land gap, the
floor self-check and oracle (h), the centre rings on the tag overlay — is reverted out
whole, as a forward commit. It looked stamped. It is one checkout away at 3b96e06. What
remains is ONE island mechanism: the organic noise-field layer, with chat2/05 stage 2's
shape untouched (freq 16, crest 24 m pre-curve, core 0.25, sharpness 2.5, corners on,
moat + falloff test + outer bound unmoved), now OffshoreSettings.Organic(). The count per
hemisphere is a statistical outcome of the tuning, read off a table, never guaranteed.

The south was measured before anything moved (OffshoreDiagnosis, over task 01's pool at
2048): the premise that the south generates fewer islands is not what the field says.
The NORTH has 1.75x less island-eligible ocean (56 % of its ocean is zone vs 69 %; 233
zone rows per column vs 407) and under half the island candidates (1,183 over-threshold
peaks in zone vs 2,422). Per cell the south is richer too (484 vs 414 candidates per
Mcell). The gate that loses candidates is the moat in the north and the falloff test in
the south — the bulging south coast pushes the 0.72 contour out — but neither binds
enough to suppress it. So the fix is density, and the binding constraint for "a couple
north, consistently" is the north.

The knobs: Density (the main one, the calibration quantile) and SouthWeight (south density
= Density x weight — applied to the quantile, so a south island looks exactly like a north
one; there are just more). Three levels batched on 12 seeds at 4096, SouthWeight 1.25:
0.016 N 2/4.4/10 S 8/13.8/21 (every seed clears N >= 2, S >= 3, but N's minimum IS 2),
0.022 N 3/6.3/12 S 15/19.9/29 (the preset — the first level whose north minimum sits
comfortably above the target), 0.030 N 3/7.8/15 S 19/25.2/33 (the too-many bookend). The
south weight is the developer's stated preference, not a fix; the SouthWeight 1.0 tables
(S 6/10.9/16 at 0.022) are in the batch's scratch/ for the comparison.

The guards, so more density does not buy slop, each a revert by component membership:
specks (< 3e-5 of the map's area, ~500 cells at 4096 — 70-100 per field at these
densities, the noise caps the reference also surfaced), clusters (the smaller of two
islands whose shores are within 0.8 % of the map width — 0-7 per field), blobs (> 6e-4 —
never bit). And the reference's submerged humps, now attributed by HUMP rather than by
bounding box: the first 4096 plate probe showed hollow ghost rings beside islands — a
guard-dropped island's cap went back to seabed but its rim survived inside its
neighbour's bbox keep-region. A hump (one connected raised region) now stays only if it
holds a kept island, and a dropped island's own cap goes even inside a kept hump.

Oracle, all passing: a1 (Phase-1 dump), a3 (task-03 dump), a4 NEW — offshore OFF at 8192
bit-identical to the terrain-curve-v1 tag's own 04 gallery dump over 67,108,864 cells —
j0 shelf inert on land, and per field moat / mainland unmoved / tag-coastline / HMaxSeed
(unchanged, 1.289169) / classify on all 36 table fields and 4 plates. Batch:
BatchRoot(6, "offshore_organic_tune") — exactly 4 plates (three densities on 1063685222,
density_mid on 424242, the table's sparsest south) + count_table.md/.csv + diagnosis.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EY3ZTF6NwzF8ukBHQXSK7
2026-08-21 06:22:54 -04:00

404 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
/// <summary>
/// ⭐⭐ PASS 1b — THE COAST SHELF AND THE OFFSHORE ISLETS (chat2/05, retuned chat2/06). Runs over
/// the finished pass-1 arrays, IN PLACE, before <c>HMaxSeed</c> is taken and before anything
/// classifies.
///
/// ═══ WHERE THIS SITS, AND WHY IT IS A SECOND SWEEP ═══
///
/// The reference did all of this INSIDE the pass-1 pixel loop (<c>MapGenerator.cs:621-664</c>):
/// combine → shelf → islets → <c>_hMaxSeed</c> → write. v2 runs the same arithmetic as a second
/// sweep over the arrays the first loop produced. Per pixel the inputs are identical — the raw
/// height, the sea level, the pre-Trench falloff, (x, y) — and the operations are applied in the
/// same order on the same floats, so the FAITHFUL mode reproduces the reference cell for cell.
/// The second sweep is also what lets the slop guards see whole islands: a component rule needs
/// the finished field.
///
/// ⚠⚠ THE ORDERING THAT CLOSES chat2/00 DRIFT §2: the caller recomputes <c>HMaxSeed</c> AFTER this
/// pass, as the reference did, so the curve's per-seed peak normalization sees the same maximum
/// the reference saw. Expected to be unchanged (a ~34 m crest is far below any peak) — reported,
/// not assumed.
///
/// ═══ THE SUB-PASSES ═══
///
/// 1. SHELF every below-sea cell; depth-preserving; held strictly below sea by BitDecrement.
/// 2. ORGANIC ⭐ THE ONE ISLAND MECHANISM — the reference's noise layer, faithful or reshaped
/// (smaller / lower / flatter / crisper, density + south weight), every below-sea
/// cell inside the zone mask. Nothing places an island from a centre; nothing
/// guarantees a count. (chat2/05's seeded floor was reverted out in chat2/06 — it
/// looked stamped. It lives in git history.)
/// 3. GUARDS (Organic only) specks, clusters and blobs reverted BY COMPONENT, then the
/// reference's submerged humps outside any surviving island's skirt.
///
/// ═══ THE TWO PROTECTIONS ═══
///
/// The moat (min depth) and the "actually offshore" test (pre-Trench falloff) gate the organic
/// layer per pixel — that is the reference. Nothing in this pass can raise a cell outside the
/// zone, so no island can bridge to shore or appear in a lake / the crater bay.
///
/// ═══ ⚠ THE ONE HONEST COUPLING ═══
///
/// Unlike the curve, this pass TURNS WATER INTO LAND. New land is new classification downstream
/// (biome/water pixels that did not exist before). That is precisely why it runs here, in the base
/// shape, before anything classifies: change an island dial, regenerate, and classification
/// re-runs consistently. Known property, stated in code, not a surprise.
///
/// Every cell lifted from below sea to at/above sea is TAGGED (<c>Result.Tag</c>) with its
/// hemisphere (<c>Result.Hemi</c>). That tag is data the shape pass sets and carries; nothing in
/// this phase reads it. → <see cref="OffshoreAnalysis"/> for the hemisphere convention.
/// </summary>
public static class OffshorePass
{
public sealed class Result
{
public bool[,] Tag; // null when the islet layer is off (shelf only)
public byte[,] Hemi;
public long ShelfCells, LiftedOrganic;
public float ThresholdNorth = float.NaN, ThresholdSouth = float.NaN;
public List<string> Notes = new();
public int CountNorth, CountSouth, Bridged;
// ---- the guards' ledger (Organic only) ----
public int PreGuardNorth, PreGuardSouth; // islands before any guard
public int SpecksNorth, SpecksSouth; // reverted as specks
public int ClustersNorth, ClustersSouth; // reverted as too-close-to-a-larger-island
public int BlobsNorth, BlobsSouth; // reverted as oversize
public long LiftedReverted; // surfaced cells the guards put back
public long RaisedReverted; // submerged bump cells the guard put back
internal List<(int x, int y, float h0)> LiftedOrigin = new(); // every SURFACED lift
internal List<(int x, int y, float h0)> RaisedOrigin = new(); // every organic raise, surfaced or not (guards on)
public OffshoreLedger ToLedger() => Tag == null ? null : new OffshoreLedger
{
ThresholdNorth = ThresholdNorth, ThresholdSouth = ThresholdSouth,
PreGuardNorth = PreGuardNorth, PreGuardSouth = PreGuardSouth,
SpecksNorth = SpecksNorth, SpecksSouth = SpecksSouth,
ClustersNorth = ClustersNorth, ClustersSouth = ClustersSouth,
BlobsNorth = BlobsNorth, BlobsSouth = BlobsSouth,
LiftedOrganic = LiftedOrganic, LiftedReverted = LiftedReverted, RaisedReverted = RaisedReverted,
CountNorth = CountNorth, CountSouth = CountSouth,
};
}
/// <summary>
/// Apply the shelf and/or islets to <paramref name="height"/> IN PLACE. Returns null when
/// both are off (nothing touched, nothing allocated).
/// </summary>
public static Result Apply(float[,] height, float[,] preTrench, int mapSize, int seed,
float sea, TerrainGenConfig cfg)
{
OffshoreSettings s = cfg.Offshore;
bool shelfOn = cfg.CoastShelf;
bool isletsOn = s != null && s.Mode != OffshoreMode.Off;
if (!shelfOn && !isletsOn) return null;
var r = new Result();
GenerationScale scale = cfg.Scale;
// ⚠ MAP-anchored, exactly as the reference: `center = MapSize / 2.0f`, `distX = |x cx| / (MapSize / 2.0f)`.
float centerX = mapSize / 2.0f, centerY = mapSize / 2.0f;
float halfSpan = mapSize / 2.0f;
// ═══ 1. THE COAST SHELF ═══
if (shelfOn)
{
// ⭐ THE CLAMP THAT MAKES "CANNOT MOVE THE WATERLINE" EXACT. Reference: the remap is
// strictly positive on positive depth, so in exact arithmetic the waterline cannot
// move; in float32 it can — a pixel a few microns under water rounds back up to
// exactly sea and `h < sea` then calls it land. That cost 5 px of 67 M on the
// reference's first batch. Hold the result strictly below sea and the invariant is
// exact. Do not port this without the clamp.
float strictlyBelowSea = MathF.BitDecrement(sea);
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
float h = height[x, y];
if (h >= sea) continue; // below-sea ONLY
float depthM = WorldScale.MetresFromRaw(sea - h); // (seaHere finalH) * 251f
height[x, y] = MathF.Min(
sea - WorldScale.RawFromMetres(IslandFalloff.CoastShelf(depthM, cfg.ShelfStrength, cfg.ShelfScaleM)),
strictlyBelowSea);
r.ShelfCells++;
}
}
r.Notes.Add($"[Offshore] coast shelf: {r.ShelfCells:N0} below-sea cells remapped " +
$"(strength {cfg.ShelfStrength:F3}, scale {cfg.ShelfScaleM:F0} m) — held strictly below sea.");
}
if (!isletsOn) return r;
r.Tag = new bool[mapSize, mapSize];
r.Hemi = new byte[mapSize, mapSize];
bool faithful = s.Mode == OffshoreMode.Faithful;
bool guardsOn = !faithful && (s.MinIslandAreaFrac > 0f || s.MinSeparationFrac > 0f || s.MaxIslandAreaFrac > 0f);
float crest = sea + WorldScale.RawFromMetres(s.CrestM); // seaHere + OFFSHORE_ISLAND_H_M / 251f
// ═══ 2. THE ORGANIC LAYER — the one island mechanism ═══
{
FastNoiseLite noise = TerrainNoise.CreateModulation(seed, s.SeedOffset, s.FreqPerMapWidth, scale);
// ⭐ Calibrate against the field's ACTUAL distribution, not the theoretical [1,1]:
// sample on a stride grid and take the quantile. Deterministic from the seed, and it
// makes the density dial mean what it says whatever FastNoiseLite's range turns out
// to be. Verbatim: stride 8, (side)² samples, (noise + 1) * 0.5.
float[] samples = CalibrationSamples(noise, mapSize);
float thrN = IslandFalloff.CalibrateThreshold(samples, s.Density);
float thrS = faithful ? thrN : IslandFalloff.CalibrateThreshold(samples, s.DensitySouth);
r.ThresholdNorth = thrN; r.ThresholdSouth = thrS;
float mid = mapSize * 0.5f;
float band = MathF.Max(1f, s.HemisphereBlendHalfWidth * mapSize);
for (int x = 0; x < mapSize; x++)
{
for (int y = 0; y < mapSize; y++)
{
float h = height[x, y];
if (h >= sea) continue; // below-sea ONLY — never touches land
float ambientDepthM = WorldScale.MetresFromRaw(sea - h);
float zone = IslandFalloff.OffshoreZoneWeight(
ambientDepthM, preTrench[x, y],
MathF.Abs(x - centerX) / halfSpan, MathF.Abs(y - centerY) / halfSpan,
s.MinDepthM, s.DepthFeatherM, s.MinFalloff, s.FalloffFeather,
s.TrenchInner, s.TrenchOuter);
if (zone <= 0f) continue;
float v = (noise.GetNoise2D(x, y) + 1.0f) * 0.5f;
// The threshold: one number in faithful mode; north/south blended smoothly
// across the midline in the reshape, so a straddling island is not sliced.
float thr = faithful ? thrN : BlendedThreshold(y, mid, band, thrN, thrS);
float blob = (faithful
? IslandFalloff.OffshoreBlob(v, thr)
: IslandFalloff.RigidBlob(v, thr, s.CoreFraction, s.EdgeSharpness)) * zone;
if (blob <= 0f) continue;
// ⭐ LERP TOWARD THE CREST, never add — surfaces at any ambient depth.
float before = h;
h = h + (crest - h) * blob; // Mathf.Lerp, written out
height[x, y] = h;
if (guardsOn) r.RaisedOrigin.Add((x, y, before)); // for the submerged-bump guard
if (before < sea && h >= sea)
{
r.LiftedOrganic++;
r.Tag[x, y] = true;
r.Hemi[x, y] = OffshoreAnalysis.HemisphereOfRow(y, mapSize);
r.LiftedOrigin.Add((x, y, before));
}
}
}
r.Notes.Add($"[Offshore] organic ({(faithful ? "faithful" : "reshaped")}): threshold N {thrN:F4}" +
$"{(faithful ? "" : $" S {thrS:F4}")} from {samples.Length:N0} samples " +
$"(range {Min(samples):F3}..{Max(samples):F3}); lifted {r.LiftedOrganic:N0} cells above sea.");
}
// ═══ 3. THE SLOP GUARDS — reshape only; over everything the pass raised ═══
//
// Four kinds of slop, three component rules and one location rule:
// (a) SPECKS a local maximum that barely clears the threshold surfaces a cap of a few
// cells — not an island. Reverted by MEMBERSHIP (a speck inside a real
// island's skirt is still a speck).
// (b) BLOBS a superlevel region that merged several maxima into one sprawling
// landmass. Reverted by membership. The cap sits well above the natural
// size so it is a net, not a sculptor; how often it bites is reported.
// (c) CLUSTERS two islands whose shores are closer than the minimum separation read as
// one; the SMALLER goes (greedy by size, so the largest of a cluster stays).
// (d) SUBMERGED BUMPS — every blob with any weight lerps the seabed toward the crest
// whether or not it surfaces, so the ocean fills with shallow humps (the
// reference's character; the water pass would draw a reef field nobody
// asked for). Attributed by HUMP: a hump (connected raised region) that
// holds a kept island is that island's own skirt and stays; one that holds
// none goes back to seabed; a dropped island's own cap goes too.
// None of these places, shapes or counts anything. (The faithful control keeps all four;
// that is the reference.)
if (guardsOn && (r.RaisedOrigin.Count > 0 || r.LiftedOrigin.Count > 0))
{
double area = (double)mapSize * mapSize;
long minCells = s.MinIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MinIslandAreaFrac * area)) : 0;
long maxCells = s.MaxIslandAreaFrac > 0f ? Math.Max(1L, (long)Math.Round(s.MaxIslandAreaFrac * area)) : long.MaxValue;
int minSep = s.MinSeparationFrac > 0f ? Math.Max(1, (int)Math.Round(s.MinSeparationFrac * mapSize)) : 0;
var pre = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize, out int[] compId);
(r.PreGuardNorth, r.PreGuardSouth) = OffshoreAnalysis.CountByHemisphere(pre);
var dropped = new HashSet<int>();
var byId = new Dictionary<int, IslandComponent>();
foreach (var c in pre) byId[c.Id] = c;
// (a) specks and (b) blobs — by size.
foreach (var c in pre)
{
if (c.Cells < minCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.SpecksNorth, ref r.SpecksSouth); }
else if (c.Cells > maxCells) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.BlobsNorth, ref r.BlobsSouth); }
}
// (c) clusters — greedy by size among the survivors of (a)/(b).
if (minSep > 0)
{
var survivors = new List<IslandComponent>();
foreach (var c in pre) if (!dropped.Contains(c.Id)) survivors.Add(c);
survivors.Sort((a, b) => b.Cells.CompareTo(a.Cells)); // largest first
var boundary = OffshoreAnalysis.BoundaryCells(r.Tag, compId, mapSize, r.LiftedOrigin);
var kept = new List<IslandComponent>();
foreach (var c in survivors)
{
bool tooClose = false;
foreach (var k in kept)
{
if (OffshoreAnalysis.BoxGap(c, k) >= minSep) continue; // cannot be closer than the bbox gap
if (OffshoreAnalysis.MinChebyshev(boundary[c.Id], boundary[k.Id], minSep) < minSep) { tooClose = true; break; }
}
if (tooClose) { dropped.Add(c.Id); Bump(r, c.Hemisphere, ref r.ClustersNorth, ref r.ClustersSouth); }
else kept.Add(c);
}
}
// (d) THE SUBMERGED BUMPS — by HUMP, not by box. A hump is one 8-connected region of
// raised cells (the blob's footprint, surfaced or not). A hump that holds a kept island
// is that island's own skirt and stays whole; a hump that holds none is a reef nobody
// asked for and goes back to seabed whole. A DROPPED island's own cap — its bbox plus a
// margin — is reverted even inside a kept hump, or its rim would stay as a hollow ring
// beside its neighbour (the first probe plate showed exactly those ghost outlines).
var raised = new bool[mapSize, mapSize];
foreach (var (x, y, _) in r.RaisedOrigin) raised[x, y] = true;
var humpId = new int[mapSize * mapSize];
var humpKept = new List<bool> { false }; // index 0 unused
{
var stack = new Stack<int>();
foreach (var (sx, sy, _) in r.RaisedOrigin)
{
if (humpId[sx * mapSize + sy] != 0) continue;
int id = humpKept.Count; humpKept.Add(false);
humpId[sx * mapSize + sy] = id; stack.Push(sx * mapSize + sy);
bool kept = false;
while (stack.Count > 0)
{
int cur = stack.Pop(); int cx = cur / mapSize, cy = cur % mapSize;
if (r.Tag[cx, cy] && !dropped.Contains(compId[cur])) kept = true;
for (int dx = -1; dx <= 1; dx++)
{
int nx = cx + dx; if (nx < 0 || nx >= mapSize) continue;
for (int dy = -1; dy <= 1; dy++)
{
int ny = cy + dy; if (ny < 0 || ny >= mapSize || (dx == 0 && dy == 0)) continue;
if (!raised[nx, ny]) continue;
int ni = nx * mapSize + ny;
if (humpId[ni] != 0) continue;
humpId[ni] = id; stack.Push(ni);
}
}
}
humpKept[id] = kept;
}
}
var dropBoxes = new List<(int x0, int y0, int x1, int y1)>();
foreach (var c in pre)
{
if (!dropped.Contains(c.Id)) continue;
int w = c.MaxX - c.MinX + 1, hgt = c.MaxY - c.MinY + 1;
int margin = Math.Max(4, Math.Max(w, hgt) / 2);
dropBoxes.Add((c.MinX - margin, c.MinY - margin, c.MaxX + margin, c.MaxY + margin));
}
bool InDropBox(int x, int y)
{
foreach (var (x0, y0, x1, y1) in dropBoxes)
if (x >= x0 && x <= x1 && y >= y0 && y <= y1) return true;
return false;
}
long revertedLifted = 0, revertedRaised = 0;
foreach (var (x, y, h0) in r.LiftedOrigin)
{
if (!dropped.Contains(compId[x * mapSize + y])) continue;
height[x, y] = h0; r.Tag[x, y] = false; r.Hemi[x, y] = OffshoreAnalysis.HemiNone;
revertedLifted++;
}
foreach (var (x, y, h0) in r.RaisedOrigin)
{
if (r.Tag[x, y]) continue; // a kept island's own surfaced cell
bool keepIt = humpKept[humpId[x * mapSize + y]] && !InDropBox(x, y);
if (keepIt) continue;
if (height[x, y] > h0) { height[x, y] = h0; revertedRaised++; }
}
r.LiftedReverted = revertedLifted;
r.RaisedReverted = revertedRaised;
r.Notes.Add($"[Offshore] guards: of {pre.Count} islands (N {r.PreGuardNorth} / S {r.PreGuardSouth}) reverted " +
$"{r.SpecksNorth + r.SpecksSouth} specks (< {minCells:N0} cells), " +
$"{r.ClustersNorth + r.ClustersSouth} clustered (< {minSep} px from a larger island), " +
$"{r.BlobsNorth + r.BlobsSouth} blobs (> {(maxCells == long.MaxValue ? "" : maxCells.ToString("N0"))} cells) " +
$"— {revertedLifted:N0} surfaced cells, plus {revertedRaised:N0} submerged bump cells outside any kept island's hump.");
}
// ═══ PROVE THE MOAT ON THE FINISHED FIELD — in the pass, before anyone looks ═══
var comps = OffshoreAnalysis.Components(r.Tag, height, sea, mapSize);
(r.CountNorth, r.CountSouth) = OffshoreAnalysis.CountByHemisphere(comps);
r.Bridged = OffshoreAnalysis.BridgedCount(comps);
var (szMin, szMed, szMean, szMax, _) = OffshoreAnalysis.SizeSummary(comps, 0);
r.Notes.Add($"[Offshore] islands: {r.CountNorth} north, {r.CountSouth} south ({comps.Count} components, " +
$"{r.Bridged} bridged to mainland); lifted {r.LiftedOrganic - r.LiftedReverted:N0} cells net" +
$"{(r.RaisedReverted > 0 ? $", {r.RaisedReverted:N0} submerged debris cells reverted" : "")}; " +
$"size cells min {szMin} median {szMed} mean {szMean:F0} max {szMax}.");
if (r.Bridged > 0)
throw new InvalidOperationException(
$"[OffshorePass] MOAT VIOLATION: {r.Bridged} island(s) touch mainland land. Refusing.");
return r;
}
/// <summary>The reference's calibration sample: stride 8, (side)² samples, (noise + 1) · 0.5. Shared with the diagnosis.</summary>
public static float[] CalibrationSamples(FastNoiseLite noise, int mapSize)
{
const int stride = 8;
int side = mapSize / stride;
var samples = new float[side * side];
for (int i = 0; i < side; i++)
for (int j = 0; j < side; j++)
samples[i * side + j] = (noise.GetNoise2D(i * stride, j * stride) + 1f) * 0.5f;
return samples;
}
/// <summary>The north/south threshold, smoothstep-blended across the midline. Shared with the diagnosis.</summary>
public static float BlendedThreshold(int y, float mid, float band, float thrN, float thrS)
{
float t = Math.Clamp((y - mid) / band * 0.5f + 0.5f, 0f, 1f);
t = t * t * (3f - 2f * t);
return thrN + (thrS - thrN) * t;
}
private static void Bump(Result r, byte hemi, ref int north, ref int south)
{
if (hemi == OffshoreAnalysis.HemiNorth) north++; else south++;
}
private static float Min(float[] a) { float m = float.MaxValue; foreach (float v in a) if (v < m) m = v; return m; }
private static float Max(float[] a) { float m = float.MinValue; foreach (float v in a) if (v > m) m = v; return m; }
}
/// <summary>
/// The islet layer's ledger, carried on <c>Pass1Result</c> for the report: thresholds, the
/// pre-guard island count, what each guard reverted, the final count. Numbers only — the tag
/// arrays are carried separately.
/// </summary>
public sealed class OffshoreLedger
{
public float ThresholdNorth, ThresholdSouth;
public int PreGuardNorth, PreGuardSouth;
public int SpecksNorth, SpecksSouth, ClustersNorth, ClustersSouth, BlobsNorth, BlobsSouth;
public long LiftedOrganic, LiftedReverted, RaisedReverted;
public int CountNorth, CountSouth;
}
}