Byte-accurate v2 contract (magic/version, tagged sections, registered FourCC table, validation rules, re-encode sentinels, extension path) plus the v1 legacy summary — v1 stays live per the safety-net plan. READMEs updated additively: parser dispatch, dual-write outputs, harness usage, server params cross-check, wire-format hazard rewording (u8 on v2 path, range-checked; append-only rule unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
116 lines
8.1 KiB
Markdown
116 lines
8.1 KiB
Markdown
# 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. |
|
||
| `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. |
|
||
| `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 grid dominates: `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 at 8K.
|
||
|
||
### 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.
|