using System; using System.Collections.Generic; using System.IO; using System.Text; using Godot; using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// /// The Phase-1 generation entry point: runs a seed batch plus the ablation ladder, writes /// hypsometric PNGs and raw height dumps into the batch layout, and quits. /// /// ═══ RUNNING IT ═══ /// /// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn /// /// ⚠ `--headless` is correct HERE and will stay correct as long as the renderer writes a /// Godot.Image directly. It awaits no render frames. The moment anything composites a `_Draw` /// overlay through a SubViewport, this becomes an `xvfb-run` job — see Tools/README.md. /// /// ═══ CONFIGURATION (all optional; every path is env-overridable per the tooling rails) ═══ /// /// ISLA_MAPSIZE map side in columns (default 2048 — iteration size) /// ISLA_SEEDS comma-separated positive ints (default: the pinned batch below) /// ISLA_TASK authoring task number (default 2) /// ISLA_BATCH descriptor, NO prefix (default "pass1_port") /// ISLA_OUTPUT_DIR where batches/ lives (Core/ToolingPaths) /// ISLA_SKIP_RAW "1" to skip the .f32 dumps /// ISLA_LADDER "0" to skip the ablation ladder (for a full-size confirmation pass) /// public partial class TerrainGenTool : Node { /// /// PINNED POSITIVE SEEDS. Pinned, not random, so a batch is reproducible and two batches are /// comparable — a standing convention. Positive only. /// private static readonly int[] DefaultSeeds = { 1063685222, 20260819, 777001, 424242 }; private const int DefaultMapSize = 2048; public override void _Ready() { ToolingPaths.Configure(OS.GetUserDataDir()); // ⭐ rivers/01: batches are namespaced by chat. The default is this tool's AUTHORING chat, // so re-running it reproduces its own batch in place; ISLA_CHAT redirects a run to another // chat's namespace — which is what keeps an acceptance run from overwriting its own anchor. ToolingPaths.ConfigureChat(EnvStr(ToolingPaths.ChatVar, "chat1")); int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); int task = EnvInt("ISLA_TASK", 2); // the task that authored this batch string batch = EnvStr("ISLA_BATCH", "pass1_port"); bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1"; bool ladder = EnvStr("ISLA_LADDER", "1") == "1"; var scale = new GenerationScale(mapSize); // ⚠ Composed by BatchRoot, never free-form. → Tools/README.md, "the prefix is the // AUTHORING TASK number, not a counter". string batchRoot = ToolingPaths.BatchRoot(task, batch); string scratch = ToolingPaths.BatchScratch(batchRoot); DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(scratch); // persistent; never cleaned GD.Print("=================================================================="); GD.Print(" PASS-1 TERRAIN GENERATION — the crown-jewel port (Phase 1)"); GD.Print("=================================================================="); GD.Print($"MapSize : {mapSize} (scaleFactor {scale.ScaleFactor:F3})"); GD.Print($"noise : {TerrainNoise.Describe(0, scale).Replace(" · seed 0", "")}"); GD.Print($"seeds : {string.Join(", ", seeds)}"); GD.Print($"batch : {batchRoot}"); GD.Print("------------------------------------------------------------------"); GD.Print(ToolingPaths.Describe()); GD.Print("=================================================================="); var rows = new List(); // ═══ 1. THE ABLATION LADDER — first seed only, one element added per rung ═══ // // This is the port-fidelity check: six simultaneous changes cannot be judged by their // sum, so each element is switched on in the reference's own order and looked at alone. if (ladder) { GD.Print("\n--- ABLATION LADDER (seed " + seeds[0] + ") ---"); foreach (var (label, mutate) in Ladder()) { // ⭐⭐ rivers/01 — FAMILY-OFF PINNED. This tool AUTHORED `02_pass1_port`, the Phase-1 // regression anchor that survived the re-baseline. If it picked up the family-on // defaults it could no longer regenerate its own dump, and the last link between // today's generator and the Phase-1 port would break silently. var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label }.WithFamilyOff(); mutate(cfg); rows.Add(RunOne(cfg, batchRoot, skipRaw)); } } else GD.Print("\n--- ablation ladder skipped (ISLA_LADDER=0) ---"); // ═══ 2. THE SEED BATCH — the full six-element config across pinned seeds ═══ GD.Print("\n--- SEED BATCH (full pass-1) ---"); foreach (int seed in seeds) { var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seed, VariantLabel = "full" }.WithFamilyOff(); // ⭐ see the ladder above rows.Add(RunOne(cfg, batchRoot, skipRaw)); } WriteIndex(batchRoot, mapSize, scale, seeds, rows); GD.Print("\n=================================================================="); GD.Print($" DONE — {rows.Count} maps in {batchRoot}"); GD.Print("=================================================================="); GetTree().Quit(0); } /// /// The ablation rungs, in the reference's execution order. Each rung ADDS one element to the /// one before it, so a difference between adjacent images is attributable to exactly one /// ported element. /// private static List<(string, Action)> Ladder() => new() { ("ab1_base_only", c => { c.IslandFalloff = false; c.EdgeNoise = false; c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }), ("ab2_falloff", c => { c.EdgeNoise = false; c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }), ("ab3_edge", c => { c.SouthernSinker = false; c.Trench = false; c.MountainSpine = false; }), ("ab4_sinker", c => { c.Trench = false; c.MountainSpine = false; }), ("ab5_trench", c => { c.MountainSpine = false; }), ("ab6_spine_full", c => { /* everything on — identical to the "full" variant */ }), }; private static string RunOne(TerrainGenConfig cfg, string batchRoot, bool skipRaw) { Pass1Result r = Topography.Generate(cfg); // batches/NN_/_/ string dir = Path.Combine(batchRoot, $"{cfg.Seed}_{cfg.VariantLabel}"); DirAccess.MakeDirRecursiveAbsolute(dir); // The generator's own quick-look uses the default 'atlas' presentation, so a generation // run and a beauty render are coloured identically and can be compared directly. var look = new LookConfig { SeaLevel = cfg.SeaLevel }; ReliefRenderer.SavePng(r.Height, r.MapSize, look, Path.Combine(dir, "height.png")); if (!skipRaw) HeightField.Save(r.Height, r.MapSize, Path.Combine(dir, "height.f32")); float land = r.LandFraction(cfg.SeaLevel); GD.Print($" {cfg.VariantLabel,-16} seed {cfg.Seed,-11} " + $"h[{r.HMinSeed,7:F3} .. {r.HMaxSeed,6:F3}] land {land * 100,5:F1}% {r.ElapsedMs,5} ms"); return $"| `{cfg.Seed}_{cfg.VariantLabel}` | {cfg.Seed} | {cfg.VariantLabel} | " + $"{r.HMinSeed:F3} | {r.HMaxSeed:F3} | {land * 100:F1}% | {r.ElapsedMs} ms |"; } private static void WriteIndex(string batchRoot, int mapSize, GenerationScale scale, int[] seeds, List rows) { var sb = new StringBuilder(); sb.AppendLine("# Batch — pass-1 port (Phase 1)"); sb.AppendLine(); sb.AppendLine("Raw pass-1 terrain from the ported crown-jewel noise. **No curve, no erosion, no"); sb.AppendLine("rivers, no water, no crater, no biomes.** Each folder holds `height.png` (hypsometric)"); sb.AppendLine("and `height.f32` (raw float32, x-major, for byte-level comparison)."); sb.AppendLine(); sb.AppendLine($"- **MapSize:** {mapSize} (scaleFactor {scale.ScaleFactor:F3})"); sb.AppendLine($"- **Noise:** {TerrainNoise.Describe(0, scale).Replace(" · seed 0", "")}"); sb.AppendLine($"- **Sea threshold (visualization only):** 0.15"); var (landLo, landHi) = ReliefPalette.LandAnchors; sb.AppendLine($"- **Palette anchors (FIXED across the batch):** land {landLo} .. {landHi}, sea compressed by d/(d+{ReliefPalette.SeaCompression})"); sb.AppendLine($"- **Seeds:** {string.Join(", ", seeds)}"); sb.AppendLine(); sb.AppendLine("## What to look at"); sb.AppendLine(); sb.AppendLine("**The ablation ladder** (`ab1`..`ab6`, all on seed " + seeds[0] + ") adds one ported element per"); sb.AppendLine("rung, in the reference's execution order — so any difference between adjacent images is"); sb.AppendLine("attributable to exactly one element:"); sb.AppendLine(); sb.AppendLine("| Rung | Adds | Expect to see |"); sb.AppendLine("|---|---|---|"); sb.AppendLine("| `ab1_base_only` | base noise | featureless fractal field, no island, no coast |"); sb.AppendLine("| `ab2_falloff` | island mask + `Pow(·,2.5)` | an island appears, smooth-edged |"); sb.AppendLine("| `ab3_edge` | edge roughness | the coastline goes jagged — **and only the coastline** |"); sb.AppendLine("| `ab4_sinker` | southern sinker | the bottom 25% sinks; southern land bridges break up |"); sb.AppendLine("| `ab5_trench` | the Trench | a hard ocean border on all four edges |"); sb.AppendLine("| `ab6_spine_full` | mountain spine | a ridge up the centre, fading out toward the south |"); sb.AppendLine(); sb.AppendLine("**The seed batch** (`_full`) is the complete six-element pass-1 across pinned seeds."); sb.AppendLine(); sb.AppendLine("> ⚠ **The seabed reads raw, and that is expected.** The submarine coast shelf and the"); sb.AppendLine("> offshore islets are DEFERRED to Phase 2 — they act only below sea level and are judged"); sb.AppendLine("> once water renders. Steep, plain bathymetry here is not a bug."); sb.AppendLine(); sb.AppendLine("## Results"); sb.AppendLine(); sb.AppendLine("| Folder | Seed | Variant | h min | h max | land % | time |"); sb.AppendLine("|---|---|---|---|---|---|---|"); foreach (string row in rows) sb.AppendLine(row); sb.AppendLine(); sb.AppendLine("`scratch/` is persistent and is never cleaned."); string index = Path.Combine(batchRoot, "INDEX.md"); using var f = Godot.FileAccess.Open(index, Godot.FileAccess.ModeFlags.Write); if (f == null) { GD.PrintErr($"could not write {index}"); return; } f.StoreString(sb.ToString()); } // ---- 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 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; } } }