BLUEPRINT_FORMAT.md: TDTL rewritten for the v2 body (30 B) with the version-skip rule spelled out -- this is the one section whose body is VERSIONED rather than extended, because v1's layout was retired rather than grown, and a reader that guesses would mint plausible nonsense. Records edgeAmpM as APPLIED (post-clamp), so the file always describes the terrain rather than the request. Config-knobs section updated: ShelfEdgeVariation is metres of INPUT height -- a boundary displacement, not an elevation. Tools/Scripts/README.md: the two detail passes and why neither can breach the red ceiling or the 420 m cap (K2/K6 do not move). Adds an explicit "no rivers, no erosion, no flow routing anywhere in this pipeline" note with the reverted D8 pass and its lesson on the record, so the next reader does not re-derive it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 lines
7 KiB
Markdown
105 lines
7 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.
|
||
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.
|
||
|
||
**No rivers, no erosion, no flow routing anywhere in this pipeline.** A 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.
|
||
Erosion is Phase C work: a hydraulic pass over final terrain, after the shape stops moving.
|
||
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.
|