brink_format/save.rs
1//! Persistent, name-keyed save state for a story's game state.
2//!
3//! [`SaveState`] is the **durable** save format: globals, visit/turn counts,
4//! turn index, and RNG, keyed by stable identities (variable name; scope
5//! [`DefinitionId`]). It captures *game state only* — not execution position
6//! (call stack / PC), which can't be made version-tolerant — so a save
7//! survives a story recompile/patch as long as the relevant names/paths are
8//! unchanged. The runtime (`Story::save_state` / `load_state`) produces and
9//! reconciles it. See `docs/external-binding-foundation.md`.
10
11use std::collections::BTreeMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::{DefinitionId, Value};
16
17/// Current [`SaveState`] format version. Bump when the *format* changes
18/// (independent of the story's own content); `version` lets a loader migrate.
19pub const SAVE_FORMAT_VERSION: u16 = 1;
20
21/// A persistent, name-keyed snapshot of a story's game state.
22///
23/// Globals are keyed by variable name; visit/turn counts by scope
24/// [`DefinitionId`] (which serializes as a stable `"$tt_hash"` string), with an
25/// advisory author path attached when the scope is named.
26#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
27pub struct SaveState {
28 /// Save-format version (see [`SAVE_FORMAT_VERSION`]).
29 pub version: u16,
30 /// Global variables by name. `BTreeMap` for deterministic serialization.
31 pub globals: BTreeMap<String, Value>,
32 /// Visit counts by scope id, sorted by id for deterministic output.
33 pub visits: Vec<VisitEntry>,
34 /// Turn-since counts by scope id, sorted by id.
35 pub turns: Vec<VisitEntry>,
36 /// Global turn index.
37 pub turn_index: u32,
38 /// RNG seed.
39 pub rng_seed: i32,
40 /// Last drawn random value (so the RNG sequence resumes correctly).
41 pub previous_random: i32,
42}
43
44/// One visit/turn-count entry: a scope id and its count, plus (when the scope
45/// is a named knot/stitch) an advisory author path for human inspection. The
46/// `id` is the load key; `path` is cosmetic.
47#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
48pub struct VisitEntry {
49 /// The counted scope's definition id (load key).
50 pub id: DefinitionId,
51 /// Author path for a named scope, e.g. `"forest.clearing"`. Absent for
52 /// anonymous counted containers (gathers, choice points).
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub path: Option<String>,
55 /// The count.
56 pub count: u32,
57}
58
59/// What `Story::load_state` couldn't apply, so a host can surface it rather
60/// than have data silently vanish. Globals whose name no longer exists are
61/// **dropped** (no slot to hold them) and reported here. Visit/turn counts are
62/// never dropped — counts for scopes the current program lacks are retained
63/// harmlessly (unused until/unless the scope returns), so they aren't reported.
64#[derive(Default, Clone, Debug, PartialEq, Serialize)]
65pub struct LoadReport {
66 /// Saved global names with no matching global in the current program.
67 pub unknown_globals: Vec<String>,
68}
69
70impl LoadReport {
71 /// Whether the load applied cleanly (nothing dropped).
72 #[must_use]
73 pub fn is_clean(&self) -> bool {
74 self.unknown_globals.is_empty()
75 }
76}