PASS B, replacing the reverted incision. The developer's sketch asked for
the shelf/riser BOUNDARY to be organic, not for water: notches, coves and
small peninsulas where a flat shelf meets its riser, instead of the clean
oval contour the curve produces.
Mechanism (the task's preferred "boundary warp", in its most
monotonic-safe form): every shelf/riser boundary is the contour where the
raw height crosses K3, K4 or K5, so the boundary is warped by SLIDING
THOSE THREE KNOTS per column -- edgeShift = simplex(resolvedSeed + 7507,
12 per island width) x ShelfEdgeVariation. The contours then wander in and
out of the terrain instead of tracing an iso-height line. Noise-warped by
construction, so there is no grid direction for an artifact to line up on
-- the failure mode of the thing this replaces.
Why slide knots rather than perturb a weight or the input height:
monotonicity becomes structural instead of conditional. The curve is
strictly monotonic for ANY ordered knot set, so no derivative bound, no
amplitude-vs-feather-width tuning, no way for a dial to invert a column.
MaxEdgeShift keeps the set ordered (half the smallest margin to a fixed
knot = 12.3 m of input height for the v5 knots); the config dial is
clamped to it, loudly. AssertMonotonic now sweeps 24 corners -- the 8
modulation extremes x {-max, 0, +max} shift -- and checks the bound first.
K1, K2 and K6 never move, which buys the guarantees exactly rather than
statistically: below K2 and above K6 a warped column is bit-identical to
an unwarped one, so the red ceiling still floors every shelf edge (storm
ladder safe), the 420 m cap still caps, and the toe and summit spikes are
untouched. The block slides rigidly, so the bench and mid-riser keep their
exact widths -- shelf interiors stay flat, riser interiors keep their
profile, and only the foothill riser and plateau stretch to absorb it.
The micro-relief mask takes the same shift, so pass A's skin follows the
shelf wherever pass B moved its edge.
Dial ShelfEdgeVariation (default 5 m of INPUT height -- a boundary
displacement, not an elevation change) under the existing TerrainDetail
gate; both passes stay one judged unit. TDTL v2 body records relief and
edge amp/frequency/seed-offset plus the applied clamp bound; writer,
parser and harness follow. No blueprint was written between the revert
and this commit, so v2 only ever means relief + edge warp on disk.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| HeightCurve.cs | ||
| HeightCurve.cs.uid | ||
| MapGenerator.cs | ||
| MapGenerator.cs.uid | ||
| MapGenerator.md | ||
| README.md | ||
| RoundTripHarness.cs | ||
| RoundTripHarness.cs.uid | ||
| TerrainDetailPass.cs | ||
| TerrainDetailPass.cs.uid | ||
/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:
- Config — reads
ServerConfig.json, which setsMapSize(8192 by default) and the crater radius. TheMapSize = 4096field initialiser is a fallback that is immediately overwritten. - 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. WhenTerrainCurve: "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. WithTerrainDetail: "v1"(the default) two detail passes follow the curve: shelf micro-relief (±3 m rolling skin on the benches/plateaus) and D8 drainage incision (rain-cut ravines in the risers — the future river routes;TerrainDetailPass.cs). Drops the0_heighthillshade snapshot (hypsometric bands × NW hillshade) in both modes. - Sea level and water — sea level per the configured model (
SeaLevelModel:"flat"scalar — the default,SeaLevelValue0.15 — or the legacy"field"latitude Lerp); flood fill separates true ocean from inland lakes; a mainland fill guarantees one contiguous landmass. - 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.
- 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/IsLakePixelpredicates 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 the0_watersnapshot. - Biomes and towns — biome zoning by height and temperature; tiered town placement (Capitol, Hubs, Villages, Outposts, POIs) filtered by slope, water proximity and spacing.
- Roads — see below. Skipped entirely when the
SkipRoadsconfig 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). - Export — writes the
.datblueprint (dual-write: the v2 tagged container under the primary seed name, plus the legacy v1 format beside it as_v1.dat— seeCore/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
- No magic numbers. Distances, radii and thresholds derive from
MapSizeorscaleFactor, so the generator behaves identically at 4K and 10K. - Respect the GPU. Anything exporting visual data must
awaitthe appropriate frame signals before reading pixels back.