plasma-prp 0.1.0

Read, write, inspect, and manipulate Plasma engine PRP files used by Myst Online: Uru Live
Documentation
//! SDL state persistence — save/load age SDL variable state to JSON files.
//!
//! When scripts change SDL variables via PtGetAgeSDL()[key] = value,
//! those changes are captured and persisted so they survive age reloads.
//! C++ ref: plNetClientMgr handles SDL state sync; we persist locally.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// Persistent store for SDL variable state across age reloads.
///
/// State is stored as JSON files in a `save/` directory next to game data:
/// `save/<AgeName>_sdl.json`
pub struct SdlStateStore {
    save_dir: PathBuf,
}

/// A single SDL variable value as stored in JSON.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum SdlValue {
    Int(i64),
    Float(f64),
    Bool(bool),
    String(String),
    Tuple(Vec<SdlValue>),
}

/// All SDL state for one age.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct AgeSdlState {
    pub variables: HashMap<String, Vec<SdlValue>>,
}

impl SdlStateStore {
    /// Create a new store. `save_dir` is the directory where state files live.
    pub fn new(save_dir: &Path) -> Self {
        Self {
            save_dir: save_dir.to_path_buf(),
        }
    }

    /// Load saved SDL state for an age. Returns empty state if no save exists.
    pub fn load(&self, age_name: &str) -> AgeSdlState {
        let path = self.state_path(age_name);
        match std::fs::read_to_string(&path) {
            Ok(json) => {
                match serde_json::from_str::<AgeSdlState>(&json) {
                    Ok(state) => {
                        log::info!("SDL: Loaded {} saved variables for '{}'",
                            state.variables.len(), age_name);
                        state
                    }
                    Err(e) => {
                        log::warn!("SDL: Failed to parse {}: {}", path.display(), e);
                        AgeSdlState::default()
                    }
                }
            }
            Err(_) => AgeSdlState::default(),
        }
    }

    /// Save SDL state for an age.
    pub fn save(&self, age_name: &str, state: &AgeSdlState) {
        if state.variables.is_empty() {
            return;
        }
        if let Err(e) = std::fs::create_dir_all(&self.save_dir) {
            log::warn!("SDL: Failed to create save dir: {}", e);
            return;
        }
        let path = self.state_path(age_name);
        match serde_json::to_string_pretty(state) {
            Ok(json) => {
                if let Err(e) = std::fs::write(&path, json) {
                    log::warn!("SDL: Failed to write {}: {}", path.display(), e);
                } else {
                    log::info!("SDL: Saved {} variables for '{}' to {}",
                        state.variables.len(), age_name, path.display());
                }
            }
            Err(e) => log::warn!("SDL: Failed to serialize state: {}", e),
        }
    }

    /// Set a single variable and persist.
    pub fn set_var(&self, age_name: &str, var_name: &str, values: Vec<SdlValue>) {
        let mut state = self.load(age_name);
        state.variables.insert(var_name.to_string(), values);
        self.save(age_name, &state);
    }

    fn state_path(&self, age_name: &str) -> PathBuf {
        self.save_dir.join(format!("{}_sdl.json", age_name))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sdl_state_roundtrip() {
        let dir = std::env::temp_dir().join("plasma_sdl_test");
        let _ = std::fs::remove_dir_all(&dir);

        let store = SdlStateStore::new(&dir);

        // Initially empty
        let state = store.load("TestAge");
        assert!(state.variables.is_empty());

        // Save some state
        let mut state = AgeSdlState::default();
        state.variables.insert("testBool".into(), vec![SdlValue::Bool(true)]);
        state.variables.insert("testInt".into(), vec![SdlValue::Int(42)]);
        store.save("TestAge", &state);

        // Reload
        let loaded = store.load("TestAge");
        assert_eq!(loaded.variables.len(), 2);
        assert!(matches!(&loaded.variables["testBool"][0], SdlValue::Bool(true)));
        assert!(matches!(&loaded.variables["testInt"][0], SdlValue::Int(42)));

        // Cleanup
        let _ = std::fs::remove_dir_all(&dir);
    }
}