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