Something went wrong. Try again.
a tiny pet with an even tinier brain, living in your browser
Something went wrong. Try again.
4.8 kB · 150 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151//! New saves, reading older saves and refusing ones this version cannot read.//! A save that fails here is never overwritten by the browser (the "bad egg").use crate::{ body::{HOUR, date_key}, types::*,};use serde_json::{Map, Value, json};
/// Save versions. `MIGRATIONS[n]` turns a version n+1 save into version n+2;/// bump `VERSION` and add one whenever a stored shape changes.pub const VERSION: u32 = 2;type Migration = fn(Map<String, Value>, &Hosting) -> Map<String, Value>;const MIGRATIONS: &[Migration] = &[|mut s, hosting| { // v2 records where the pet runs; every v1 save ran in its browser. s.insert("v".into(), json!(2)); s.insert("hosting".into(), json!(hosting)); s}];
pub fn fresh(t: f64, time_zone: &str) -> PetState { let hosting = Hosting { mode: "local".into(), brain_id: None, time_zone: time_zone.into(), }; let mut state = PetState { v: VERSION, created: t, owner: String::new(), pet: None, egg: None, stats: Stats { tummy: 70.0, fun: 70.0, energy: 80.0, clean: 90.0, }, sick: false, poop: 0, asleep: false, lights_off: false, memories: vec![], shelf: vec![], chat: vec![], diary: vec![], today: Today { date: String::new(), log: DayLog::default(), }, history: Default::default(), offer: None, woke_at: 0.0, last_tick: t, last_active: t, grime_since: 0.0, next_poop: t + 3.0 * HOUR, settings: Settings { chatter: true, extra: Map::new(), }, counts: Counts { fed: 0, pats: 0, games: 0, baths: 0, }, hosting, extra: Map::new(), }; state.today.date = date_key(&state, t); state}
#[derive(Clone, Debug, PartialEq, serde::Serialize)]pub struct SaveError { pub message: String, /// The save came from a newer version: keep it for that version. pub newer: bool,}impl SaveError { fn new(message: impl Into<String>) -> Self { Self { message: message.into(), newer: false, } }}impl std::fmt::Display for SaveError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.message) }}impl std::error::Error for SaveError {}
/// Reads a stored or exported save, migrating older versions step by step./// Missing top-level fields get a new save's defaults, as earlier browsers did.pub fn parse_save(raw: &str, t: f64, time_zone: &str) -> Result<PetState, SaveError> { let value: Value = serde_json::from_str(raw).map_err(|_| SaveError::new("it is damaged"))?; let Value::Object(mut s) = value else { return Err(SaveError::new("it isn't a subtokenling save")); }; let Some(mut v) = s.get("v").and_then(Value::as_u64).filter(|v| *v >= 1) else { return Err(SaveError::new("it isn't a subtokenling save")); }; if v > VERSION as u64 { return Err(SaveError { message: format!( "it was saved by a newer version of subtokenling (save version {v}, this page reads {VERSION})" ), newer: true, }); } let base = fresh(t, time_zone); while v < VERSION as u64 { s = MIGRATIONS[v as usize - 1](s, &base.hosting); if s.get("v").and_then(Value::as_u64) != Some(v + 1) { return Err(SaveError::new(format!( "updating it from version {v} failed" ))); } v += 1; } if !s.get("chat").is_some_and(Value::is_array) || !s.get("diary").is_some_and(Value::is_array) || !s.get("stats").is_some_and(Value::is_object) { return Err(SaveError::new("parts of it are missing")); } let hosting_ok = s.get("hosting").is_some_and(|h| { matches!( h.get("mode").and_then(Value::as_str), Some("local" | "remote") ) && h.get("timeZone").is_some_and(Value::is_string) && h.get("brainId") .is_some_and(|b| b.is_null() || b.is_string()) }); if !hosting_ok { return Err(SaveError::new( "its execution setting is missing or damaged", )); } let Value::Object(mut merged) = serde_json::to_value(&base).unwrap() else { unreachable!() }; merged.extend(s); let state: PetState = serde_json::from_value(Value::Object(merged)) .map_err(|e| SaveError::new(format!("parts of it are damaged ({e})")))?; state.validate().map_err(|e| SaveError::new(e.0))?; Ok(state)}