Skip to main content

mirage_engine/
save.rs

1//! Every value a game keeps between runs: what an earlier run stored, and
2//! whatever this one has saved over it.
3//!
4//! A game names its save keys as one vocabulary through
5//! `#[derive(Saves)]`, and reads and writes them with
6//! [`saved`](crate::FrameContext::saved) and
7//! [`save`](crate::FrameContext::save). A read always returns: the stored
8//! value where a run stored one, and the key's own
9//! [`fallback`](crate::SaveKey::fallback) where none did, so a first run
10//! needs no pass that stores the defaults. A value the store kept that no
11//! longer reads as the key's value returns that fallback too, with a debug
12//! log.
13//!
14//! A vocabulary holds one of four kinds of value — a whole number, a
15//! number, a flag, or text — and every key in it keeps that kind; keys of
16//! another kind need a vocabulary of their own.
17//!
18//! Writes are kept through a frame's ticks and its own calls, and stored
19//! once the frame ends, and only where something changed. The store is the
20//! platform's own, per title: the file beside the one the bindings are
21//! kept in on the desktop, and the browser's own storage on the web. A
22//! game run through the offscreen session keeps nothing and reads the
23//! fallbacks. Nothing drops a key; a game writes the value the next read
24//! should return.
25
26use std::collections::BTreeMap;
27
28use crate::platform::Store;
29
30use sealed::Kept;
31
32/// The store's first line; text that does not start with it loads nothing.
33const HEADER: &str = "mirage-engine saves 1";
34
35/// Save key value types: `bool`, `i64`, `f64` or `String`, and nothing
36/// else.
37///
38/// A game's own type is kept as one of these: an enum as a number, a whole
39/// state as text.
40pub trait SaveValue: Kept {}
41
42/// Trait every save vocabulary implements, written by
43/// [`Saves`](macro@crate::Saves).
44///
45/// Required if you want a vocabulary of keys; what each of them keeps is
46/// declared by hand, in [`SaveKey`].
47pub trait Saves {
48    /// Name the store keeps this key under, made of the vocabulary's name
49    /// and this key's own name.
50    fn name(&self) -> &'static str;
51}
52
53/// One thing a game keeps between runs, named by a vocabulary of its own.
54///
55/// Required if you want [`save`](crate::FrameContext::save) and
56/// [`saved`](crate::FrameContext::saved) to take a key: they read and keep its
57/// [`Value`](Self::Value) and nothing else. Every key of a vocabulary
58/// keeps the same kind of value; keys of another kind need a vocabulary of
59/// their own.
60pub trait SaveKey: Saves {
61    /// The value this key keeps, of the four a store takes.
62    type Value: SaveValue;
63
64    /// Value this key reads as before any run has saved it. Saving this
65    /// value again leaves the key at the same fallback.
66    fn fallback(&self) -> Self::Value;
67}
68
69/// Every entry this run reads through: what the store kept of the runs
70/// before it, and whatever this one has saved since.
71pub(crate) struct Saved {
72    /// By name, in an order the store writes the same way twice.
73    entries: BTreeMap<String, String>,
74    dirty: bool,
75    store: Store,
76}
77
78impl Saved {
79    /// State a run starts with: whatever `store` kept, which every key of
80    /// this run reads through.
81    pub(crate) fn new(store: Store) -> Self {
82        Self {
83            entries: entries(store.read().as_deref()),
84            dirty: false,
85            store,
86        }
87    }
88
89    /// Value last saved under `key`; falls back to `key.fallback()` if no
90    /// run has saved it, or if what was saved no longer reads back as
91    /// this key's value.
92    pub(crate) fn read<K: SaveKey>(&self, key: K) -> K::Value {
93        let Some(kept) = self.entries.get(key.name()) else {
94            return key.fallback();
95        };
96        K::Value::read(kept).unwrap_or_else(|| {
97            log::debug!(
98                "what the store kept for `{}` does not read as its value: {kept}",
99                key.name()
100            );
101            key.fallback()
102        })
103    }
104
105    /// Keeps `value` under `key`, and marks the store to be written where
106    /// that is not what it already holds.
107    pub(crate) fn write<K: SaveKey>(&mut self, key: K, value: K::Value) {
108        let written = value.written();
109        let kept = self.entries.get(key.name());
110        if kept.is_some_and(|kept| *kept == written) {
111            return;
112        }
113        self.entries.insert(key.name().to_owned(), written);
114        self.dirty = true;
115    }
116
117    /// Writes the whole store where a value has changed since the last
118    /// call, and nothing at all where none has.
119    pub(crate) fn flush(&mut self) {
120        if core::mem::take(&mut self.dirty) {
121            self.store.write(&self.written());
122        }
123    }
124
125    /// Every entry as the store keeps it, with the ones no key of this run
126    /// names left in place.
127    fn written(&self) -> String {
128        let mut out = String::from(HEADER);
129        out.push('\n');
130        for (name, value) in &self.entries {
131            escaped(name, &mut out);
132            out.push(' ');
133            out.push_str(value);
134            out.push('\n');
135        }
136        out
137    }
138}
139
140/// Every entry one store's text holds, or nothing where it is not text
141/// this version wrote.
142fn entries(text: Option<&str>) -> BTreeMap<String, String> {
143    let Some(text) = text else {
144        return BTreeMap::new();
145    };
146
147    let mut lines = text.lines();
148    if lines.next().map(str::trim) != Some(HEADER) {
149        log::debug!("what the store kept is not this version's; every key reads as its fallback");
150        return BTreeMap::new();
151    }
152
153    lines
154        .filter(|line| !line.is_empty())
155        .filter_map(|line| {
156            let read = line
157                .split_once(' ')
158                .and_then(|(name, value)| Some((unescaped(name)?, value.to_owned())));
159            if read.is_none() {
160                log::debug!("a kept entry was dropped: {line}");
161            }
162            read
163        })
164        .collect()
165}
166
167/// Writes `text` as one word, so that a name and a value keep whatever
168/// spaces and line ends they were saved with.
169fn escaped(text: &str, out: &mut String) {
170    for letter in text.chars() {
171        match letter {
172            '\\' => out.push_str("\\\\"),
173            '\n' => out.push_str("\\n"),
174            '\r' => out.push_str("\\r"),
175            ' ' => out.push_str("\\s"),
176            _ => out.push(letter),
177        }
178    }
179}
180
181/// The text [`escaped`] wrote, or nothing where the word is not one it
182/// could have written.
183fn unescaped(text: &str) -> Option<String> {
184    let mut out = String::with_capacity(text.len());
185    let mut letters = text.chars();
186    while let Some(letter) = letters.next() {
187        match letter {
188            '\\' => out.push(match letters.next()? {
189                '\\' => '\\',
190                'n' => '\n',
191                'r' => '\r',
192                's' => ' ',
193                _ => return None,
194            }),
195            _ => out.push(letter),
196        }
197    }
198    Some(out)
199}
200
201/// The values a store keeps as one word each, written and read back by the
202/// standard types themselves.
203macro_rules! parsed {
204    ($($value:ty),*) => {$(
205        impl Kept for $value {
206            fn written(&self) -> String {
207                self.to_string()
208            }
209
210            fn read(text: &str) -> Option<Self> {
211                text.parse().ok()
212            }
213        }
214
215        impl SaveValue for $value {}
216    )*};
217}
218
219parsed!(bool, i64, f64);
220
221impl Kept for String {
222    fn written(&self) -> String {
223        let mut out = String::with_capacity(self.len());
224        escaped(self, &mut out);
225        out
226    }
227
228    fn read(text: &str) -> Option<Self> {
229        unescaped(text)
230    }
231}
232
233impl SaveValue for String {}
234
235/// Sealed: `Kept` is `pub` so code can name it, and this module is
236/// `pub(crate)` so only this crate can implement it.
237pub(crate) mod sealed {
238    /// The store's write and read of one kind of value; the text is one
239    /// word, so a line is a name and a value.
240    pub trait Kept: Sized {
241        fn written(&self) -> String;
242        fn read(text: &str) -> Option<Self>;
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    /// A game's four vocabularies, one per kind of value it keeps.
251    macro_rules! vocabulary {
252        ($name:ident, $value:ty, $fallback:expr) => {
253            #[derive(Clone, Copy)]
254            struct $name;
255
256            impl Saves for $name {
257                fn name(&self) -> &'static str {
258                    stringify!($name)
259                }
260            }
261
262            impl SaveKey for $name {
263                type Value = $value;
264
265                fn fallback(&self) -> $value {
266                    $fallback
267                }
268            }
269        };
270    }
271
272    vocabulary!(Score, i64, 0);
273    vocabulary!(Lap, f64, 0.0);
274    vocabulary!(Seen, bool, false);
275    vocabulary!(Player, String, "nobody".to_owned());
276
277    /// Text with everything a line cannot hold as it is.
278    fn awkward() -> String {
279        "  two\nlines \\ and a tab\t ".to_owned()
280    }
281
282    fn saves(kept: Option<&str>) -> Saved {
283        Saved {
284            entries: entries(kept),
285            dirty: false,
286            store: Store::saves(None),
287        }
288    }
289
290    #[test]
291    fn every_kind_of_value_reads_back_as_it_was_saved() {
292        let mut written = saves(None);
293        written.write(Score, 120);
294        written.write(Lap, -0.5);
295        written.write(Seen, true);
296        written.write(Player, awkward());
297
298        let read = saves(Some(&written.written()));
299
300        assert_eq!(read.read(Score), 120);
301        assert_eq!(read.read(Lap), -0.5);
302        assert!(read.read(Seen));
303        assert_eq!(read.read(Player), awkward());
304        assert_eq!(
305            read.written(),
306            written.written(),
307            "and writes the same store again"
308        );
309    }
310
311    #[test]
312    fn a_key_no_run_kept_reads_as_its_fallback_and_what_was_kept_reads_over_it() {
313        let mut written = saves(None);
314        written.write(Score, 7);
315
316        let read = saves(Some(&written.written()));
317
318        assert_eq!(read.read(Score), 7);
319        assert_eq!(read.read(Lap), 0.0, "which the store never kept");
320        assert_eq!(read.read(Player), "nobody");
321    }
322
323    #[test]
324    fn an_entry_no_key_of_this_run_names_is_carried_through_a_rewrite() {
325        let kept = format!("{HEADER}\nScore 3\nFurthest\\sLevel 9\n");
326
327        let mut read = saves(Some(&kept));
328        read.write(Score, 4);
329
330        assert_eq!(read.read(Score), 4);
331        assert_eq!(
332            read.written(),
333            format!("{HEADER}\nFurthest\\sLevel 9\nScore 4\n"),
334            "the entry this run knows nothing of is written back as it was"
335        );
336    }
337
338    #[test]
339    fn a_store_that_reads_as_nothing_leaves_every_fallback_standing() {
340        for broken in [
341            "",
342            "nonsense",
343            "mirage-engine saves 2\nScore 5\n",
344            &format!("{HEADER}\nScore\n"),
345            &format!("{HEADER}\nScore twelve\n"),
346            &format!("{HEADER}\nScore\\q 5\n"),
347        ] {
348            let read = saves(Some(broken));
349            assert_eq!(read.read(Score), 0, "`{broken}` left the fallback standing");
350        }
351    }
352
353    #[test]
354    fn saving_a_value_the_store_already_says_leaves_it_with_nothing_to_write() {
355        let mut written = saves(None);
356        written.write(Score, 42);
357        assert!(written.dirty);
358
359        let mut read = saves(Some(&written.written()));
360        read.write(Score, 42);
361
362        assert!(!read.dirty, "the same value again is not a change");
363        read.write(Score, 43);
364        assert!(read.dirty);
365    }
366
367    #[test]
368    fn a_store_mangled_any_which_way_still_reads_as_one_this_run_can_use() {
369        let mut written = saves(None);
370        written.write(Score, 120);
371        written.write(Player, awkward());
372        let kept = written.written();
373
374        for mangled in crate::platform::manglings(&kept) {
375            let mut read = saves(Some(&mangled));
376            read.write(Score, 7);
377
378            assert_eq!(read.read(Score), 7, "over {mangled:?}");
379            assert_eq!(
380                saves(Some(&read.written())).written(),
381                read.written(),
382                "and what it writes reads back the same, over {mangled:?}"
383            );
384        }
385    }
386
387    #[test]
388    fn a_run_with_no_title_keeps_nothing_and_still_reads_what_it_saved() {
389        let mut saves = Saved::new(Store::saves(None));
390        saves.write(Score, 5);
391        saves.flush();
392
393        assert_eq!(saves.read(Score), 5);
394        assert_eq!(Saved::new(Store::saves(None)).read(Score), 0);
395    }
396}