201 lines
15 KiB
Markdown
201 lines
15 KiB
Markdown
# /Tools/Scripts — generation logic
|
||
|
||
C# backend for the offline developer tools. Decoupled from the live server and client.
|
||
|
||
## `MapGenerator.cs`
|
||
|
||
Generates the entire 2D blueprint. Roughly in order:
|
||
|
||
1. **Config** — reads `ServerConfig.json`, which sets `MapSize` (8192 by default) and the crater
|
||
radius. The `MapSize = 4096` field initialiser is a fallback that is immediately overwritten.
|
||
2. **Topography** — FastNoiseLite base height plus a mountain spine, minus a squircle distance
|
||
falloff, giving a guaranteed island. Noise frequency is divided by `scaleFactor`
|
||
(`MapSize / 1024f`) so terrain features stay the same real-world size at any map profile.
|
||
The island's proportions are the `IslandAxisX`/`IslandAxisY` dials (`1.30`/`0.78`; the
|
||
pre-task-11 shape was `1.15`/`0.90`). **`IslandAxisX` is a weak lever** — the island is
|
||
already Trench-clamped at ~90 % of the map width, so aspect responds almost entirely to
|
||
`IslandAxisY`, which trades against land area. These move the coastline, so they move biomes.
|
||
The mountain spine shares `IslandAxisX` for its width, and its crest is rounded
|
||
(`IslandFalloff.SmoothAbs`) — `1 - |x - centre|` used to peak with a slope discontinuity that
|
||
was, measured, the largest slope step on the map outside the Trench walls. **The spine's AXIS
|
||
is still a straight line down the map centre; that is known, deferred, and its own task.**
|
||
With `CoastProfile: "wide"` (the default) the *seabed* leaving the shoreline is shelved
|
||
(`IslandFalloff.CoastShelf`): the height curve is identity at and below sea level, so it never
|
||
reached the water, and the shoreline used to shelve gently on land then drop 6.7× steeper the
|
||
moment it went under. The shelf cannot move the waterline — it is strictly positive for
|
||
positive depth — so biomes and water are bit-identical with it on or off. `OffshoreIslandDensity`
|
||
(`0.02`, `0` disables) seeds sparse discoverable islets in the open ocean; they are held off the
|
||
mainland by a depth moat and out of the Trench by a distance mask, both by construction.
|
||
When `TerrainCurve: "v5"` (the default — the task-09 gate's BALANCED winner), the calibrated
|
||
height-redistribution curve
|
||
(`HeightCurve.cs` — the terraced ascent with spatially modulated shelves: flat farmable
|
||
lowlands, a walkable foothill bench at 100±12 m, the white mountain-town plateau at
|
||
220±20 m — shelf heights and strength drift across the island so no two flanks wear the same
|
||
ring — and a per-seed-normalized summit spike to the 420 m cap) reshapes above-sea terrain
|
||
after noise/falloff/Trench and before the crater carve; biome classification reads a
|
||
retained uncurved map, so biomes are identical either way. With `TerrainDetail: "v1"` (the
|
||
default) two detail passes ride along with the curve (`TerrainDetailPass.cs`): shelf
|
||
micro-relief (`ShelfReliefAmp`, ±3 m rolling skin on the benches/plateaus) and shelf-edge
|
||
variation (`ShelfEdgeVariation`, 12 m) — a per-column shift of the shelf/riser knot block that
|
||
makes the shelf edge scallop into notches, coves and peninsulas instead of tracing a clean
|
||
height contour. Both touch exported heights only, and neither can reach below the red ceiling
|
||
or above the 420 m cap: the curve's K2 and K6 knots do not move, so a warped column is
|
||
bit-identical to an unwarped one outside the shelf/riser stack. Drops the `0_height` hillshade
|
||
snapshot (hypsometric bands × NW hillshade) in both modes.
|
||
|
||
**Erosion (tasks 17–18, Phase C0)** — with `Erosion: "v1"` (default **"off"**, opt-in until the
|
||
developer's gate approves it) a droplet-based hydraulic erosion pass (`HydraulicErosion.cs`,
|
||
standalone numeric, deterministic from the resolved seed) details the curved+detailed render
|
||
map after the detail passes and before the crater carve: droplets walk downhill with inertia,
|
||
eroding steep fast stretches and depositing where the ground flattens — both spread over the
|
||
same cone brush, so neither carving nor dumping can spike a single cell. **Four** hard
|
||
governors bound it (`ErosionDropletCount`/`ErosionDropletLifetime`/`ErosionCarveCap`/
|
||
`ErosionDepositCap`; both caps are per-cell metres against one net-displacement ledger and are
|
||
asserted on exit), a sea clamp forbids carving below sea + margin and leaves below-sea cells
|
||
untouched in both directions (the rendered coastline cannot move — asserted every generation),
|
||
and the crater is handled by `CraterErosionMode` (task 19): nothing inside the protected strike
|
||
core (`CraterErosionCore`, **0.80 ×** CraterRadius — the carve's own extent) is modified, and
|
||
outside it either `"full"` applies full strength at once or `"feather"` ramps 0→full out to
|
||
`CraterErosionFeather` (1.05 ×). Task 17's hard 1.2 × cutoff was replaced because it held
|
||
**620 811 land cells** of ordinary terrain smooth for no geometric reason — measured, the carve's
|
||
displacement is exactly 0 beyond 0.80 × — which read as an un-eroded disc with a hard edge.
|
||
Keeping the core at the carve radius is what makes erosion and the carve touch **disjoint**
|
||
cells; a narrower core lets the carve, which runs afterwards and scales height toward the sea
|
||
target, amplify erosion's deltas across the waterline (measured at a 0.50 core: 79 rendered
|
||
waterline crossings). The flooded bay and its sea connection never depend on the crater mode —
|
||
below-sea cells are read-only in both directions, so the bay can be neither carved open nor
|
||
silted shut. Output-height only: biome/water
|
||
classification reads the retained pre-erosion map, so `1_biomes`/`0_water` are bit-identical
|
||
with erosion on or off.
|
||
The task-18 defaults tune for a drainage **hierarchy** — fine rills everywhere feeding a set of
|
||
clearly deeper convergent channels — by letting droplets live long enough (384 steps at inertia
|
||
0.35, low evaporation) that their paths overlap and deepen shared low lines, and by raising the
|
||
carve cap to 15 m so trunks separate from rills instead of both piling against one ceiling.
|
||
Measured on seed 1280587109: ~1.9 M cells carved past 0.5 m, 42 k past 5 m, 1.4 k past 10 m,
|
||
and 177 connected channel systems of 200+ cells at the 3 m threshold. Erosion concentrates
|
||
~9× on the curve's shelf risers, because that is where sustained slope exists; the flat
|
||
shelves and lowlands are barely touched, so **the lowland continuation of a trunk is not
|
||
erosion's to cut** — that is river promotion's job. **No rivers yet** — the carved channels are
|
||
the designated future river routes (Phase C0b promotes them by flow accumulation).
|
||
The predecessor D8 drainage-incision pass was written and reverted in task 10 — per-cell
|
||
steepest descent on a regular grid can only route along eight headings, and at map scale that
|
||
reads as straight hatching, not drainage; the droplet model is the working replacement (its
|
||
carve field measures isotropic to within 1.5 % across the folded 45° grid period).
|
||
**River plan (task 21, C0b part 1)** — `DrainageAnalysis.cs` + the headless
|
||
`RiverPlanTool.tscn` produce a river PLAN from an erosion-ON blueprint: priority-flood with a
|
||
one-ulp epsilon resolves the ~15k erosion pits for ROUTING ONLY (terrain untouched), deep+large
|
||
depressions survive as terminal basins, D8 flow directions + Kahn accumulation build the
|
||
drainage network, and the top ~3 TRUE-ocean outlets (the ocean body, WBID 1 — enclosed lagoons
|
||
do not count as "the sea") become trunk candidates with lean tributaries, mountain-exit handoff
|
||
points, and lean endorheic terminals. Task 21b extends the plan to the developer's MIXED
|
||
promotion: the ocean trunks PLUS the top endorheic giants (`RIVERPLAN_GIANT_N`, default 3),
|
||
each classified — a giant whose terminal basin holds a classify lake stays a **lake-ender**
|
||
(rivers ending in lakes are real geography); a dry-pan giant, and always the **SOUTHERN
|
||
CANDIDATE** (the giant pooling nearest the southernmost town), gets a **PROVISIONAL route** to
|
||
the ocean: steepest descent on the full (no-terminal) fill, so the basin overtops at its spill
|
||
and the walk follows the terrain's own drainage to the sea — drawn dashed for the gate, never
|
||
carved. Output is a JSON *plan* sidecar + console report — deliberately NOT a blueprint
|
||
section, so a plan can never masquerade as realized water. Part 2 (task 22) carves and waters
|
||
the gated plan. Pure analysis: the source blueprint is never written.
|
||
|
||
**River carving (task 22, C0b part 2a)** — with `Rivers: "v1"` (default **"off"**, pending the
|
||
routing-style gate) the frozen task-21b plan is executed as real terrain (`RiverCarvePass.cs`):
|
||
the drainage analysis reruns in-pipeline (deterministic), each routed giant gets a lowland
|
||
route to the nearest ocean by deterministic Dijkstra — `RiverRoutingStyle: "short"` (direct,
|
||
uphill-penalised) or `"lowground"` (cost ≈ elevation; follows the lowest ground and meanders;
|
||
the provisional default) — and every promoted course is carved as a parabolic channel with a
|
||
smoothstep shoulder, width/depth growing downstream with drainage (`RiverWidthScale`/
|
||
`RiverDepthScale`), bed made monotone non-increasing toward the outlet and clamped to
|
||
sea + `RiverSeaMargin` everywhere (the erosion flood-guard discipline: below-sea cells
|
||
read-only, zero new below-sea cells, coastline provably fixed; asserted per generation).
|
||
Slots AFTER towns (town placement reads the render map and must not shift) and BEFORE roads.
|
||
Crater core excluded.
|
||
|
||
**River water (task 23, C0b finale)** — the payoff. Each carved main river becomes a chain of
|
||
**stepped flat water-body REACHES**: a new reach starts every `RiverStepDropM` (2 m) of bed
|
||
descent and sits `RiverWaterDepthM` (1.2 m) above its bed, each reach strictly lower than the
|
||
one upstream, stepping down to the sea (small vertical drops — riffles — smoothable purely by
|
||
the step dial; tilted continuous-slope water is the deferred model B). Reaches are ordinary
|
||
water bodies: WBID cells + a WBTB entry (`type 2 = river`, fresh) each; WSRF derives from body
|
||
levels exactly as for lakes, and river water renders through the C1/task-13 path with the
|
||
task-15 presence rule at its banks. Lake-enders route the last reach into their lake by the
|
||
lowground Dijkstra (a pooling terminal IS a local minimum — blind descent dead-ends there);
|
||
rivers meet lakes and the sea without repainting them (existing bodies are never overwritten).
|
||
Only the lowland reaches are ROUTE-SMOOTHED (RDP+Chaikin, the road pipeline): the upland stems
|
||
already thread carved valley floors, and smoothing them off-line cut valley walls (measured).
|
||
The BIOME oracle holds (classify untouched; `1_biomes` md5-identical); `0_water` and the
|
||
re-captured `0_height` change — that IS the river water and its beds (both snapshots are
|
||
re-captured after the river pass so the exported PNGs show them). Max-cut is now reported
|
||
CUMULATIVELY vs the pre-pass surface.
|
||
|
||
3. **Sea level and water** — sea level per the configured model (`SeaLevelModel`: `"flat"` scalar
|
||
— the default, `SeaLevelValue` 0.15 — or the legacy `"field"` latitude Lerp); flood fill
|
||
separates true ocean from inland lakes; a mainland fill guarantees one contiguous landmass.
|
||
4. **The crater** — placed along the northern coast and carved to below sea level, but only out to
|
||
**80 % of its radius**, which guarantees a landbridge rather than severing the island.
|
||
5. **Water bodies** — promotes the classified water into explicit data: the ocean as body 1, each
|
||
lake connected-component labeled (same 4-connectivity as the true-ocean fill), one transitional
|
||
surface level per body. Classification comes from the shared `IsOceanPixel`/`IsLakePixel`
|
||
predicates that the biome stage also uses, so the two can never disagree. A priority-flood
|
||
pit-fill runs here as validated diagnostics (basin statistics to console; serializes nothing).
|
||
Drops the `0_water` snapshot.
|
||
6. **Biomes and towns** — biome zoning by height and temperature; tiered town placement (Capitol,
|
||
Hubs, Villages, Outposts, POIs) filtered by slope, water proximity and spacing.
|
||
7. **Roads** — see below. Skipped entirely when the `SkipRoads` config toggle is on (the ~25-min
|
||
A\* pass is the bottleneck; a skip run exports a road-less iteration blueprint in ~80 s with a
|
||
loud console banner).
|
||
8. **Export** — writes the `.dat` blueprint (dual-write: the v2 tagged container under the primary
|
||
seed name, plus the legacy v1 format beside it as `_v1.dat` — see
|
||
`Core/Scripts/BLUEPRINT_FORMAT.md`), then renders the PNG snapshot. The v2 file embeds the
|
||
resolved generation params (seed, MapSize, crater radius, density, impact centre, provenance).
|
||
|
||
### The road network
|
||
|
||
`AStarGrid2D` over the whole map at one node per pixel. Mountains are made expensive rather than
|
||
impassable (`1 + elevation³ × 400`), water and the crater are marked solid.
|
||
|
||
- **Continental loop** — highway nodes sorted by radial angle and connected in a ring.
|
||
- **Mountain branch** — a spur from the loop to the snow hub.
|
||
- **County roads** — Prim's algorithm daisy-chains remaining towns onto the network. Villages become
|
||
Rugged roads, everything else Trails.
|
||
- **Abandon protocol** — a town further than 8 % of the map from the network is skipped rather than
|
||
pathed to, so one unreachable outpost cannot hang generation.
|
||
- **Smoothing** — every path is decimated with Ramer–Douglas–Peucker (tolerance 4.0) then smoothed
|
||
with 4 Chaikin passes.
|
||
|
||
⚠ **This is the project's dominant performance problem.** At 8K the grid is 67 million nodes, and the
|
||
weight-setup pass touches every one before the first path is requested. The `await` yields between
|
||
stages cannot interrupt a single engine-side `GetPointPath` call, so a long path still blocks. Full
|
||
regenerations frequently get abandoned.
|
||
|
||
### The PNG snapshot
|
||
|
||
`SaveMapSnapshot()` builds an offscreen `SubViewport` in code rather than relying on the scene's
|
||
layout, because Godot's UI layout engine will otherwise crush a 4096+ render down to the editor
|
||
window size. A `MapDrawProxy` control re-issues the `_Draw()` calls into that viewport — `GetImage()`
|
||
captures only the base texture and would otherwise miss the roads and towns entirely — and the code
|
||
awaits two `RenderingServer.FramePostDraw` signals so the GPU has actually painted before the image
|
||
is read back.
|
||
|
||
Road colours on the snapshot, useful for identifying a road: **red** = Highway, **black** = Branch,
|
||
**dark brown** = Rugged, **light brown** = Trail.
|
||
|
||
### Outputs
|
||
`MapData_Seed_<seed>.dat` (v2), `MapData_Seed_<seed>_v1.dat` (legacy dual-write), and the staged
|
||
snapshots `Map_Seed_<seed>_{0_height,0_water,1_biomes,2_towns,3_roads}.png`, all to `user://`.
|
||
(`0_height` is the hypsometric hillshade of the curved terrain; `0_water` is painted from the
|
||
water stage's own outputs — ocean deep blue, lakes lighter blue, land neutral; `3_roads` is
|
||
absent on a SkipRoads run.)
|
||
|
||
## `RoundTripHarness.cs`
|
||
|
||
The blueprint format regression test (scene: `Tools/Scenes/RoundTripHarness.tscn`). Loads a
|
||
known-good blueprint through the real parser, re-writes it as v2 through the real writer, re-loads
|
||
it, and asserts semantic equality (heights bitwise, biomes, towns, every road point). Headless,
|
||
seconds per cycle, no generation. Exit code 0 = pass.
|
||
|
||
## Rules
|
||
1. **No magic numbers.** Distances, radii and thresholds derive from `MapSize` or `scaleFactor`, so
|
||
the generator behaves identically at 4K and 10K.
|
||
2. **Respect the GPU.** Anything exporting visual data must `await` the appropriate frame signals
|
||
before reading pixels back.
|