islaApocalypse/Core/Scripts/BLUEPRINT_FORMAT.md
beezm 85e7bceb72 docs: erosion hierarchy defaults, EROS v2, and where erosion cannot reach (terrain-water task 18)
Records the four governors and the task-18 defaults, and states the measured
limit plainly: erosion concentrates ~9x on the curve's shelf risers because
that is where sustained slope is, so flat shelves and lowlands are barely
touched and the lowland continuation of a trunk is river promotion's job, not
erosion's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 04:16:33 -04:00

186 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BLUEPRINT_FORMAT — the `.dat` container, v2 (and the v1 legacy summary)
The `.dat` blueprint is the sole handoff from the offline generator (`/Tools`) to the server
(`/Server`). This document is the byte-accurate contract. Code authority: constants in
`BlueprintFormat.cs`, writer in `BlueprintWriter.cs`, reader in `MapDataParser.cs`. On any
disagreement between this file and the code, the code wins and this file must be corrected.
- **Byte order: little-endian throughout** (BinaryWriter/BinaryReader platform default; stated here
because the format itself records no endianness marker).
- **File naming:** `user://MapData_Seed_<seed>.dat`, where `<seed>` is the generator's **resolved**
noise seed. The server looks the file up by config `WorldSeed` — a config seed of 0 ("randomize")
therefore never finds the file it just generated (historical hazard H4; config-side, unchanged).
- The generator currently **dual-writes**: v2 under the primary name, legacy v1 beside it as
`MapData_Seed_<seed>_v1.dat`. The v1 fallback is scheduled for removal in a future task.
## Format detection
The reader dispatches on the **first byte** of the file:
| First byte | Format | Why it's unambiguous |
|---|---|---|
| `0x49` (`'I'`) | **v2** — opens with the raw 4 bytes `ISLA` | v2's magic is raw bytes, deliberately not a .NET string |
| `0x07` | **v1 (legacy)** — opens with the length-prefixed .NET string `"ISLA_V1"` (prefix byte 7) | v1 loads intact, with a deprecation warning |
| anything else | rejected loudly, `null` return | |
## v2 layout
```
[4 B] magic: raw bytes 'I','S','L','A' (== little-endian u32 0x414C5349)
[4 B] format version: u32 = 2 (any other value -> loud reject, null)
[...] sections, sequentially, until EOF
```
### Section framing
Every section: `[u32 tag][u64 payload-length in bytes][payload]`.
- **Reader rule: known tag → parse; unknown tag → skip payload-length bytes and continue.** This is
the forward-compatibility property the v2 redesign exists to buy: a reader that predates a section
(a future water layer, for instance) loads the file and never sees it.
- The u64 length is deliberate headroom (a u32 caps a section at 4 GiB; a 16K-map height section
would already be 1 GiB, and future per-pixel sections should never have to shard on length).
- Tag values are FourCC codes stored as little-endian u32, so the on-disk bytes read as ASCII in a
hex dump. **All tags are registered in `BlueprintFormat.cs` and in the table below — one place
each. Never reuse a retired tag value.**
- **Rules enforced by the reader:** the params section must be **first**; duplicate tags are an
error; every declared length is checked against the remaining file before the payload is read; a
parsed section must consume exactly its declared length.
### Registered sections (v2, current content)
| Tag (ASCII / u32) | Payload | Notes |
|---|---|---|
| `PRMS` / `0x534D5250` | `i32 WorldSeed` · `i32 MapSize` · `f32 CraterRadius` · `f32 DensityMultiplier` · `f32 ImpactCenterX` · `f32 ImpactCenterY` · `str GeneratedUtc` · `str GeneratorGitHash` | Mandatory, first. The **resolved** generation inputs — the file is self-describing; the server cross-checks seed and MapSize against config and warns loudly on desync. `str` = .NET length-prefixed UTF-8 (fine *inside* a length-framed section; only the file header must avoid it). `CraterRadius` is f32 because `ConfigManager.CraterRadius` is a float in code. `GeneratorGitHash` is the repo short hash or `""`. |
| `HGTS` / `0x53544748` | `MapSize²` × `f32` height, **X outer / Y inner** | Mandatory. The second index is the map's north/south axis; the server consumes it as world **Z**. (This axis convention was never written down for v1 — it is now normative.) Length must equal `4·MapSize²`. |
| `BIOM` / `0x4D4F4942` | `MapSize²` × `u8` biome ordinal, same pixel order | Mandatory. Ordinals from `Enums.cs::Biome`**append-only, never reorder** (the ordinal IS the wire value). Writer refuses ordinals > 255; reader rejects ordinals ≥ the known biome count (parse-time validation; the palette's runtime Dirt fallback for in-memory values is unchanged). Length must equal `MapSize²`. |
| `TOWN` / `0x4E574F54` | `i32 count`, then per town: `f32 X` · `f32 Y` · `u8 tier` · `u8 isHighwayNode` (0/1) | Tier ordinals from `Enums.cs::TownTier`, append-only, range-checked on read. The highway-node flag is what the generator's road topology was built from (v1 dropped it); carried and exposed on the parsed blueprint, consumed by nothing server-side yet. |
| `TCRV` / `0x56524354` | 50 B base: `u16 curveVersion` · `f32 knot1..knot4` · `f32 spikeMax` · `f32 sea` · `f32 orangeCeil` · `f32 redCeil` · `f32 benchLo` · `f32 benchHi` · `f32 peakCap` · `f32 tailSlope`. **When `curveVersion ≥ 4`, a 36 B modulation extension follows:** `f32 benchAmp` · `f32 plateauAmp` · `f32 shelfSpanMin` · `f32 shelfSpanMax` · `f32 elevFreqIslands` · `f32 strengthFreqIslands` · `i32 benchSeedOffset` · `i32 plateauSeedOffset` · `i32 strengthSeedOffset`. **When `curveVersion ≥ 5`, a 9 B preset extension follows:** `u8 presetId` (1 = compact, 2 = balanced) · `f32 k5` · `f32 k6` — with the four base knot slots this makes the effective curve unambiguous from the record alone (record total 95 B; the length-framed section + version byte keep every layout change safe) | Optional — present iff the height-redistribution curve shaped this blueprint's `HGTS` (config `TerrainCurve: "v3"`); absent = raw legacy profile. **The `u16 curveVersion` selects the field semantics and the unserialized anchor set:** v1 — knots t1..t4, spikeMax = pooled calibration max, benchLo/Hi = the 50 m plateau's lo/hi. v2 — as v1 but spikeMax = the seed's effective raw pre-curve maximum (per-seed spike normalizer). v3 — knots = K1..K4 of six (K5 = 0.930304, K6 = 1.050720 are version constants, not serialized), spikeMax per-seed, benchLo/benchHi = the fixed 100 m / 220 m shelves. **v4 (current)** — as v3, but benchLo/benchHi are the shelf **BASE** anchors and the extension record carries the spatial-modulation parameters: shelf elevations vary ±benchAmp/±plateauAmp and shelf strength blends the output span across `[shelfSpanMin, shelfSpanMax]`, via Simplex fields seeded `resolvedWorldSeed + seedOffset` at `freq/MapSize` (frequencies stated in undulations per island width). Because spikeMax is per-seed (v2+), blueprints are not reproducible from curve constants alone — that is why it is recorded. **Metadata only:** exported heights are already curved; nothing re-applies the map. Calibration provenance: `HeightCurve.cs` header + the task-05/06/07 reports. |
| `TDTL` / `0x4C544454` | 30 B: `u16 detailVersion` · `f32 reliefAmpM` · `f32 reliefFreqIslands` · `i32 reliefSeedOffset` · `f32 edgeAmpM` · `f32 edgeFreqIslands` · `i32 edgeSeedOffset` · `f32 edgeMaxShiftM` | Optional — present iff the terrain detail passes shaped this blueprint's `HGTS` (config `TerrainDetail: "v1"`, requires the curve). **`detailVersion` selects the body layout and a reader that does not recognise it SKIPS the section** (leaving detail metadata null) rather than misreading a differently shaped payload — the one section whose body is versioned rather than extended, because v1's layout was retired rather than grown. **v1 (retired, never shipped)** — shelf micro-relief + D8 drainage incision, 38 B; the incision produced grid-aligned artifacts and was reverted whole, so the only v1 payloads that exist are in that batch's own tree. **v2 (current)** — shelf micro-relief (±`reliefAmpM` output metres, shelf-ness weighted) + shelf-edge variation: a per-column shift of the curve's shelf/riser knot block K3/K4/K5, drawn from a Simplex field seeded `resolvedWorldSeed + edgeSeedOffset` at `edgeFreqIslands/MapSize`, amplitude ±`edgeAmpM` **metres of INPUT height** — a displacement of the shelf boundary contour, not an elevation change. `edgeAmpM` is recorded **as applied**, after the clamp to `edgeMaxShiftM` (the preset's band-squeeze bound), so the record always describes the terrain rather than the request. **Metadata only** — heights are already detailed. Machinery: `Tools/Scripts/TerrainDetailPass.cs`. |
| `EROS` / `0x534F5245` | 62 B: `u16 erosionVersion` · `i32 dropletCount` · `i32 lifetime` · `i32 brushRadius` · `i32 seedOffset` · `f32 carveCapM` · `f32 depositCapM` · `f32 seaMarginM` · `f32 inertia` · `f32 capacityFactor` · `f32 minSlopeM` · `f32 erodeRate` · `f32 depositRate` · `f32 evaporation` · `f32 gravity` · `f32 craterExclFactor` | Optional — present iff the droplet hydraulic-erosion pass shaped this blueprint's `HGTS` (config `Erosion: "v1"`, terrain-water tasks 1718). **`erosionVersion` selects the body layout; an unrecognised version is SKIPPED whole** (erosion metadata left null), same rule as `TDTL`. **v1 (task 17, superseded)** — 58 B, no `depositCapM`; deposition was bilinear over 4 cells and unbounded, which built isolated cones (measured 15.5 m). The only v1 payloads in existence are in that task's batch tree. **v2 (task 18, current)** — deposition is spread over the same cone brush as carving and bounded per cell by `depositCapM` (`<= 0` = unbounded, the reference model). The FOUR governors are `dropletCount`/`lifetime`/`carveCapM`/`depositCapM`; both caps are enforced against one per-cell NET displacement ledger (positive = carved below the height the pass found, negative = built up above it) and asserted on exit. `seaMarginM` is the flood-guard clamp — no cell is carved below sea + margin, and below-sea cells are untouched in BOTH directions, so the rendered coastline cannot move. Droplets are deterministic from `resolvedWorldSeed + seedOffset` (PCG32); no cell within `craterExclFactor × CraterRadius` of the impact centre is modified. Remaining fields are the droplet-model strength dials; slopes/amounts in metres (1 raw = 251 m). All values recorded **as applied** (post config clamping). **Metadata only** — heights are already eroded, and the classify-side sections (`BIOM`/`WBID`/…) never saw the pass by design. Machinery: `Tools/Scripts/HydraulicErosion.cs`. |
| `WBID` / `0x44494257` | `MapSize²` × `u16` water-body id, same pixel order as `HGTS` | Optional (absent = no water data, e.g. a legacy re-encode). `0` = no water, `1` = **the** ocean body, `2..N` = lakes. Ids assigned in deterministic scan order (X outer / Y inner, first-encountered pixel), lakes labeled with the **same 4-connectivity as `CalculateTrueOcean`**. Membership is exactly the generator's water classification — the biome grid's Ocean/Lake pixels and this grid's nonzero pixels are the same set **by construction** (shared predicates). Length must equal `2·MapSize²`. |
| `WBTB` / `0x42544257` | `i32 count`, then per body (20 B): `u16 id` · `u8 type` (0 ocean, 1 lake) · `u8 salinity` (0 fresh, 1 salt) · `f32 surfaceLevel` · `i32 pixelCount` · `f32 centroidX` · `f32 centroidY` | Optional, paired with `WBID`. **`surfaceLevel` is a documented TRANSITIONAL rule:** one flat level per body — `GetSeaLevel` at the body's pixel centroid (ocean: at the map centre) under the still-live latitude field; superseded by the flat-scalar sea model (minted, lands with the coast change set). The field's per-pixel slope is deliberately NOT baked into any section. **Salinity is a provisional default** (ocean salt, lake fresh) — a placeholder for the future fresh/salt irrigation mechanic, not a mechanic. |
| `WSRF` / `0x46525357` | `MapSize²` × `u16` quantized water-surface elevation, same pixel order | Optional, paired with `WBID`. `0` is the reserved **no-water sentinel**; a real level `L` (raw height units) encodes as `1 + round(L × 32768)` so it can never encode to 0; decode `(q 1)/32768` (`BlueprintFormat.EncodeWaterLevel`/`DecodeWaterLevel`). Covers `[0 … ~1.99997]` raw at `1/32768` raw ≈ **7.7 mm** of world height (1 raw = 251 m) — far finer than the 1 m voxel. Nonzero exactly where `WBID` is nonzero; the value is the pixel's body level. |
| `RDHW` / `0x57484452` | `i32 pathCount`, then per path: `i32 pointCount` + `pointCount` × (`f32 X` · `f32 Y`) | Highway tier. **Tags, not file position, identify the tier** — the v1 order-fragility is gone. Road sections may legitimately be **present but empty** (zero paths): the `SkipRoads` config toggle exports a road-less iteration blueprint. |
| `RDBR` / `0x52424452` | same layout | Branch tier. |
| `RDRG` / `0x47524452` | same layout | Rugged tier. |
| `RDTL` / `0x4C544452` | same layout | Trail tier. |
Missing `TOWN`/road sections load as empty lists with a warning; missing `PRMS`/`HGTS`/`BIOM` is an
error. Section order after `PRMS` is not significant (the reference writer emits the table order).
### Reader validation (v2 path)
1. Magic and version gate — loud, specific errors; `null` return (the caller's null-check aborts
world load cleanly).
2. `MapSize` sanity bound **before any allocation**: `256 ≤ MapSize ≤ 32768`.
3. Every section length checked against the remaining file length; truncation is a loud error, not
a read-past-end crash.
4. Section parsers must consume exactly their declared length.
5. Biome and town-tier ordinals range-checked against the known enum counts.
6. After load, `ServerChunkManager` compares embedded `WorldSeed`/`MapSize` against
`ServerConfig.json` and logs a prominent desync warning on mismatch (warning, not abort).
### Sentinel params (re-encoded files)
A v2 file produced by re-encoding a v1 source (e.g. the round-trip harness) cannot know the
original generation inputs. It carries: `WorldSeed = 0`, `CraterRadius = -1`, `DensityMultiplier =
-1`, `ImpactCenter = (-1, -1)` (`MapSize` is always real). The seed cross-check skips sentinel
seed 0. Provenance (`GeneratedUtc`, `GeneratorGitHash`) is stamped at **write** time and describes
the file, not the original generation.
### Size
`total = 8 (header) + Σ per section (12 + payload)`. The pixel grids dominate: without water
sections `5·MapSize²` bytes (4 height + 1 biome) ≈ **320 MiB at 8K**, vs v1's `8·MapSize²`
512 MiB — the u8 biome section saves ~192 MiB. With the water sections the per-pixel cost is
`9·MapSize²` (adds 2 B `WBID` + 2 B `WSRF`) ≈ **576 MiB at 8K** plus a trivial `WBTB` (20 B per
body).
### Config knobs that shape blueprint content (not part of the byte format)
- **`SeaLevelModel`** (`"flat"` | `"field"`) + **`SeaLevelValue`** (float; used when flat) —
D-033: selects what `MapGenerator.GetSeaLevel` returns. `"flat"` (the default, value 0.15) is
one scalar sea level everywhere; `"field"` is the legacy latitude Lerp (0.26 north → 0.15
south). Generator-only — all nine `GetSeaLevel` call sites inherit the model; nothing at
runtime computes sea level. The model in force shapes the coastline, masks, biomes, water
bodies, and `WBTB` levels of the blueprint being generated; the file itself does not record
which model produced it (the `WBTB` levels are the observable).
- **`SkipRoads`** (bool, default false) — exports with present-but-empty road sections (see the
road-section table note).
- **`TerrainCurve`** (`"v5"` | `"off"`, default `"v5"` — the task-09 gate's winner, the
BALANCED preset; the tri-state and `"v1"``"v4"` are retired and rejected loudly) — whether
the height-redistribution curve (terraced ascent, corner-eased junctions, spatially modulated
shelves at 100±12 m / 220±20 m, per-seed-normalized 420 m summit spires, 60 % lowland) shapes
`HGTS`.
- **`TerrainDetail`** (`"v1"` | `"off"`, default `"v1"`; no-op without the curve) — the task-10
detail passes as one judged unit: shelf micro-relief (`ShelfReliefAmp`, default 3 m of output
height) and shelf-edge variation (`ShelfEdgeVariation`, default 12 m of **input** height — how
far the shelf/riser boundary contour wanders, clamped at load to the preset's band-squeeze
bound, loudly). Both are output-height only. When on, `TDTL` records the parameters as applied.
Biome/water classification is curve-invariant by
construction (it classifies the retained uncurved heights); town positions and everything 3D
follow the curved terrain. When on, `TCRV` records the effective parameters including the
per-seed `spikeMax`.
- **`Erosion`** (`"v1"` | `"off"`, default `"off"` — opt-in until the developer's gate approves
it) — the droplet hydraulic-erosion pass (tasks 1718): carve-and-deposit drainage detailing of
`HGTS` after the detail passes and before the crater carve. FOUR governor dials
(`ErosionDropletCount` / `ErosionDropletLifetime` / `ErosionCarveCap` / `ErosionDepositCap`,
defaults 250 000 / 384 / 15 m / 6 m; bounds clamped loudly at load) plus the sea-clamp margin
and strength constants — the full dial list and semantics live in the `EROS` row above and
`ConfigManager.cs`. The task-18 defaults tune for a drainage HIERARCHY: droplets live long
enough (384 steps at inertia 0.35) for their paths to overlap and deepen shared low lines into
trunk channels rather than dying as short independent scratches, and the raised carve cap lets
those trunks separate from the fine rills instead of both piling against one ceiling.
Output-height only: the classify path reads pre-erosion heights, so `BIOM` and every water
section stay bit-identical with erosion on or off, and the sea clamp keeps even the RENDERED
coastline fixed. When on, `EROS` records the parameters as applied.
- **`CoastProfile`** (`"wide"` | `"steep"`, default `"wide"`) — the submarine shelf. The height
curve is identity at and below sea level, so it never reshaped the seabed; `"wide"` compresses
shallow depth so the shallows reach 2.42.9× further out. It is strictly positive for positive
depth and therefore **cannot move the waterline**, so `HGTS` changes while `BIOM`, `WBID`,
`WBTB` and `WSRF` stay bit-identical. `"steep"` is the pre-task-11 seabed.
- **`IslandAxisX`** / **`IslandAxisY`** (default `1.30` / `0.78`; pre-task-11 `1.15` / `0.90`) —
the falloff axis ratios, and the island's proportions. These **move the coastline**, so unlike
the curve and detail work they legitimately change `BIOM` and the water sections: an elongated
seed has a new biome baseline. Rejected (with a loud restore to legacy) if ≤ 0.01, which would
divide by ~zero and silently yield an all-ocean map.
- **`OffshoreIslandDensity`** (default `0.02`, `0` disables, clamped to 0.5) — the fraction of the
offshore noise field above the islet threshold, calibrated per seed against the field's own
distribution. Adds land, so it also moves `BIOM` and the water sections.
### Deliberately NOT a section: basins (`BSIN`)
The priority-flood pit-fill that runs in the water-bodies stage is **diagnostics only** and
serializes nothing. Rationale, on the record so nobody adds a basin section later without meeting
the argument: basin geometry is a pure function of the heightmap, and it goes stale the moment the
coast-smoothing change set (or any terrain change) touches heights — a serialized `BSIN` would be
a cache with a silent invalidation hazard. The rivers stage recomputes basins fresh at generation
time (measured ~12 s at 8K — cheap enough to never cache).
### Adding a new section (the intended extension path)
1. Register a fresh FourCC in `BlueprintFormat.cs` and in the table above.
2. Emit it from `BlueprintWriter.WriteV2` (any position after `PRMS`).
3. Parse it in `MapDataParser.LoadV2`'s tag dispatch.
4. Old readers skip it automatically; **no version bump is needed for additive sections.** Bump
`VERSION` only for changes that alter the meaning of *existing* bytes.
## v1 legacy summary (still readable, still dual-written, removal pending)
Positional and untagged — every field's meaning derives from its offset; no lengths, no checksums,
no skip capability. Layout: length-prefixed string `"ISLA_V1"``i32 MapSize``MapSize²` ×
(`f32 height` · **`i32` biome ordinal**) X-outer/Y-inner → `i32 townCount` + per town (`f32 X` ·
`f32 Y` · `i32 tier`) → four road blocks **identified by position** (Highway → Branch → Rugged →
Trail), each `i32 pathCount` + per path `i32 pointCount` + points. The v1 reader stops after the
Trail block and ignores trailing bytes. No validation beyond the header string. It does not carry
generation params, the impact centre, or the highway-node flag.
## Shared wire-format rule (both formats)
**`Biome` and `TownTier` ordinals are the serialized values** (u8 in v2, i32 in v1). The enums in
`Enums.cs` are declared without explicit numeric values, so their ordinals are positional:
**append new members only at the end; never reorder or insert.** v2 adds parse-time range checks,
which turn enum drift from silent reinterpretation into a loud load failure — but the append-only
rule is still what keeps old files *meaning* the same thing.