Skip to main content

fforge_core/
session.rs

1//! `Session` glues the pieces: it owns the append-only log and the folded
2//! state, routes commands through `step`, and notifies observers. Save/load
3//! is *literally* serialize/replay the log — there is no separate save-game
4//! format to drift out of sync.
5
6use crate::commands::{step, Command, CommandError};
7use crate::event::Event;
8use crate::observer::EventObserver;
9use crate::state::GameState;
10use std::io::{BufRead, Write};
11use std::path::Path;
12
13pub struct Session {
14    pub state: GameState,
15    pub log: Vec<Event>,
16}
17
18impl Session {
19    /// Start a session from a log (a fresh `[GameStarted]` or a loaded save).
20    /// Observers see every event, replayed ones included, so derived consumers
21    /// (telemetry, traces) rebuild for free.
22    pub fn from_events(log: Vec<Event>, observers: &mut [&mut dyn EventObserver]) -> Session {
23        let state = GameState::replay(&log);
24        for event in &log {
25            for obs in observers.iter_mut() {
26                obs.on_event(event);
27            }
28        }
29        Session { state, log }
30    }
31
32    /// Validate + produce events, append them, fold them, notify observers.
33    /// Returns the slice of newly produced events for presentation.
34    pub fn execute(
35        &mut self,
36        command: Command,
37        observers: &mut [&mut dyn EventObserver],
38    ) -> Result<&[Event], CommandError> {
39        let events = step(&self.state, command)?;
40        let start = self.log.len();
41        for event in events {
42            self.state.apply(&event);
43            for obs in observers.iter_mut() {
44                obs.on_event(&event);
45            }
46            self.log.push(event);
47        }
48        Ok(&self.log[start..])
49    }
50}
51
52/// JSON-lines event log persistence: one event per line, append-friendly,
53/// diffable, trivially inspectable. (SQLite — the settled storage choice —
54/// arrives when there is queryable state worth indexing, Phase 2 stats; see
55/// the delivery notes.)
56pub fn save_log(path: &Path, log: &[Event]) -> std::io::Result<()> {
57    let file = std::fs::File::create(path)?;
58    let mut writer = std::io::BufWriter::new(file);
59    for event in log {
60        let line = serde_json::to_string(event)
61            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
62        writeln!(writer, "{line}")?;
63    }
64    writer.flush()
65}
66
67pub fn load_log(path: &Path) -> std::io::Result<Vec<Event>> {
68    let file = std::fs::File::open(path)?;
69    let reader = std::io::BufReader::new(file);
70    let mut log = Vec::new();
71    for line in reader.lines() {
72        let line = line?;
73        if line.trim().is_empty() {
74            continue;
75        }
76        let event: Event = serde_json::from_str(&line)
77            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
78        log.push(event);
79    }
80    Ok(log)
81}