use brink_format::{CountingFlags, LoadReport, SAVE_FORMAT_VERSION, SaveState, VisitEntry};
use crate::StoryRng;
use crate::debug::NameResolver;
use crate::program::Program;
use crate::state::ContextAccess;
use crate::story::Story;
#[must_use]
pub fn save_state<C: ContextAccess + ?Sized>(program: &Program, ctx: &C) -> SaveState {
let resolver = NameResolver::new(program);
let globals = (0..program.global_count())
.filter_map(|idx| {
program
.global_slot_name(idx as usize)
.map(|name| (name.to_owned(), ctx.global(idx).clone()))
})
.collect();
let mut visits = Vec::new();
let mut turns = Vec::new();
for container in &program.containers {
if !container.counting_flags.contains(CountingFlags::VISITS) {
continue;
}
let id = container.id;
let count = ctx.visit_count(id);
if count > 0 {
visits.push(VisitEntry {
id,
path: resolver.def_path(id).map(str::to_owned),
count,
});
}
if let Some(turn) = ctx.turn_count(id) {
turns.push(VisitEntry {
id,
path: resolver.def_path(id).map(str::to_owned),
count: turn,
});
}
}
visits.sort_by_key(|e| e.id.to_raw());
turns.sort_by_key(|e| e.id.to_raw());
SaveState {
version: SAVE_FORMAT_VERSION,
globals,
visits,
turns,
turn_index: ctx.turn_index(),
rng_seed: ctx.rng_seed(),
previous_random: ctx.previous_random(),
}
}
pub fn load_state<C: ContextAccess + ?Sized>(
program: &Program,
ctx: &mut C,
save: &SaveState,
) -> LoadReport {
let mut report = LoadReport::default();
for (name, value) in &save.globals {
match program.global_index(name) {
Some(idx) => ctx.set_global(idx, value.clone()),
None => report.unknown_globals.push(name.clone()),
}
}
ctx.set_turn_index(save.turn_index);
ctx.set_rng_seed(save.rng_seed);
ctx.set_previous_random(save.previous_random);
for e in &save.visits {
ctx.set_visit_count(e.id, e.count);
}
for e in &save.turns {
ctx.set_turn_count(e.id, e.count);
}
report
}
impl<R: StoryRng> Story<R> {
#[must_use]
pub fn save_state(&self) -> SaveState {
save_state(self.program(), &self.default_context)
}
pub fn load_state(&mut self, save: &SaveState) -> LoadReport {
let program = self.program_arc();
load_state(&program, &mut self.default_context, save)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::link;
use crate::rng::FastRng;
fn compile_for_flow(src: &str) -> (Program, Vec<Vec<brink_format::LineEntry>>) {
let out = brink_compiler::compile("t.ink", |p| {
if p == "t.ink" {
Ok(src.to_string())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no such include",
))
}
})
.expect("compile");
link(&out.data).expect("link")
}
#[test]
fn visits_are_sorted_by_id_regardless_of_visit_order() {
let (program, tables) = compile_for_flow(
"-> alpha\n\
=== alpha ===\n\
Alpha.\n\
-> DONE\n\
=== beta ===\n\
Beta.\n\
-> DONE\n\
=== gamma ===\n\
Gamma.\n\
-> DONE\n\
=== reader ===\n\
{READ_COUNT(-> alpha)} {READ_COUNT(-> beta)} {READ_COUNT(-> gamma)}\n\
-> DONE\n",
);
let program = Arc::new(program);
let mut story = crate::Story::<FastRng>::new(Arc::clone(&program), tables);
story.continue_maximally().expect("continue");
story.choose_path_string("gamma").expect("jump");
story.continue_maximally().expect("continue");
story.choose_path_string("beta").expect("jump");
story.continue_maximally().expect("continue");
let save = story.save_state();
assert_eq!(
save.visits.len(),
3,
"alpha/beta/gamma should each have a visit entry: {:?}",
save.visits
);
let ids: Vec<u64> = save.visits.iter().map(|e| e.id.to_raw()).collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(ids, sorted, "SaveState::visits must be sorted by id");
}
}