using System; using System.Collections.Generic; using System.IO; using System.Text; using Godot; using IslaApocalypse.Core; namespace IslaApocalypse.Tools { /// /// Builds the Phase-1 REVIEW batch: plain grayscale views so the noise itself can be judged, the /// wide gradient with a legend as the pretty map, and two labelled relief comparisons. /// /// ⚠ PRESENTATION ONLY. Reads existing `.f32` dumps; generates nothing unless a dump is missing. /// /// ⚠ THE BATCH FOLDER IS `<task>_<descriptor>` AND THE TASK NUMBER IS EXPLICIT. /// It is passed as ISLA_TASK and composed by , which /// refuses a descriptor carrying its own prefix. The prefix names the task that AUTHORED the /// batch; it is not a running counter. → Tools/README.md. /// /// ═══ RUNNING IT ═══ /// /// Godot_v4.7.2-stable_mono_linux.x86_64 --headless \ /// --path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/ReviewBatchTool.tscn /// /// ISLA_TASK authoring task number (default 4) /// ISLA_BATCH descriptor, NO prefix (default "review") /// ISLA_SOURCE batch holding the .f32 (default 01_pass1_port) /// ISLA_MAPSIZE side in columns (default 2048) /// ISLA_SEEDS comma-separated positive (default: the 4 pinned seeds) /// public partial class ReviewBatchTool : Node { private static readonly int[] DefaultSeeds = { 1063685222, 20260819, 777001, 424242 }; /// Top of the legend's scale, in raw height units. Covers the measured max (1.415). private const float LegendTop = 1.45f; public override void _Ready() { // ⚠ An exception thrown out of _Ready does NOT stop the engine — Godot logs it and the // process sits there with no main loop to end it, so a misconfigured batch run HANGS // instead of failing. Measured, not assumed: a deliberately bad ISLA_BATCH hung until // killed. A tool that hangs on bad input is worse than one that crashes, because a // hang looks like slow work. So: catch, say what went wrong, exit non-zero. try { Run(); } catch (Exception e) { GD.PrintErr("=================================================================="); GD.PrintErr($" REFUSED: {e.Message}"); GD.PrintErr("=================================================================="); GetTree().Quit(2); } } private void Run() { ToolingPaths.Configure(OS.GetUserDataDir()); int task = EnvInt("ISLA_TASK", 4); string descr = EnvStr("ISLA_BATCH", "review"); string source = EnvStr("ISLA_SOURCE", "01_pass1_port"); int mapSize = EnvInt("ISLA_MAPSIZE", 2048); int[] seeds = EnvSeeds("ISLA_SEEDS", DefaultSeeds); string batchRoot = ToolingPaths.BatchRoot(task, descr); // ⚠ composed, never free-form string sourceRoot = Path.Combine(ToolingPaths.BatchesRoot, source); DirAccess.MakeDirRecursiveAbsolute(batchRoot); DirAccess.MakeDirRecursiveAbsolute(ToolingPaths.BatchScratch(batchRoot)); GD.Print("=================================================================="); GD.Print(" PHASE-1 REVIEW — raw grayscale, the wide gradient, labelled relief"); GD.Print("=================================================================="); GD.Print($"batch : {batchRoot}"); GD.Print($"source : {sourceRoot}"); GD.Print($"MapSize : {mapSize} seeds: {string.Join(", ", seeds)}"); GD.Print("=================================================================="); var notes = new List(); int primary = seeds[0]; // ---- 1. the raw crown-jewel noise, uncoloured (base-noise-only ablation) ---- float[,] baseNoise = Load(sourceRoot, $"{primary}_ab1_base_only", mapSize); if (baseNoise != null) notes.Add(Gray(baseNoise, mapSize, batchRoot, "1_noise_grayscale", primary, "the raw crown-jewel noise, before any island shaping")); else GD.PrintErr($" ⚠ no ab1_base_only dump for {primary} at {mapSize} — 1_noise_grayscale SKIPPED, not faked."); // ---- 2..5, all from the full pass-1 field ---- foreach (int seed in seeds) { float[,] h = Load(sourceRoot, $"{seed}_full", mapSize); if (h == null) { GD.PrintErr($" ⚠ no full dump for {seed} at {mapSize} — generating deterministically."); h = Topography.Generate(new TerrainGenConfig { MapSize = mapSize, Seed = seed }).Height; } notes.Add(Gray(h, mapSize, batchRoot, "2_height_grayscale", seed, "the same noise, shaped into an island")); notes.Add(Gradient(h, mapSize, batchRoot, "3_gradient_flat", seed)); // The two relief comparisons are for the primary seed only — they answer a question // about the LOOK, and asking it four times is the iteration fatigue the batch // discipline warns about. if (seed == primary) { notes.Add(Relief(h, mapSize, batchRoot, "4_subtle_relief", seed, "subtle_relief")); notes.Add(Relief(h, mapSize, batchRoot, "5_diagnostic_relief", seed, "diagnostic_relief")); } } WriteIndex(batchRoot, source, mapSize, seeds, notes); GD.Print("\n=================================================================="); GD.Print($" DONE — {notes.Count} images in {batchRoot}"); GD.Print("=================================================================="); GetTree().Quit(0); } private static float[,] Load(string sourceRoot, string folder, int mapSize) => HeightField.Load(Path.Combine(sourceRoot, folder, "height.f32"), mapSize); private static string Gray(float[,] field, int mapSize, string batchRoot, string sub, int seed, string what) { ulong t0 = Time.GetTicksMsec(); string dir = Path.Combine(batchRoot, sub); DirAccess.MakeDirRecursiveAbsolute(dir); string path = Path.Combine(dir, $"{seed}.png"); var (min, max) = GrayscaleRenderer.SavePng(field, mapSize, path); ulong ms = Time.GetTicksMsec() - t0; GD.Print($" {sub}/{seed}.png black={min:F3} white={max:F3} {ms} ms ({what})"); return $"| `{sub}/{seed}.png` | {seed} | grayscale | black `{min:F3}` → white `{max:F3}` | {ms} ms |"; } private static string Gradient(float[,] h, int mapSize, string batchRoot, string sub, int seed) { ulong t0 = Time.GetTicksMsec(); string dir = Path.Combine(batchRoot, sub); DirAccess.MakeDirRecursiveAbsolute(dir); var look = new LookConfig { Name = "gradient_flat", Palette = ReliefPalette.Kind.CostaRica, HillshadeStrength = 0f, }; Image map = ReliefRenderer.Render(h, mapSize, look); Image withLegend = LegendRenderer.WithLegend(map, look.Palette, look.SeaLevel, LegendTop, $"SEED {seed}"); string path = Path.Combine(dir, $"{seed}.png"); withLegend.SavePng(path); ulong ms = Time.GetTicksMsec() - t0; GD.Print($" {sub}/{seed}.png wide gradient + legend, FLAT (no relief) {ms} ms"); return $"| `{sub}/{seed}.png` | {seed} | ⭐ wide gradient, flat + legend | CostaRica, no hillshade | {ms} ms |"; } private static string Relief(float[,] h, int mapSize, string batchRoot, string sub, int seed, string lookName) { ulong t0 = Time.GetTicksMsec(); string dir = Path.Combine(batchRoot, sub); DirAccess.MakeDirRecursiveAbsolute(dir); LookConfig look = null; foreach (LookConfig l in LookConfig.Variants()) if (l.Name == lookName) look = l; string path = Path.Combine(dir, $"{seed}.png"); ReliefRenderer.SavePng(h, mapSize, look, path); ulong ms = Time.GetTicksMsec() - t0; GD.Print($" {sub}/{seed}.png {look} {ms} ms"); return $"| `{sub}/{seed}.png` | {seed} | {lookName} | zex {look.ZExaggeration:F0}, strength {look.HillshadeStrength:F2} | {ms} ms |"; } private static void WriteIndex(string batchRoot, string source, int mapSize, int[] seeds, List rows) { var sb = new StringBuilder(); sb.AppendLine("# Batch — Phase 1 review: judge the noise, confirm the colour direction"); sb.AppendLine(); sb.AppendLine("**Presentation only.** Every image here is the *same terrain* as"); sb.AppendLine($"`{source}`, loaded from its `.f32` dumps. Nothing here can change the world — only how it"); sb.AppendLine("is drawn."); sb.AppendLine(); sb.AppendLine($"- **MapSize:** {mapSize} · **Seeds:** {string.Join(", ", seeds)}"); sb.AppendLine("- **Sea colour boundary:** 0.15 — a *colour* boundary, **not a water surface.** No water is modelled (Phase 2)."); sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); sb.AppendLine("## What each folder is"); sb.AppendLine(); sb.AppendLine("### `1_noise_grayscale` — the raw noise, uncoloured"); sb.AppendLine(); sb.AppendLine("**This is the crown jewel with nothing done to it.** No island, no falloff, no spine — just"); sb.AppendLine("the FastNoiseLite field the whole world is built from, normalized black-to-white. If the"); sb.AppendLine("noise itself is good or bad, this is where you can see it: a palette bends the value"); sb.AppendLine("distribution and a hillshade adds shape the data doesn't have, so neither can answer that"); sb.AppendLine("question. This one can."); sb.AppendLine(); sb.AppendLine("### `2_height_grayscale` — the same noise, shaped into an island"); sb.AppendLine(); sb.AppendLine("Identical noise, now with the island mask, the edge roughness, the southern sinker, the"); sb.AppendLine("Trench and the mountain spine applied. Still uncoloured, so you are seeing the SHAPE and"); sb.AppendLine("nothing else. Compare against `1_` to see exactly what the shaping did."); sb.AppendLine(); sb.AppendLine("### ⭐ `3_gradient_flat` — THE PRETTY MAP. This is the direction."); sb.AppendLine(); sb.AppendLine("The wide Costa-Rica-style gradient, **rendered flat — no hillshade at all**, with an"); sb.AppendLine("elevation legend down the right side. Colour alone carries the elevation."); sb.AppendLine(); sb.AppendLine("**⚠ The legend is in RELATIVE units, not metres.** Raw height runs sea `0.15` to peak"); sb.AppendLine("`~1.45`. The metres conversion is Phase 2's elevation profile; labelling this bar \"m\""); sb.AppendLine("now would be inventing a fact."); sb.AppendLine(); sb.AppendLine("**⚠ Honest expectation:** this island is mostly lowland — median land height `0.456`"); sb.AppendLine("against a `1.415` peak — so it reads green-to-yellow with the spine in orange, brown and"); sb.AppendLine("white. That is *accurate for a lowland island with a central range*, not a palette that"); sb.AppendLine("failed to use its range. The fuller Costa-Rica spread arrives when **Phase 2's"); sb.AppendLine("redistribution curve** moves the height distribution upward; the palette is built now so"); sb.AppendLine("Phase 2 inherits it rather than re-tuning it."); sb.AppendLine(); sb.AppendLine("### `4_subtle_relief` — the same gradient with gentle relief"); sb.AppendLine(); sb.AppendLine("What tasteful relief looks like (exaggeration 18, strength 0.30) versus the amped version."); sb.AppendLine(); sb.AppendLine("**⚠ Even this still shows fuzz, and that is the terrain, not the setting.** Raw pass-1"); sb.AppendLine("noise is fine-grained and incoherent — there is nothing yet for a light to model. Relief"); sb.AppendLine("comes into its own after **Phase 2's erosion carves coherent landforms**: ridges with"); sb.AppendLine("valleys between them, drainage that runs somewhere. Then a subtle hillshade has real shape"); sb.AppendLine("to light, and it will look like the reference maps."); sb.AppendLine(); sb.AppendLine("### ⚠ `5_diagnostic_relief` — A DEV TOOL. NOT A PRETTY MAP."); sb.AppendLine(); sb.AppendLine("Strong exaggeration (100) and strength (0.80). **This is for spotting artifacts** — a"); sb.AppendLine("centre-line crease, a stair-step, a seam, a discontinuity — by making slope impossible to"); sb.AppendLine("miss. **Its bumpiness is exaggerated SLOPE, not extra terrain.** The terrain in this image"); sb.AppendLine("is identical to `3_gradient_flat`; only the lighting lies. Do not judge the world by it,"); sb.AppendLine("and do not show it as the deliverable."); sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); sb.AppendLine("## ⭐ What Phase 2 changes"); sb.AppendLine(); sb.AppendLine("The fine bumpiness you see everywhere is **raw fractal noise** — statistically correct and"); sb.AppendLine("geologically meaningless. Phase 2's **redistribution curve** reshapes the height"); sb.AppendLine("distribution (flats become flat, peaks become peaks) and **hydraulic erosion** carves"); sb.AppendLine("drainage into it, turning that fuzz into **coherent farmland, ridgelines and river"); sb.AppendLine("valleys**. The colour direction settled here is what those landforms will be painted with."); sb.AppendLine(); sb.AppendLine("---"); sb.AppendLine(); sb.AppendLine("## Images"); sb.AppendLine(); sb.AppendLine("| File | Seed | View | Settings / range | Time |"); sb.AppendLine("|---|---|---|---|---|"); foreach (string r in rows) sb.AppendLine(r); sb.AppendLine(); sb.AppendLine("`scratch/` is persistent and is never cleaned."); sb.AppendLine(); sb.AppendLine("> **Folder naming:** the `04_` prefix is the number of the TASK that authored this batch —"); sb.AppendLine("> not a running counter. Everything task 04 produces is `04_*`."); using var f = Godot.FileAccess.Open(Path.Combine(batchRoot, "INDEX.md"), Godot.FileAccess.ModeFlags.Write); if (f == null) { GD.PrintErr("could not write INDEX.md"); return; } f.StoreString(sb.ToString()); } 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 o = new List(); foreach (string p in v.Split(',', StringSplitOptions.RemoveEmptyEntries)) if (int.TryParse(p.Trim(), out int s) && s > 0) o.Add(s); return o.Count > 0 ? o.ToArray() : fallback; } } }