Skip to main content

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}