Skip to main content

qualia_client_core/
canvas_state.rs

1//! Runtime Chora navigation state (active world + temporal scrub position).
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8pub const STATE_FILE: &str = "wellfair/canvas_state.json";
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct CanvasNavigationState {
12    pub active_world_id: String,
13    /// Current temporal scrub position (unix seconds).
14    pub temporal_t: u64,
15    /// Spawn/decay ramp duration (seconds).
16    #[serde(default = "default_ramp")]
17    pub ramp_secs: u64,
18}
19
20fn default_ramp() -> u64 {
21    86_400 // one day fade
22}
23
24impl Default for CanvasNavigationState {
25    fn default() -> Self {
26        Self {
27            active_world_id: "q42:world:demo-offline".to_string(),
28            temporal_t: 1_750_000_000,
29            ramp_secs: default_ramp(),
30        }
31    }
32}
33
34pub fn load(storage_root: impl AsRef<Path>) -> CanvasNavigationState {
35    let path = storage_root.as_ref().join(STATE_FILE);
36    match fs::read(&path) {
37        Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(),
38        Err(_) => CanvasNavigationState::default(),
39    }
40}
41
42pub fn save(storage_root: impl AsRef<Path>, state: &CanvasNavigationState) -> std::io::Result<()> {
43    let path: PathBuf = storage_root.as_ref().join(STATE_FILE);
44    if let Some(parent) = path.parent() {
45        fs::create_dir_all(parent)?;
46    }
47    let tmp = path.with_extension("json.tmp");
48    let json =
49        serde_json::to_vec_pretty(state).map_err(|e| std::io::Error::other(e.to_string()))?;
50    fs::write(&tmp, &json)?;
51    fs::rename(&tmp, &path)?;
52    Ok(())
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn roundtrip() {
61        let dir = std::env::temp_dir().join(format!("chora-state-{}", std::process::id()));
62        let _ = fs::remove_dir_all(&dir);
63        let mut s = CanvasNavigationState::default();
64        s.temporal_t = 999;
65        save(&dir, &s).unwrap();
66        assert_eq!(load(&dir).temporal_t, 999);
67        let _ = fs::remove_dir_all(&dir);
68    }
69}