use std::collections::BTreeMap;
use crate::platform::Store;
use sealed::Kept;
const HEADER: &str = "mirage-engine saves 1";
pub trait SaveValue: Kept {}
pub trait Saves {
fn name(&self) -> &'static str;
}
pub trait SaveKey: Saves {
type Value: SaveValue;
fn fallback(&self) -> Self::Value;
}
pub(crate) struct Saved {
entries: BTreeMap<String, String>,
dirty: bool,
store: Store,
}
impl Saved {
pub(crate) fn new(store: Store) -> Self {
Self {
entries: entries(store.read().as_deref()),
dirty: false,
store,
}
}
pub(crate) fn read<K: SaveKey>(&self, key: K) -> K::Value {
let Some(kept) = self.entries.get(key.name()) else {
return key.fallback();
};
K::Value::read(kept).unwrap_or_else(|| {
log::debug!(
"what the store kept for `{}` does not read as its value: {kept}",
key.name()
);
key.fallback()
})
}
pub(crate) fn write<K: SaveKey>(&mut self, key: K, value: K::Value) {
let written = value.written();
let kept = self.entries.get(key.name());
if kept.is_some_and(|kept| *kept == written) {
return;
}
self.entries.insert(key.name().to_owned(), written);
self.dirty = true;
}
pub(crate) fn flush(&mut self) {
if core::mem::take(&mut self.dirty) {
self.store.write(&self.written());
}
}
fn written(&self) -> String {
let mut out = String::from(HEADER);
out.push('\n');
for (name, value) in &self.entries {
escaped(name, &mut out);
out.push(' ');
out.push_str(value);
out.push('\n');
}
out
}
}
fn entries(text: Option<&str>) -> BTreeMap<String, String> {
let Some(text) = text else {
return BTreeMap::new();
};
let mut lines = text.lines();
if lines.next().map(str::trim) != Some(HEADER) {
log::debug!("what the store kept is not this version's; every key reads as its fallback");
return BTreeMap::new();
}
lines
.filter(|line| !line.is_empty())
.filter_map(|line| {
let read = line
.split_once(' ')
.and_then(|(name, value)| Some((unescaped(name)?, value.to_owned())));
if read.is_none() {
log::debug!("a kept entry was dropped: {line}");
}
read
})
.collect()
}
fn escaped(text: &str, out: &mut String) {
for letter in text.chars() {
match letter {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
' ' => out.push_str("\\s"),
_ => out.push(letter),
}
}
}
fn unescaped(text: &str) -> Option<String> {
let mut out = String::with_capacity(text.len());
let mut letters = text.chars();
while let Some(letter) = letters.next() {
match letter {
'\\' => out.push(match letters.next()? {
'\\' => '\\',
'n' => '\n',
'r' => '\r',
's' => ' ',
_ => return None,
}),
_ => out.push(letter),
}
}
Some(out)
}
macro_rules! parsed {
($($value:ty),*) => {$(
impl Kept for $value {
fn written(&self) -> String {
self.to_string()
}
fn read(text: &str) -> Option<Self> {
text.parse().ok()
}
}
impl SaveValue for $value {}
)*};
}
parsed!(bool, i64, f64);
impl Kept for String {
fn written(&self) -> String {
let mut out = String::with_capacity(self.len());
escaped(self, &mut out);
out
}
fn read(text: &str) -> Option<Self> {
unescaped(text)
}
}
impl SaveValue for String {}
pub(crate) mod sealed {
pub trait Kept: Sized {
fn written(&self) -> String;
fn read(text: &str) -> Option<Self>;
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! vocabulary {
($name:ident, $value:ty, $fallback:expr) => {
#[derive(Clone, Copy)]
struct $name;
impl Saves for $name {
fn name(&self) -> &'static str {
stringify!($name)
}
}
impl SaveKey for $name {
type Value = $value;
fn fallback(&self) -> $value {
$fallback
}
}
};
}
vocabulary!(Score, i64, 0);
vocabulary!(Lap, f64, 0.0);
vocabulary!(Seen, bool, false);
vocabulary!(Player, String, "nobody".to_owned());
fn awkward() -> String {
" two\nlines \\ and a tab\t ".to_owned()
}
fn saves(kept: Option<&str>) -> Saved {
Saved {
entries: entries(kept),
dirty: false,
store: Store::saves(None),
}
}
#[test]
fn every_kind_of_value_reads_back_as_it_was_saved() {
let mut written = saves(None);
written.write(Score, 120);
written.write(Lap, -0.5);
written.write(Seen, true);
written.write(Player, awkward());
let read = saves(Some(&written.written()));
assert_eq!(read.read(Score), 120);
assert_eq!(read.read(Lap), -0.5);
assert!(read.read(Seen));
assert_eq!(read.read(Player), awkward());
assert_eq!(
read.written(),
written.written(),
"and writes the same store again"
);
}
#[test]
fn a_key_no_run_kept_reads_as_its_fallback_and_what_was_kept_reads_over_it() {
let mut written = saves(None);
written.write(Score, 7);
let read = saves(Some(&written.written()));
assert_eq!(read.read(Score), 7);
assert_eq!(read.read(Lap), 0.0, "which the store never kept");
assert_eq!(read.read(Player), "nobody");
}
#[test]
fn an_entry_no_key_of_this_run_names_is_carried_through_a_rewrite() {
let kept = format!("{HEADER}\nScore 3\nFurthest\\sLevel 9\n");
let mut read = saves(Some(&kept));
read.write(Score, 4);
assert_eq!(read.read(Score), 4);
assert_eq!(
read.written(),
format!("{HEADER}\nFurthest\\sLevel 9\nScore 4\n"),
"the entry this run knows nothing of is written back as it was"
);
}
#[test]
fn a_store_that_reads_as_nothing_leaves_every_fallback_standing() {
for broken in [
"",
"nonsense",
"mirage-engine saves 2\nScore 5\n",
&format!("{HEADER}\nScore\n"),
&format!("{HEADER}\nScore twelve\n"),
&format!("{HEADER}\nScore\\q 5\n"),
] {
let read = saves(Some(broken));
assert_eq!(read.read(Score), 0, "`{broken}` left the fallback standing");
}
}
#[test]
fn saving_a_value_the_store_already_says_leaves_it_with_nothing_to_write() {
let mut written = saves(None);
written.write(Score, 42);
assert!(written.dirty);
let mut read = saves(Some(&written.written()));
read.write(Score, 42);
assert!(!read.dirty, "the same value again is not a change");
read.write(Score, 43);
assert!(read.dirty);
}
#[test]
fn a_store_mangled_any_which_way_still_reads_as_one_this_run_can_use() {
let mut written = saves(None);
written.write(Score, 120);
written.write(Player, awkward());
let kept = written.written();
for mangled in crate::platform::manglings(&kept) {
let mut read = saves(Some(&mangled));
read.write(Score, 7);
assert_eq!(read.read(Score), 7, "over {mangled:?}");
assert_eq!(
saves(Some(&read.written())).written(),
read.written(),
"and what it writes reads back the same, over {mangled:?}"
);
}
}
#[test]
fn a_run_with_no_title_keeps_nothing_and_still_reads_what_it_saved() {
let mut saves = Saved::new(Store::saves(None));
saves.write(Score, 5);
saves.flush();
assert_eq!(saves.read(Score), 5);
assert_eq!(Saved::new(Store::saves(None)).read(Score), 0);
}
}