13 KiB
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 configWorldSeed— 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.csand 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: u16 curveVersion · f32 knot1..knot4 · f32 spikeMax · f32 sea · f32 orangeCeil · f32 redCeil · f32 benchLo · f32 benchHi · f32 peakCap · f32 tailSlope |
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 (current) — knots = K1..K4 of six (K5 = 0.930304, K6 = 1.050720 are version constants, not serialized), spikeMax per-seed, benchLo = the 100 m foothill bench, benchHi = the 220 m white plateau; bench step (+0.008) and segment shapes are version constants in HeightCurve.cs. 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)
- Magic and version gate — loud, specific errors;
nullreturn (the caller's null-check aborts world load cleanly). MapSizesanity bound before any allocation:256 ≤ MapSize ≤ 32768.- Every section length checked against the remaining file length; truncation is a loud error, not a read-past-end crash.
- Section parsers must consume exactly their declared length.
- Biome and town-tier ordinals range-checked against the known enum counts.
- After load,
ServerChunkManagercompares embeddedWorldSeed/MapSizeagainstServerConfig.jsonand 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 whatMapGenerator.GetSeaLevelreturns."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 nineGetSeaLevelcall sites inherit the model; nothing at runtime computes sea level. The model in force shapes the coastline, masks, biomes, water bodies, andWBTBlevels of the blueprint being generated; the file itself does not record which model produced it (theWBTBlevels are the observable).SkipRoads(bool, default false) — exports with present-but-empty road sections (see the road-section table note).TerrainCurve("v3"|"off", default"v3";"v1"/"v2"retired by successor recalibrations and rejected loudly) — whether the height-redistribution curve (the terraced ascent: 59 % orange lowlands, 100 m foothill bench, 220 m white plateau, per-seed-normalized 420 m summit spires) shapesHGTS. 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,TCRVrecords the effective parameters including the per-seedspikeMax.
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)
- Register a fresh FourCC in
BlueprintFormat.csand in the table above. - Emit it from
BlueprintWriter.WriteV2(any position afterPRMS). - Parse it in
MapDataParser.LoadV2's tag dispatch. - Old readers skip it automatically; no version bump is needed for additive sections. Bump
VERSIONonly 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.