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_BATCH batch folder name (default 01_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());
int mapSize = EnvInt("ISLA_MAPSIZE", DefaultMapSize);
int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds);
string batch = EnvStr("ISLA_BATCH", "01_pass1_port");
bool skipRaw = EnvStr("ISLA_SKIP_RAW", "0") == "1";
bool ladder = EnvStr("ISLA_LADDER", "1") == "1";
var scale = new GenerationScale(mapSize);
string batchRoot = Path.Combine(ToolingPaths.BatchesRoot, 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())
{
var cfg = new TerrainGenConfig { MapSize = mapSize, Seed = seeds[0], VariantLabel = label };
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" };
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);
string png = HeightMapRenderer.SavePng(r, cfg.SeaLevel, Path.Combine(dir, "height.png"));
if (!skipRaw) HeightMapRenderer.SaveRaw(r, 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");
sb.AppendLine($"- **Colour ramp anchors (FIXED across the batch):** sea {HeightMapRenderer.RampBottomHeight} .. 0.15, land 0.15 .. {HeightMapRenderer.RampTopHeight}");
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;
}
}
}