Skip to main content

shuyi_core/
save.rs

1//! Save versions (game ADR-0039): every save names the version of its format
2//! that wrote it, and a build reads only its own version. A save of another
3//! version is refused, never migrated. [`SaveHeader`] reads the version of
4//! any save, even one whose other fields no longer parse, so the refusal
5//! can say "another version" rather than "damaged". What a save holds, and
6//! where it is kept, are the game's.
7
8use core::fmt;
9use core::marker::PhantomData;
10
11use serde::Deserialize;
12use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
13
14/// A game's save format: the type its saves are read into.
15pub trait SaveFormat {
16    /// The name the save's type is written under: the struct name a text
17    /// format may put before the save's fields.
18    const NAME: &'static str;
19    /// The version this build reads and writes. Raise it whenever a saved
20    /// type changes shape or meaning.
21    const VERSION: u32;
22}
23
24/// Just the version of a save of format `F`: reads any such save,
25/// whatever the rest of it holds, in a format that names its fields (RON,
26/// JSON).
27pub struct SaveHeader<F> {
28    /// The version that wrote the save.
29    pub version: u32,
30    format: PhantomData<fn() -> F>,
31}
32
33impl<F: SaveFormat> SaveHeader<F> {
34    /// Whether this build can read the save: it is of
35    /// [`F::VERSION`](SaveFormat::VERSION).
36    pub fn is_current(&self) -> bool {
37        self.version == F::VERSION
38    }
39}
40
41// By hand rather than derived, so that they ask nothing of `F`.
42impl<F> Clone for SaveHeader<F> {
43    fn clone(&self) -> Self {
44        *self
45    }
46}
47
48impl<F> Copy for SaveHeader<F> {}
49
50impl<F> PartialEq for SaveHeader<F> {
51    fn eq(&self, other: &Self) -> bool {
52        self.version == other.version
53    }
54}
55
56impl<F> Eq for SaveHeader<F> {}
57
58impl<F> fmt::Debug for SaveHeader<F> {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.debug_struct("SaveHeader")
61            .field("version", &self.version)
62            .finish()
63    }
64}
65
66impl<'de, F: SaveFormat> Deserialize<'de> for SaveHeader<F> {
67    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
68        let version = deserializer.deserialize_struct(F::NAME, &["version"], VersionOnly)?;
69        Ok(SaveHeader {
70            version,
71            format: PhantomData,
72        })
73    }
74}
75
76/// A save's fields: its version, and the others, which are skipped.
77#[derive(Deserialize)]
78#[serde(field_identifier, rename_all = "lowercase")]
79enum Field {
80    Version,
81    #[serde(other)]
82    Other,
83}
84
85/// Reads a save's `version` and skips its other fields.
86struct VersionOnly;
87
88impl<'de> Visitor<'de> for VersionOnly {
89    type Value = u32;
90
91    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_str("a save with a version")
93    }
94
95    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<u32, A::Error> {
96        let mut version = None;
97        while let Some(field) = map.next_key::<Field>()? {
98            match field {
99                Field::Version if version.is_some() => {
100                    return Err(de::Error::duplicate_field("version"));
101                }
102                Field::Version => version = Some(map.next_value()?),
103                Field::Other => {
104                    map.next_value::<IgnoredAny>()?;
105                }
106            }
107        }
108        version.ok_or_else(|| de::Error::missing_field("version"))
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use alloc::format;
115    use alloc::string::{String, ToString};
116
117    use serde::de::value::{Error, U32Deserializer};
118
119    use super::*;
120
121    struct Toy;
122
123    impl SaveFormat for Toy {
124        const NAME: &'static str = "ToySave";
125        const VERSION: u32 = 3;
126    }
127
128    type Header = SaveHeader<Toy>;
129
130    fn read(text: &str) -> Result<Header, String> {
131        ron::from_str(text).map_err(|e| e.to_string())
132    }
133
134    #[test]
135    fn the_header_reads_the_version_whatever_the_rest_holds() {
136        let header = read("(version:3,world:(seed:9),units:[1,2])").unwrap();
137        assert_eq!(header.version, 3);
138        assert!(header.is_current());
139        // Named, with the version last, from a build whose fields differ.
140        let header = read("ToySave(units:[(a:1)],era:\"old\",version:2)").unwrap();
141        assert_eq!(header.version, 2);
142        assert!(!header.is_current());
143        assert!(!read("(version:4)").unwrap().is_current());
144    }
145
146    #[test]
147    fn what_is_not_a_save_of_the_format_is_refused() {
148        assert!(read("OtherSave(version:3)").is_err());
149        assert!(read("not a save").is_err());
150        assert!(read("[3]").is_err());
151        let missing = read("(units:[1])").unwrap_err();
152        assert!(
153            missing.contains("missing field named `version`"),
154            "{missing}"
155        );
156        let twice = read("(version:3,version:4)").unwrap_err();
157        assert!(twice.contains("duplicate field named `version`"), "{twice}");
158    }
159
160    #[test]
161    fn headers_compare_and_show_their_version() {
162        let header = read("(version:2)").unwrap();
163        let copy = header;
164        assert_eq!(header, copy);
165        assert_ne!(header, read("(version:3)").unwrap());
166        assert_eq!(format!("{header:?}"), "SaveHeader { version: 2 }");
167    }
168
169    #[test]
170    fn the_error_says_a_save_was_expected() {
171        let error = Header::deserialize(U32Deserializer::<Error>::new(3)).unwrap_err();
172        assert!(
173            error.to_string().contains("expected a save with a version"),
174            "{error}"
175        );
176    }
177}