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; } } }