islaApocalypse/Core/Scripts/BLUEPRINT_FORMAT.md
beezm a7e6c17422 docs: BLUEPRINT_FORMAT.md + README updates for the v2 container (terrain-water task 02)
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>
2026-08-06 07:31:03 -04:00

116 lines
8.1 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. |
| `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.