# 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_.dat`, where `` 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__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. | | `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-compact"` | `"v5-balanced"` | `"off"`; interim default `"v5-balanced"` — the task-09 taste batch's tri-state, resolved when the developer's verdict picks the winner as plain `"v5"`; `"v1"`–`"v4"` 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; COMPACT = 65 % lowland cordillera-from-plains, BALANCED = 60 % lowland gradual approach) shapes `HGTS`. 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`. ### 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.