use crate::components::{
AccessControl, CarCall, DestinationQueue, Elevator, HallCall, Line, Patience, Position,
Preferences, Rider, Route, Stop, Velocity,
};
use crate::entity::EntityId;
use crate::ids::GroupId;
use crate::metrics::Metrics;
use crate::stop::StopId;
use crate::tagged_metrics::MetricTags;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySnapshot {
pub original_id: EntityId,
pub position: Option<Position>,
pub velocity: Option<Velocity>,
pub elevator: Option<Elevator>,
pub stop: Option<Stop>,
pub rider: Option<Rider>,
pub route: Option<Route>,
#[serde(default)]
pub line: Option<Line>,
pub patience: Option<Patience>,
pub preferences: Option<Preferences>,
#[serde(default)]
pub access_control: Option<AccessControl>,
pub disabled: bool,
#[cfg(feature = "energy")]
#[serde(default)]
pub energy_profile: Option<crate::energy::EnergyProfile>,
#[cfg(feature = "energy")]
#[serde(default)]
pub energy_metrics: Option<crate::energy::EnergyMetrics>,
#[serde(default)]
pub service_mode: Option<crate::components::ServiceMode>,
#[serde(default)]
pub destination_queue: Option<DestinationQueue>,
#[serde(default)]
pub car_calls: Vec<CarCall>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorldSnapshot {
#[serde(default)]
pub version: u32,
pub tick: u64,
pub dt: f64,
pub entities: Vec<EntitySnapshot>,
pub groups: Vec<GroupSnapshot>,
pub stop_lookup: BTreeMap<StopId, usize>,
pub metrics: Metrics,
pub metric_tags: MetricTags,
pub extensions: BTreeMap<String, BTreeMap<EntityId, String>>,
pub ticks_per_second: f64,
#[serde(default)]
pub hall_calls: Vec<HallCall>,
#[serde(default)]
pub arrival_log: crate::arrival_log::ArrivalLog,
#[serde(default)]
pub arrival_log_retention: crate::arrival_log::ArrivalLogRetention,
#[serde(default)]
pub destination_log: crate::arrival_log::DestinationLog,
#[serde(default)]
pub traffic_detector: crate::traffic_detector::TrafficDetector,
#[serde(default)]
pub reposition_cooldowns: crate::dispatch::reposition::RepositionCooldowns,
#[serde(default)]
pub dispatch_config: BTreeMap<GroupId, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineSnapshotInfo {
pub entity_index: usize,
pub elevator_indices: Vec<usize>,
pub stop_indices: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupSnapshot {
pub id: GroupId,
pub name: String,
pub elevator_indices: Vec<usize>,
pub stop_indices: Vec<usize>,
pub strategy: crate::dispatch::BuiltinStrategy,
#[serde(default)]
pub lines: Vec<LineSnapshotInfo>,
#[serde(default)]
pub reposition: Option<crate::dispatch::BuiltinReposition>,
#[serde(default)]
pub hall_call_mode: crate::dispatch::HallCallMode,
#[serde(default)]
pub ack_latency_ticks: u32,
}
pub(crate) struct PendingExtensions(pub(crate) BTreeMap<String, BTreeMap<EntityId, String>>);
pub type CustomStrategyFactory<'a> =
Option<&'a dyn Fn(&str) -> Option<Box<dyn crate::dispatch::DispatchStrategy>>>;
#[derive(Clone, Copy, Default)]
#[non_exhaustive]
pub struct RestoreOptions<'a> {
pub custom_strategy_factory: CustomStrategyFactory<'a>,
}
impl<'a> RestoreOptions<'a> {
#[must_use]
pub fn with_factory(
factory: &'a dyn Fn(&str) -> Option<Box<dyn crate::dispatch::DispatchStrategy>>,
) -> Self {
Self {
custom_strategy_factory: Some(factory),
}
}
}
impl WorldSnapshot {
pub const CURRENT_SCHEMA_VERSION: u32 = SNAPSHOT_SCHEMA_VERSION;
pub fn migrate(self) -> Result<Self, crate::error::SimError> {
if self.version > Self::CURRENT_SCHEMA_VERSION {
return Err(crate::error::SimError::SnapshotVersion {
saved: format!("schema {}", self.version),
current: format!("schema {}", Self::CURRENT_SCHEMA_VERSION),
});
}
if self.version == Self::CURRENT_SCHEMA_VERSION {
return Ok(self);
}
Err(crate::error::SimError::SnapshotVersion {
saved: format!("schema {}", self.version),
current: format!("schema {}", Self::CURRENT_SCHEMA_VERSION),
})
}
pub fn restore(
self,
options: RestoreOptions<'_>,
) -> Result<crate::sim::Simulation, crate::error::SimError> {
use crate::world::{SortedStops, World};
let RestoreOptions {
custom_strategy_factory,
..
} = options;
if self.version != SNAPSHOT_SCHEMA_VERSION {
return Err(crate::error::SimError::SnapshotVersion {
saved: format!("schema {}", self.version),
current: format!("schema {SNAPSHOT_SCHEMA_VERSION}"),
});
}
let mut world = World::new();
let (index_to_id, id_remap) = Self::spawn_entities(&mut world, &self.entities);
Self::attach_components(&mut world, &self.entities, &index_to_id, &id_remap);
self.attach_hall_calls(&mut world, &id_remap);
let mut sorted: Vec<(f64, EntityId)> = world
.iter_stops()
.map(|(eid, stop)| (stop.position, eid))
.collect();
sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
world.insert_resource(SortedStops(sorted));
let (mut groups, stop_lookup, dispatchers, strategy_ids) =
self.rebuild_groups_and_dispatchers(&index_to_id, custom_strategy_factory)?;
Self::fix_legacy_line_entities(&mut groups, &mut world);
Self::install_runtime_resources(
&mut world,
&id_remap,
self.tick,
&self.extensions,
self.metric_tags,
self.arrival_log,
self.arrival_log_retention,
self.destination_log,
self.traffic_detector,
self.reposition_cooldowns,
);
let mut sim = crate::sim::Simulation::from_parts(
world,
self.tick,
self.dt,
groups,
stop_lookup,
dispatchers,
strategy_ids,
self.metrics,
self.ticks_per_second,
);
Self::replay_dispatcher_tuning(&mut sim, &self.dispatch_config);
Self::restore_reposition_strategies(&mut sim, &self.groups);
Self::emit_dangling_warnings(
&self.entities,
&self.hall_calls,
&id_remap,
self.tick,
&mut sim,
);
Ok(sim)
}
fn fix_legacy_line_entities(
groups: &mut [crate::dispatch::ElevatorGroup],
world: &mut crate::world::World,
) {
for group in groups.iter_mut() {
let group_id = group.id();
for line_info in group.lines_mut().iter_mut() {
if line_info.entity() != EntityId::default() {
continue;
}
let (min_pos, max_pos) = line_info
.serves()
.iter()
.filter_map(|&sid| world.stop(sid).map(|s| s.position))
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
(lo.min(p), hi.max(p))
});
let line_eid = world.spawn();
world.set_line(
line_eid,
Line {
name: format!("Legacy-{group_id}"),
group: group_id,
orientation: crate::components::Orientation::Vertical,
position: None,
min_position: if min_pos.is_finite() { min_pos } else { 0.0 },
max_position: if max_pos.is_finite() { max_pos } else { 0.0 },
max_cars: None,
},
);
for &elev_eid in line_info.elevators() {
if let Some(car) = world.elevator_mut(elev_eid) {
car.line = line_eid;
}
}
line_info.set_entity(line_eid);
}
}
}
#[allow(clippy::too_many_arguments)]
fn install_runtime_resources(
world: &mut crate::world::World,
id_remap: &HashMap<EntityId, EntityId>,
tick: u64,
extensions: &BTreeMap<String, BTreeMap<EntityId, String>>,
metric_tags: MetricTags,
arrival_log: crate::arrival_log::ArrivalLog,
arrival_log_retention: crate::arrival_log::ArrivalLogRetention,
destination_log: crate::arrival_log::DestinationLog,
traffic_detector: crate::traffic_detector::TrafficDetector,
reposition_cooldowns: crate::dispatch::reposition::RepositionCooldowns,
) {
let remapped_exts = Self::remap_extensions(extensions, id_remap);
world.insert_resource(PendingExtensions(remapped_exts));
let mut tags = metric_tags;
tags.remap_entity_ids(id_remap);
world.insert_resource(tags);
let mut log = arrival_log;
log.remap_entity_ids(id_remap);
world.insert_resource(log);
world.insert_resource(crate::arrival_log::CurrentTick(tick));
world.insert_resource(arrival_log_retention);
let mut dest_log = destination_log;
dest_log.remap_entity_ids(id_remap);
world.insert_resource(dest_log);
world.insert_resource(traffic_detector);
let mut cooldowns = reposition_cooldowns;
cooldowns.remap_entity_ids(id_remap);
world.insert_resource(cooldowns);
}
fn replay_dispatcher_tuning(
sim: &mut crate::sim::Simulation,
dispatch_config: &std::collections::BTreeMap<GroupId, String>,
) {
for (gid, serialized) in dispatch_config {
if let Some(dispatcher) = sim.dispatchers_mut().get_mut(gid)
&& let Err(err) = dispatcher.restore_config(serialized)
{
sim.push_event(crate::events::Event::DispatchConfigNotRestored {
group: *gid,
reason: err,
});
}
}
}
fn restore_reposition_strategies(sim: &mut crate::sim::Simulation, groups: &[GroupSnapshot]) {
for gs in groups {
let Some(ref repo_id) = gs.reposition else {
continue;
};
if let Some(strategy) = repo_id.instantiate() {
sim.set_reposition(gs.id, strategy, repo_id.clone());
} else {
sim.push_event(crate::events::Event::RepositionStrategyNotRestored {
group: gs.id,
});
}
}
}
fn spawn_entities(
world: &mut crate::world::World,
entities: &[EntitySnapshot],
) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
for snap in entities {
let new_id = world.spawn();
index_to_id.push(new_id);
id_remap.insert(snap.original_id, new_id);
}
(index_to_id, id_remap)
}
fn attach_components(
world: &mut crate::world::World,
entities: &[EntitySnapshot],
index_to_id: &[EntityId],
id_remap: &HashMap<EntityId, EntityId>,
) {
let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
for (i, snap) in entities.iter().enumerate() {
let eid = index_to_id[i];
if let Some(pos) = snap.position {
world.set_position(eid, pos);
}
if let Some(vel) = snap.velocity {
world.set_velocity(eid, vel);
}
if let Some(ref elev) = snap.elevator {
let mut e = elev.clone();
e.riders = e.riders.iter().map(|&r| remap(r)).collect();
e.target_stop = remap_opt(e.target_stop);
e.line = remap(e.line);
e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
e.phase = match e.phase {
crate::components::ElevatorPhase::MovingToStop(s) => {
crate::components::ElevatorPhase::MovingToStop(remap(s))
}
crate::components::ElevatorPhase::Repositioning(s) => {
crate::components::ElevatorPhase::Repositioning(remap(s))
}
other => other,
};
world.set_elevator(eid, e);
}
if let Some(ref stop) = snap.stop {
world.set_stop(eid, stop.clone());
}
if let Some(ref rider) = snap.rider {
use crate::components::RiderPhase;
let mut r = rider.clone();
r.current_stop = remap_opt(r.current_stop);
r.phase = match r.phase {
RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
other => other,
};
world.set_rider(eid, r);
}
if let Some(ref route) = snap.route {
let mut rt = route.clone();
for leg in &mut rt.legs {
leg.from = remap(leg.from);
leg.to = remap(leg.to);
if let crate::components::TransportMode::Line(ref mut l) = leg.via {
*l = remap(*l);
}
}
world.set_route(eid, rt);
}
if let Some(ref line) = snap.line {
world.set_line(eid, line.clone());
}
if let Some(patience) = snap.patience {
world.set_patience(eid, patience);
}
if let Some(prefs) = snap.preferences {
world.set_preferences(eid, prefs);
}
if let Some(ref ac) = snap.access_control {
let remapped =
AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
world.set_access_control(eid, remapped);
}
if snap.disabled {
world.disable(eid);
}
#[cfg(feature = "energy")]
if let Some(ref profile) = snap.energy_profile {
world.set_energy_profile(eid, profile.clone());
}
#[cfg(feature = "energy")]
if let Some(ref em) = snap.energy_metrics {
world.set_energy_metrics(eid, em.clone());
}
if let Some(mode) = snap.service_mode {
world.set_service_mode(eid, mode);
}
if let Some(ref dq) = snap.destination_queue {
use crate::components::DestinationQueue as DQ;
let mut new_dq = DQ::new();
for &e in dq.queue() {
new_dq.push_back(remap(e));
}
world.set_destination_queue(eid, new_dq);
}
Self::attach_car_calls(world, eid, &snap.car_calls, id_remap);
}
}
fn attach_car_calls(
world: &mut crate::world::World,
car: EntityId,
car_calls: &[CarCall],
id_remap: &HashMap<EntityId, EntityId>,
) {
if car_calls.is_empty() {
return;
}
let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
let Some(slot) = world.car_calls_mut(car) else {
return;
};
for cc in car_calls {
let mut c = cc.clone();
c.car = car;
c.floor = remap(c.floor);
c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
slot.push(c);
}
}
fn attach_hall_calls(
&self,
world: &mut crate::world::World,
id_remap: &HashMap<EntityId, EntityId>,
) {
let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
for hc in &self.hall_calls {
let mut c = hc.clone();
c.stop = remap(c.stop);
c.destination = remap_opt(c.destination);
c.assigned_cars_by_line = c
.assigned_cars_by_line
.iter()
.map(|(&line, &car)| (remap(line), remap(car)))
.collect();
c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
world.set_hall_call(c);
}
}
#[allow(clippy::type_complexity)]
fn rebuild_groups_and_dispatchers(
&self,
index_to_id: &[EntityId],
custom_strategy_factory: CustomStrategyFactory<'_>,
) -> Result<
(
Vec<crate::dispatch::ElevatorGroup>,
HashMap<StopId, EntityId>,
std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
),
crate::error::SimError,
> {
use crate::dispatch::ElevatorGroup;
let groups: Vec<ElevatorGroup> = self
.groups
.iter()
.map(|gs| {
let elevator_entities: Vec<EntityId> = gs
.elevator_indices
.iter()
.filter_map(|&i| index_to_id.get(i).copied())
.collect();
let stop_entities: Vec<EntityId> = gs
.stop_indices
.iter()
.filter_map(|&i| index_to_id.get(i).copied())
.collect();
let lines = if gs.lines.is_empty() {
vec![crate::dispatch::LineInfo::new(
EntityId::default(),
elevator_entities,
stop_entities,
)]
} else {
gs.lines
.iter()
.filter_map(|lsi| {
let entity = index_to_id.get(lsi.entity_index).copied()?;
Some(crate::dispatch::LineInfo::new(
entity,
lsi.elevator_indices
.iter()
.filter_map(|&i| index_to_id.get(i).copied())
.collect(),
lsi.stop_indices
.iter()
.filter_map(|&i| index_to_id.get(i).copied())
.collect(),
))
})
.collect()
};
ElevatorGroup::new(gs.id, gs.name.clone(), lines)
.with_hall_call_mode(gs.hall_call_mode)
.with_ack_latency_ticks(gs.ack_latency_ticks)
})
.collect();
let stop_lookup: HashMap<StopId, EntityId> = self
.stop_lookup
.iter()
.filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
.collect();
let mut dispatchers = std::collections::BTreeMap::new();
let mut strategy_ids = std::collections::BTreeMap::new();
for (gs, group) in self.groups.iter().zip(groups.iter()) {
let strategy: Box<dyn crate::dispatch::DispatchStrategy> =
if let Some(builtin) = gs.strategy.instantiate() {
builtin
} else if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
custom_strategy_factory
.and_then(|f| f(name))
.ok_or_else(|| crate::error::SimError::UnresolvedCustomStrategy {
name: name.clone(),
group: group.id(),
})?
} else {
Box::new(crate::dispatch::scan::ScanDispatch::new())
};
dispatchers.insert(group.id(), strategy);
strategy_ids.insert(group.id(), gs.strategy.clone());
}
Ok((groups, stop_lookup, dispatchers, strategy_ids))
}
fn remap_extensions(
extensions: &BTreeMap<String, BTreeMap<EntityId, String>>,
id_remap: &HashMap<EntityId, EntityId>,
) -> BTreeMap<String, BTreeMap<EntityId, String>> {
extensions
.iter()
.map(|(name, entries)| {
let remapped: BTreeMap<EntityId, String> = entries
.iter()
.map(|(old_id, data)| {
let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
(new_id, data.clone())
})
.collect();
(name.clone(), remapped)
})
.collect()
}
fn emit_dangling_warnings(
entities: &[EntitySnapshot],
hall_calls: &[HallCall],
id_remap: &HashMap<EntityId, EntityId>,
tick: u64,
sim: &mut crate::sim::Simulation,
) {
let mut seen = HashSet::new();
let mut check = |old: EntityId| {
if !id_remap.contains_key(&old) && seen.insert(old) {
sim.push_event(crate::events::Event::SnapshotDanglingReference {
stale_id: old,
tick,
});
}
};
for snap in entities {
Self::collect_referenced_ids(snap, &mut check);
}
for hc in hall_calls {
check(hc.stop);
for (&line, &car) in &hc.assigned_cars_by_line {
check(line);
check(car);
}
if let Some(dest) = hc.destination {
check(dest);
}
for &rider in &hc.pending_riders {
check(rider);
}
}
}
fn collect_referenced_ids(snap: &EntitySnapshot, mut visit: impl FnMut(EntityId)) {
if let Some(ref elev) = snap.elevator {
for &r in &elev.riders {
visit(r);
}
if let Some(t) = elev.target_stop {
visit(t);
}
visit(elev.line);
match elev.phase {
crate::components::ElevatorPhase::MovingToStop(s)
| crate::components::ElevatorPhase::Repositioning(s) => visit(s),
_ => {}
}
for &s in &elev.restricted_stops {
visit(s);
}
}
if let Some(ref rider) = snap.rider {
if let Some(s) = rider.current_stop {
visit(s);
}
match rider.phase {
crate::components::RiderPhase::Boarding(e)
| crate::components::RiderPhase::Riding(e)
| crate::components::RiderPhase::Exiting(e) => visit(e),
_ => {}
}
}
if let Some(ref route) = snap.route {
for leg in &route.legs {
visit(leg.from);
visit(leg.to);
if let crate::components::TransportMode::Line(l) = leg.via {
visit(l);
}
}
}
if let Some(ref ac) = snap.access_control {
for &s in ac.allowed_stops() {
visit(s);
}
}
if let Some(ref dq) = snap.destination_queue {
for &e in dq.queue() {
visit(e);
}
}
for cc in &snap.car_calls {
visit(cc.floor);
for &r in &cc.pending_riders {
visit(r);
}
}
}
}
const SNAPSHOT_MAGIC: [u8; 8] = *b"ELEVSNAP";
const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Serialize, Deserialize)]
struct SnapshotEnvelope {
magic: [u8; 8],
version: String,
payload: WorldSnapshot,
}
impl crate::sim::Simulation {
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn snapshot(&self) -> WorldSnapshot {
self.snapshot_inner()
}
pub fn try_snapshot(&self) -> Result<WorldSnapshot, crate::error::SimError> {
if self.tick_in_progress() {
return Err(crate::error::SimError::MidTickSnapshot);
}
Ok(self.snapshot())
}
#[allow(clippy::too_many_lines)]
fn snapshot_inner(&self) -> WorldSnapshot {
let world = self.world();
let all_ids: Vec<EntityId> = world.alive.keys().collect();
let id_to_index: HashMap<EntityId, usize> = all_ids
.iter()
.copied()
.enumerate()
.map(|(i, e)| (e, i))
.collect();
let entities: Vec<EntitySnapshot> = all_ids
.iter()
.map(|&eid| EntitySnapshot {
original_id: eid,
position: world.position(eid).copied(),
velocity: world.velocity(eid).copied(),
elevator: world.elevator(eid).cloned(),
stop: world.stop(eid).cloned(),
rider: world.rider(eid).cloned(),
route: world.route(eid).cloned(),
line: world.line(eid).cloned(),
patience: world.patience(eid).copied(),
preferences: world.preferences(eid).copied(),
access_control: world.access_control(eid).cloned(),
disabled: world.is_disabled(eid),
#[cfg(feature = "energy")]
energy_profile: world.energy_profile(eid).cloned(),
#[cfg(feature = "energy")]
energy_metrics: world.energy_metrics(eid).cloned(),
service_mode: world.service_mode(eid).copied(),
destination_queue: world.destination_queue(eid).cloned(),
car_calls: world.car_calls(eid).to_vec(),
})
.collect();
let groups: Vec<GroupSnapshot> = self
.groups()
.iter()
.map(|g| {
let lines: Vec<LineSnapshotInfo> = g
.lines()
.iter()
.filter_map(|li| {
let entity_index = id_to_index.get(&li.entity()).copied()?;
Some(LineSnapshotInfo {
entity_index,
elevator_indices: li
.elevators()
.iter()
.filter_map(|eid| id_to_index.get(eid).copied())
.collect(),
stop_indices: li
.serves()
.iter()
.filter_map(|eid| id_to_index.get(eid).copied())
.collect(),
})
})
.collect();
GroupSnapshot {
id: g.id(),
name: g.name().to_owned(),
elevator_indices: g
.elevator_entities()
.iter()
.filter_map(|eid| id_to_index.get(eid).copied())
.collect(),
stop_indices: g
.stop_entities()
.iter()
.filter_map(|eid| id_to_index.get(eid).copied())
.collect(),
strategy: self
.strategy_id(g.id())
.cloned()
.unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
lines,
reposition: self.reposition_id(g.id()).cloned(),
hall_call_mode: g.hall_call_mode(),
ack_latency_ticks: g.ack_latency_ticks(),
}
})
.collect();
let stop_lookup: BTreeMap<StopId, usize> = self
.stop_lookup_iter()
.filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
.collect();
WorldSnapshot {
version: SNAPSHOT_SCHEMA_VERSION,
tick: self.current_tick(),
dt: self.dt(),
entities,
groups,
stop_lookup,
metrics: self.metrics().clone(),
metric_tags: self
.world()
.resource::<MetricTags>()
.cloned()
.unwrap_or_default(),
extensions: self.world().serialize_extensions(),
ticks_per_second: 1.0 / self.dt(),
hall_calls: world.iter_hall_calls().cloned().collect(),
arrival_log: world
.resource::<crate::arrival_log::ArrivalLog>()
.cloned()
.unwrap_or_default(),
arrival_log_retention: world
.resource::<crate::arrival_log::ArrivalLogRetention>()
.copied()
.unwrap_or_default(),
destination_log: world
.resource::<crate::arrival_log::DestinationLog>()
.cloned()
.unwrap_or_default(),
traffic_detector: world
.resource::<crate::traffic_detector::TrafficDetector>()
.cloned()
.unwrap_or_default(),
reposition_cooldowns: world
.resource::<crate::dispatch::reposition::RepositionCooldowns>()
.cloned()
.unwrap_or_default(),
dispatch_config: self
.dispatchers()
.iter()
.filter_map(|(gid, d)| d.snapshot_config().map(|s| (*gid, s)))
.collect(),
}
}
pub fn snapshot_bytes(&self) -> Result<Vec<u8>, crate::error::SimError> {
let envelope = SnapshotEnvelope {
magic: SNAPSHOT_MAGIC,
version: env!("CARGO_PKG_VERSION").to_owned(),
payload: self.try_snapshot()?,
};
postcard::to_allocvec(&envelope)
.map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))
}
pub fn snapshot_checksum(&self) -> Result<u64, crate::error::SimError> {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let snapshot = self.try_snapshot()?;
let bytes = postcard::to_allocvec(&snapshot)
.map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
let mut h: u64 = FNV_OFFSET;
for byte in &bytes {
h ^= u64::from(*byte);
h = h.wrapping_mul(FNV_PRIME);
}
Ok(h)
}
pub fn restore_bytes(
bytes: &[u8],
options: RestoreOptions<'_>,
) -> Result<Self, crate::error::SimError> {
let (envelope, tail): (SnapshotEnvelope, &[u8]) = postcard::take_from_bytes(bytes)
.map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
if !tail.is_empty() {
return Err(crate::error::SimError::SnapshotFormat(format!(
"trailing bytes: {} unread of {}",
tail.len(),
bytes.len()
)));
}
if envelope.magic != SNAPSHOT_MAGIC {
return Err(crate::error::SimError::SnapshotFormat(
"magic bytes do not match".to_string(),
));
}
let current = env!("CARGO_PKG_VERSION");
if envelope.version != current {
return Err(crate::error::SimError::SnapshotVersion {
saved: envelope.version,
current: current.to_owned(),
});
}
envelope.payload.restore(options)
}
}