Presentation only. No generation file is touched — Topography, TerrainNoise, IslandFalloff, Pass1Result, TerrainGenConfig and GenerationScale are all unchanged. Batch naming, fixed in code. The prefix is the AUTHORING TASK number, not a running counter: 04_review means "the batch task 04 authored", not "the fifth batch". It had already drifted — tasks 02 and 03 produced 00_smoke through 05_trophy_10240 across two tasks, so no folder name said which task made what. Now the task number is an explicit argument to ToolingPaths.BatchRoot(taskNumber, descriptor), which composes the prefix itself and REFUSES a descriptor that carries its own. All three tools take it as ISLA_TASK. Verified: ISLA_BATCH=05_foo is refused with a stated reason and exit 2, and creates no folder. Also fixed: an exception out of _Ready does not stop Godot — it logs and the process sits there with no main loop, so a misconfigured run HUNG rather than failing. A hang looks like slow work, which is worse than a crash. The batch tools now catch, print what was refused, and exit non-zero. Grayscale mode: normalize a field to its own [min,max]. This is the one place per-image normalization is correct — everywhere else anchors are fixed so images compare, but here the point is to see one field at full contrast. The range is printed and indexed so a shade reads back to a height. You cannot judge noise through a palette: a ramp bends the distribution, a hillshade adds shape the data does not have. The wide Costa-Rica palette, and the reframing the developer asked for: the pretty map is the WIDE GRADIENT RENDERED FLAT, and relief is no longer the hero. On raw pass-1 noise a hillshade has nothing coherent to shade, so it renders fine fractal bumpiness as fuzz that actively hides the elevation the colour is showing. Strong hillshade is retained as diagnostic_relief and labelled a dev view — its bumpiness is exaggerated slope, not extra terrain. Subtle relief is kept for comparison, with the honest note that it still fuzzes until Phase 2 carves coherent landforms. Legend: a colour bar with ticks drawn from the palette's own ramp, so it cannot drift from the map beside it. Ticks are RELATIVE height and the image says NOT METRES — the conversion is Phase 2's, and labelling it "m" would invent a fact in the artefact a reader most trusts. Text comes from a 5x7 bitmap font written for this, specifically so a legend does not drag in the SubViewport capture path.
216 lines
11 KiB
C#
216 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using Godot;
|
|
using IslaApocalypse.Core;
|
|
|
|
namespace IslaApocalypse.Tools
|
|
{
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
public partial class TerrainGenTool : Node
|
|
{
|
|
/// <summary>
|
|
/// PINNED POSITIVE SEEDS. Pinned, not random, so a batch is reproducible and two batches are
|
|
/// comparable — a standing convention. Positive only.
|
|
/// </summary>
|
|
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);
|
|
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<string>();
|
|
|
|
// ═══ 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static List<(string, Action<TerrainGenConfig>)> 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_<name>/<seed>_<variant>/
|
|
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<string> 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** (`<seed>_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<int>();
|
|
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;
|
|
}
|
|
}
|
|
}
|