Ports MapGenerator.GenerateTopography's pass-1 loop (reference ~:553-619 at tag
pre-rewrite-reference / ab78883) into Tools/. Working code and its tuned constants
carried over verbatim, read from source rather than re-derived from a design
summary (D-050). Six elements, in the reference's execution order — the order is
load-bearing: the sinker lands before the Pow (superlinear), the Trench after it
(a raw additive wall), preTrenchFalloff captured between them.
The fractal config is now PINNED. The reference set only NoiseType/Seed/Frequency,
so the island's entire fractal character was Godot 4.7.1 constructor defaults that
nothing recorded. Measured on 4.7.2 (NoiseDefaultsProbe): Fbm / 5 / 0.5 / 2.0 / 0.0
— identical to the 4.7.1 values, so pinning reproduces the look with no delta. The
terrain no longer depends on an engine default, and the probe makes a future drift
visible instead of silent.
Two deliberate departures from the reference's literals, both flagged in-code:
- the latitude noise offset is expressed in map widths, not the reference's raw
+1000 px, so a resize no longer samples a different slice of the noise field
(chat1/00 §4.2). Pinned to 1000/8192 so the 8K realization is unchanged.
- the `if (temperature < 0.65f)` spine gate is dropped as the provable no-op it
is — southernFade already reaches exactly 0 at 0.65. Bit-identical.
The reference's "temperature" is ported as a latitude field and deliberately NOT
named temperature: it is stage-1-local input to terrain geometry, and climate is
stage 3 (D-049 §2, D-056). The ±0.1 wobble is kept — it is what stops the spine's
southern terminus being a ruler-straight line.
Deferred to Phase 2 with the seam open: coast shelf and offshore islets (below-sea,
judged once water renders). Pass1Result exposes PreTrenchFalloff at their exact
consumption point, plus HMaxSeed for the seed-dependent curve.
Render is a direct Godot.Image, not the SubViewport capture path — that machinery
exists to composite vector overlays and is why --headless hangs; Phase 1 has none.
Hypsometric above 0.15, bathymetric below, fixed ramp anchors so seeds and ablation
rungs are comparable. No biome words anywhere.
Verified: land fraction 51.5% identical at 1024, 2048 and 10240 — the scaling
discipline holds across a 10x resize. Full-size 10240 pass: 36.3 s.
No curve, erosion, rivers, water, crater, biomes, roads, or mesher.
9.4 KiB
Tools — the offline generator
Holds: the world generator and its diagnostics. Everything that produces a blueprint, and nothing that consumes one at play time.
Boundary — the tools wall:
⚠⚠ TOOLS MAY NOT REFERENCE
Client/. THE DEPENDENCY RUNS ONE WAY, OR THE WALL IS NOT A WALL.No UI, no player controllers, no shaders, no rendering code.
Tools/may useCore/.Two reasons, both from
Design - Tooling - Offline Generation.md: a lighter shipped client, and — less obviously — not handing players the world-generation logic. Shipping the generator lets it be reverse-engineered into an unfair map preview, with the whole island's layout, every settlement and every route known before anyone sets foot on the beach.Tools emit data; they never write to a live save. A generator that could touch live server state is a corruption risk for no benefit.
What is here
The generator — pass 1 (Phase 1)
Ported from the reference's MapGenerator.GenerateTopography pass-1 loop at tag
pre-rewrite-reference (ab78883). This is the crown-jewel port: working code and its tuned
constants, carried over verbatim — not re-derived from a design summary (→ D-050,
Design - Rewrite - Extraction Manifest.md).
| File | What it is |
|---|---|
Scripts/TerrainNoise.cs |
⭐ The FastNoiseLite config, every fractal property pinned explicitly |
Scripts/Topography.cs |
⭐⭐ Pass 1 — the six elements, in the reference's execution order |
Scripts/IslandFalloff.cs |
SmoothAbs + CREST_EPSILON (pass-1 half of the reference file) |
Scripts/Pass1Result.cs |
The height field and the Phase-2 seams |
Scripts/TerrainGenConfig.cs |
Config + the per-element ablation toggles |
Scripts/HeightMapRenderer.cs |
Hypsometric PNG + raw .f32 dump |
Scripts/TerrainGenTool.cs |
Batch entry point (ladder + seed batch + INDEX.md) |
Scenes/TerrainGenTool.tscn |
Run this |
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/TerrainGenTool.tscn
ISLA_MAPSIZE (default 2048) · ISLA_SEEDS (comma-separated, positive) · ISLA_BATCH ·
ISLA_SKIP_RAW=1 · ISLA_LADDER=0
The six ported elements, in order — the order is load-bearing, not an implementation detail:
- wobbled latitude scalar —
y/MapSizeplus a ±0.1 low-frequency wobble - island falloff/mask — squircle + ellipse, 50/50, then
Pow(·, 2.5f) - edge roughness —
noise(x·2.5, y·2.5), modulated by the squircle so it bites only at the rim - southern sinker — bottom 25%, before the power (its effect is superlinear)
- the Trench — map-anchored, additive on both axes, after the power
- mountain spine —
SmoothAbscrest, cubic, southern fade, amplitude0.6f
⚠ The latitude scalar is NOT climate temperature.
The reference called it
temperatureand let biomes read the same array — a large part of how climate and terrain got fused. Here it is a stage-1-local latitude field that terrain geometry reads, and nothing else. Climate is stage 3 and classifies finished shape (D-049 §2, D-056). Do not alias, store, or rename this into a climate map.
Deferred to Phase 2, with the seam already open: the submarine coast shelf and the
offshore islets (reference ~:621-664). Both act only below sea level and are judged once water
renders. Pass1Result.PreTrenchFalloff is captured at the exact point they consume, and
Pass1Result.HMaxSeed is carried for the seed-dependent redistribution curve.
Not here at all: the curve, erosion, rivers, water bodies, the crater, biomes, roads, the mesher.
⚠ The render writes a Godot.Image directly — not the capture path
The reference captured maps through a SubViewport + a TextureRect._Draw, because it composited
vector overlays (roads, town markers, river polylines) onto the raster. That machinery awaits
render frames — which is exactly why unattended runs need xvfb-run and why --headless hangs on
it.
Phase 1 has no overlays. A heightmap is a raster, so it is written per-pixel into an Image and
saved: no viewport, no frame awaits, no display. Do not reintroduce the capture path by habit —
it becomes correct the moment something vector needs compositing, and not before.
Diagnostics
Scenes/UserDirProbe.tscn— confirms this project'suser://is its own, not the old prototype's. Exits non-zero on a clash.Scenes/NoiseDefaultsProbe.tscn— prints this engine'sFastNoiseLiteconstructor defaults and checks them against the recorded baseline. Run it after any Godot upgrade. It cannot change the terrain (TerrainNoisepins every value); it makes a drifting default visible instead of silent.
⭐ The conventions Phase 1's generator inherits
These were earned, not assumed — each one came out of the prototype's terrain arc.
→ Design - Tooling - Iteration and Batching.md.
1. xvfb-run for unattended generation — NOT --headless
⚠
--headlessHANGS on a real generation run.The snapshot capture awaits render frames, and there is no frame loop without a display. Measured, not assumed. A run left on
--headlessdoes not fail loudly; it sits there.
xvfb-run -a Godot_v4.7.2-stable_mono_linux.x86_64 --path ~/celerNexus/islaApocalypse-v2 <scene>
A windowed run on the desktop also invites being closed mid-run by whoever is using the machine.
The one exception is a job that awaits no frames — like UserDirProbe, which is why that one
documents --headless explicitly:
Godot_v4.7.2-stable_mono_linux.x86_64 --headless \
--path ~/celerNexus/islaApocalypse-v2 res://Tools/Scenes/UserDirProbe.tscn
If a job awaits a frame, it needs xvfb-run. When unsure, use xvfb-run — it is correct in
both cases and costs nothing.
2. Environment overrides — safe BY CODE, not by care
Every path a tool reads or writes resolves through Core/Scripts/ToolingPaths.cs, and each has an
environment override, so a batch cannot read over or write to the developer's live files:
| Variable | Overrides | Default |
|---|---|---|
ISLA_CONFIG_PATH |
the generation config file | user://config.json |
ISLA_BLUEPRINT_PATH |
blueprints read/written | user://blueprints |
ISLA_OUTPUT_DIR |
generation output (batches live under it) | user://output |
⭐ The point is that it is enforced by CODE, not by care. A rule that depends on an executor remembering it will eventually meet an executor who does not.
There is no second way to obtain these paths. Do not add one.
3. ⚠ File safety — permanent rules
Enforced by Core/Scripts/FileSafety.cs, which throws rather than advises.
- No deletion in the runtime root or anywhere under
batches/. - Intermediates persist in a
scratch/subfolder that is never cleaned. - The developer's placed files are never touched.
- Any deletion is named explicitly in the run report.
⚠ These came from a real incident: an executor admitted it had been deleting the developer's staged test blueprint. It was owned and fixed in code. That is why these are rules and not guidance.
4. Batch layout
batches/NN_<name>/<seed>_<variant>/
batches/NN_<name>/INDEX.md
batches/NN_<name>/scratch/ ← persistent; never cleaned
A/B comparisons are browsed by a human, and a flat directory of same-named PNGs is not
browsable. The INDEX.md is what makes a batch readable a week later.
⚠ Batches are OUTPUT. They live under ISLA_OUTPUT_DIR (i.e. under user://), not in this
repo. The batches/ folder here carries the convention, not the data. No batches are run this
phase.
5. Iteration levers, for when the generator exists
- Skip the road pass for all terrain and water iteration. In the prototype this cut a run from ~26 min to ~80 s — a 19× cut, and the difference between iterating on terrain and not iterating on terrain. Blueprints from such a run carry present-but-empty road sections, which is legitimate, not corrupt.
- ⭐ Build the oracle before the taste-iteration, not after. Because the classify path was pinned to uncurved height, every shaping iteration had a hard automatic correctness check (biome and water maps md5-identical). Five rounds of taste-iteration were safe because correctness was not being judged by eye. Where a future phase has a subjective gate, ask first what the automatic invariant is.
- Config-gate every shaping change, with the legacy behaviour surviving on the other side. It keeps changes comparable as an A/B pair, and it means a rejected change costs a flipped default rather than a reverted commit.
- The lever is variants per batch, not speed per pass. Two Phase B tasks ran ~2 hours each, which looked like the pipeline getting slower. It was not — a single pass stayed at ~2 minutes. The cost was A/B/ablation multiplication. When a batch is genuinely wide, say so up front.
6. Scaling discipline
Nothing uses a raw pixel number. Every distance, radius, threshold and noise frequency comes
from Core/Scripts/GenerationScale.cs. Read its header before adding any constant — including the
tightening on normalized additive noise offsets, which is a forward-guard for the Phase 1 noise
port. → Design - Tooling - Scaling Discipline.md.