Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

D010: Simulation — Snapshottable State

Decision: Full sim state must be serializable/deserializable at any tick.

Rationale enables:

  • Save games (trivially)
  • Replay system (initial state + orders)
  • Desync debugging (diff snapshots between clients at divergence point)
  • Rollback netcode (restore state N frames back, replay with corrected inputs)
  • Cross-engine reconciliation (restore from authoritative checkpoint)
  • Automated testing (load known state, apply inputs, verify result)

Crash-safe serialization (from Valve Fossilize): Save files use an append-only write strategy with a final header update — the same pattern Valve uses in Fossilize (their pipeline cache serialization library, see research/valve-github-analysis.md § Part 3). The payload is written first into a temporary file; only after the full payload is fsynced does the header (containing checksum + payload length) get written atomically. If the process crashes mid-write, the incomplete temporary file is detected and discarded on next load — the previous valid save remains intact. This eliminates the “corrupted save file” failure mode that plagues games with naïve serialization.

Autosave threading: Autosave MUST NOT block the game loop thread. The game thread’s responsibilities are: (1) produce a SimCoreDelta via delta_snapshot(baseline) — fast (~0.5–1 ms for 500 units via ChangeMask bitfield iteration), and (2) capture the current CampaignState and ScriptState if either has changed since the last autosave (cheap clone / reference-counted snapshot — see state-recording.md for the identical pattern used by replay keyframes). The game thread sends the SimCoreDelta plus any changed non-sim state to the I/O thread via the same ring buffer used for SQLite events. The I/O thread applies the sim delta to its cached SimCoreSnapshot baseline, composes the full SimSnapshot { core, campaign_state, script_state }, then serializes and LZ4-compresses it into a standard .icsave file (see formats/save-replay-formats.md). On a 5400 RPM HDD, the fsync() call alone takes 50–200 ms — this latency is fully absorbed by the I/O thread. Autosave files are ordinary .icsave files (full SimSnapshot payload, not delta-only) — any save can be loaded independently without a baseline chain. The I/O thread updates its cached sim baseline after each full reconstruction; the autosave manager also caches last_campaign_state and last_script_state for delta comparison, following the same baseline pattern as StateRecorder in state-recording.md.

Delta encoding for snapshots: Periodic full snapshots (for save games, desync debugging) are complemented by delta snapshots that encode only changed state since the last full snapshot. Delta encoding uses property-level diffing: each ECS component that changed since the last snapshot is serialized; unchanged components are omitted. For a 500-unit game where ~10% of components change per tick, a delta snapshot is ~10x smaller than a full snapshot. This reduces game-thread snapshot cost (autosave transfers a small sim delta plus any changed campaign/script state to the I/O thread) and shrinks replay keyframes (~30 KB delta vs ~300 KB full). Reconnection sends a full SimSnapshot (not a delta) — delta encoding does not reduce reconnection bandwidth, but it does reduce the ongoing cost of the periodic keyframes used in replays and autosave. Inspired by Source Engine’s CNetworkVar per-field change detection (see research/valve-github-analysis.md § 2.2) and the SPROP_CHANGES_OFTEN priority flag — components that change every tick (position, health) are checked first during delta computation, improving cache locality. See 10-PERFORMANCE.md for the performance impact and 09-DECISIONS.md § D054 for the SnapshotCodec version dispatch.