Skip to main content

elevator_core/
snapshot.rs

1//! World snapshot for save/load functionality.
2//!
3//! Provides [`WorldSnapshot`] which captures the full simulation state
4//! (all entities, components, groups, metrics, tick counter) in a
5//! serializable form. Games choose the serialization format via serde.
6//!
7//! Extension components are NOT included — games must serialize their
8//! own extensions separately and re-attach them after restoring.
9
10use crate::components::{
11    AccessControl, Elevator, Line, Patience, Position, Preferences, Rider, Route, Stop, Velocity,
12};
13use crate::entity::EntityId;
14use crate::ids::GroupId;
15use crate::metrics::Metrics;
16use crate::stop::StopId;
17use crate::tagged_metrics::MetricTags;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Serializable snapshot of a single entity's components.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct EntitySnapshot {
24    /// The original `EntityId` (used for remapping cross-references on restore).
25    pub original_id: EntityId,
26    /// Position component (if present).
27    pub position: Option<Position>,
28    /// Velocity component (if present).
29    pub velocity: Option<Velocity>,
30    /// Elevator component (if present).
31    pub elevator: Option<Elevator>,
32    /// Stop component (if present).
33    pub stop: Option<Stop>,
34    /// Rider component (if present).
35    pub rider: Option<Rider>,
36    /// Route component (if present).
37    pub route: Option<Route>,
38    /// Line component (if present).
39    #[serde(default)]
40    pub line: Option<Line>,
41    /// Patience component (if present).
42    pub patience: Option<Patience>,
43    /// Preferences component (if present).
44    pub preferences: Option<Preferences>,
45    /// Access control component (if present).
46    #[serde(default)]
47    pub access_control: Option<AccessControl>,
48    /// Whether this entity is disabled.
49    pub disabled: bool,
50    /// Energy profile (if present, requires `energy` feature).
51    #[cfg(feature = "energy")]
52    #[serde(default)]
53    pub energy_profile: Option<crate::energy::EnergyProfile>,
54    /// Energy metrics (if present, requires `energy` feature).
55    #[cfg(feature = "energy")]
56    #[serde(default)]
57    pub energy_metrics: Option<crate::energy::EnergyMetrics>,
58    /// Service mode (if present).
59    #[serde(default)]
60    pub service_mode: Option<crate::components::ServiceMode>,
61}
62
63/// Serializable snapshot of the entire simulation state.
64///
65/// Capture via [`Simulation::snapshot()`] and restore via
66/// [`WorldSnapshot::restore()`]. The game chooses the serde format
67/// (RON, JSON, bincode, etc.).
68///
69/// Extension components and resources are NOT included. Games must
70/// handle their own custom data separately.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct WorldSnapshot {
73    /// Current simulation tick.
74    pub tick: u64,
75    /// Time delta per tick.
76    pub dt: f64,
77    /// All entities indexed by position in this vec.
78    /// `EntityId`s are regenerated on restore.
79    pub entities: Vec<EntitySnapshot>,
80    /// Elevator groups (references into entities by index).
81    pub groups: Vec<GroupSnapshot>,
82    /// Stop ID → entity index mapping.
83    pub stop_lookup: HashMap<StopId, usize>,
84    /// Global metrics at snapshot time.
85    pub metrics: Metrics,
86    /// Per-tag metric accumulators and entity-tag associations.
87    pub metric_tags: MetricTags,
88    /// Serialized extension component data: name → (`EntityId` → RON string).
89    pub extensions: HashMap<String, HashMap<EntityId, String>>,
90    /// Ticks per second (for `TimeAdapter` reconstruction).
91    pub ticks_per_second: f64,
92}
93
94/// Per-line snapshot info within a group.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct LineSnapshotInfo {
97    /// Index into the `entities` vec for the line entity.
98    pub entity_index: usize,
99    /// Indices into the `entities` vec for elevators on this line.
100    pub elevator_indices: Vec<usize>,
101    /// Indices into the `entities` vec for stops served by this line.
102    pub stop_indices: Vec<usize>,
103}
104
105/// Serializable representation of an elevator group.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct GroupSnapshot {
108    /// Group identifier.
109    pub id: GroupId,
110    /// Group name.
111    pub name: String,
112    /// Indices into the `entities` vec for elevators in this group.
113    pub elevator_indices: Vec<usize>,
114    /// Indices into the `entities` vec for stops in this group.
115    pub stop_indices: Vec<usize>,
116    /// The dispatch strategy used by this group.
117    pub strategy: crate::dispatch::BuiltinStrategy,
118    /// Per-line snapshot data. Empty in legacy snapshots.
119    #[serde(default)]
120    pub lines: Vec<LineSnapshotInfo>,
121    /// Optional repositioning strategy for idle elevators.
122    #[serde(default)]
123    pub reposition: Option<crate::dispatch::BuiltinReposition>,
124}
125
126/// Pending extension data from a snapshot, awaiting type registration.
127///
128/// Stored as a world resource after `restore()`. Call
129/// `sim.load_extensions()` after registering extension types to
130/// deserialize the data.
131pub(crate) struct PendingExtensions(pub(crate) HashMap<String, HashMap<EntityId, String>>);
132
133/// Factory function type for instantiating custom dispatch strategies by name.
134type CustomStrategyFactory<'a> =
135    Option<&'a dyn Fn(&str) -> Option<Box<dyn crate::dispatch::DispatchStrategy>>>;
136
137impl WorldSnapshot {
138    /// Restore a simulation from this snapshot.
139    ///
140    /// Built-in strategies (Scan, Look, `NearestCar`, ETD) are auto-restored.
141    /// For `Custom` strategies, provide a factory function that maps strategy
142    /// names to instances. Pass `None` if only using built-in strategies.
143    ///
144    /// To restore extension components, call `world.register_ext::<T>(name)`
145    /// on the returned simulation's world for each extension type, then call
146    /// [`Simulation::load_extensions()`] with this snapshot's `extensions` data.
147    #[must_use]
148    pub fn restore(
149        self,
150        custom_strategy_factory: CustomStrategyFactory<'_>,
151    ) -> crate::sim::Simulation {
152        use crate::world::{SortedStops, World};
153
154        let mut world = World::new();
155
156        // Phase 1: spawn all entities and build old→new EntityId mapping.
157        let (index_to_id, id_remap) = Self::spawn_entities(&mut world, &self.entities);
158
159        // Phase 2: attach components with remapped EntityIds.
160        Self::attach_components(&mut world, &self.entities, &index_to_id, &id_remap);
161
162        // Rebuild sorted stops index.
163        let mut sorted: Vec<(f64, EntityId)> = world
164            .iter_stops()
165            .map(|(eid, stop)| (stop.position, eid))
166            .collect();
167        sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
168        world.insert_resource(SortedStops(sorted));
169
170        // Rebuild groups, stop lookup, dispatchers, and extensions (borrows self).
171        let (mut groups, stop_lookup, dispatchers, strategy_ids) =
172            self.rebuild_groups_and_dispatchers(&index_to_id, custom_strategy_factory);
173
174        // Fix legacy snapshots: synthetic LineInfo entries with EntityId::default()
175        // need real line entities spawned in the world.
176        for group in &mut groups {
177            let group_id = group.id();
178            let lines = group.lines_mut();
179            for line_info in lines.iter_mut() {
180                if line_info.entity() != EntityId::default() {
181                    continue;
182                }
183                // Compute min/max position from the line's served stops.
184                let (min_pos, max_pos) = line_info
185                    .serves()
186                    .iter()
187                    .filter_map(|&sid| world.stop(sid).map(|s| s.position))
188                    .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
189                        (lo.min(p), hi.max(p))
190                    });
191                let line_eid = world.spawn();
192                world.set_line(
193                    line_eid,
194                    Line {
195                        name: format!("Legacy-{group_id}"),
196                        group: group_id,
197                        orientation: crate::components::Orientation::Vertical,
198                        position: None,
199                        min_position: if min_pos.is_finite() { min_pos } else { 0.0 },
200                        max_position: if max_pos.is_finite() { max_pos } else { 0.0 },
201                        max_cars: None,
202                    },
203                );
204                // Update all elevators on this line to reference the new entity.
205                for &elev_eid in line_info.elevators() {
206                    if let Some(car) = world.elevator_mut(elev_eid) {
207                        car.line = line_eid;
208                    }
209                }
210                line_info.set_entity(line_eid);
211            }
212        }
213
214        // Remap EntityIds in extension data for later deserialization.
215        let remapped_exts = Self::remap_extensions(&self.extensions, &id_remap);
216        world.insert_resource(PendingExtensions(remapped_exts));
217
218        // Restore MetricTags with remapped entity IDs (moves out of self).
219        let mut tags = self.metric_tags;
220        tags.remap_entity_ids(&id_remap);
221        world.insert_resource(tags);
222
223        let mut sim = crate::sim::Simulation::from_parts(
224            world,
225            self.tick,
226            self.dt,
227            groups,
228            stop_lookup,
229            dispatchers,
230            strategy_ids,
231            self.metrics,
232            self.ticks_per_second,
233        );
234
235        // Restore reposition strategies from group snapshots.
236        for gs in &self.groups {
237            if let Some(ref repo_id) = gs.reposition {
238                if let Some(strategy) = repo_id.instantiate() {
239                    sim.set_reposition(gs.id, strategy, repo_id.clone());
240                }
241            }
242        }
243
244        sim
245    }
246
247    /// Spawn entities in the world and build the old→new `EntityId` mapping.
248    fn spawn_entities(
249        world: &mut crate::world::World,
250        entities: &[EntitySnapshot],
251    ) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
252        let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
253        let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
254        for snap in entities {
255            let new_id = world.spawn();
256            index_to_id.push(new_id);
257            id_remap.insert(snap.original_id, new_id);
258        }
259        (index_to_id, id_remap)
260    }
261
262    /// Attach components to spawned entities, remapping cross-references.
263    fn attach_components(
264        world: &mut crate::world::World,
265        entities: &[EntitySnapshot],
266        index_to_id: &[EntityId],
267        id_remap: &HashMap<EntityId, EntityId>,
268    ) {
269        let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
270        let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
271
272        for (i, snap) in entities.iter().enumerate() {
273            let eid = index_to_id[i];
274
275            if let Some(pos) = snap.position {
276                world.set_position(eid, pos);
277            }
278            if let Some(vel) = snap.velocity {
279                world.set_velocity(eid, vel);
280            }
281            if let Some(ref elev) = snap.elevator {
282                let mut e = elev.clone();
283                e.riders = e.riders.iter().map(|&r| remap(r)).collect();
284                e.target_stop = remap_opt(e.target_stop);
285                e.line = remap(e.line);
286                e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
287                e.phase = match e.phase {
288                    crate::components::ElevatorPhase::MovingToStop(s) => {
289                        crate::components::ElevatorPhase::MovingToStop(remap(s))
290                    }
291                    other => other,
292                };
293                world.set_elevator(eid, e);
294            }
295            if let Some(ref stop) = snap.stop {
296                world.set_stop(eid, stop.clone());
297            }
298            if let Some(ref rider) = snap.rider {
299                use crate::components::RiderPhase;
300                let mut r = rider.clone();
301                r.current_stop = remap_opt(r.current_stop);
302                r.phase = match r.phase {
303                    RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
304                    RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
305                    RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
306                    other => other,
307                };
308                world.set_rider(eid, r);
309            }
310            if let Some(ref route) = snap.route {
311                let mut rt = route.clone();
312                for leg in &mut rt.legs {
313                    leg.from = remap(leg.from);
314                    leg.to = remap(leg.to);
315                    if let crate::components::TransportMode::Line(ref mut l) = leg.via {
316                        *l = remap(*l);
317                    }
318                }
319                world.set_route(eid, rt);
320            }
321            if let Some(ref line) = snap.line {
322                world.set_line(eid, line.clone());
323            }
324            if let Some(patience) = snap.patience {
325                world.set_patience(eid, patience);
326            }
327            if let Some(prefs) = snap.preferences {
328                world.set_preferences(eid, prefs);
329            }
330            if let Some(ref ac) = snap.access_control {
331                let remapped =
332                    AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
333                world.set_access_control(eid, remapped);
334            }
335            if snap.disabled {
336                world.disable(eid);
337            }
338            #[cfg(feature = "energy")]
339            if let Some(ref profile) = snap.energy_profile {
340                world.set_energy_profile(eid, profile.clone());
341            }
342            #[cfg(feature = "energy")]
343            if let Some(ref em) = snap.energy_metrics {
344                world.set_energy_metrics(eid, em.clone());
345            }
346            if let Some(mode) = snap.service_mode {
347                world.set_service_mode(eid, mode);
348            }
349        }
350    }
351
352    /// Rebuild groups, stop lookup, and dispatchers from snapshot data.
353    #[allow(clippy::type_complexity)]
354    fn rebuild_groups_and_dispatchers(
355        &self,
356        index_to_id: &[EntityId],
357        custom_strategy_factory: CustomStrategyFactory<'_>,
358    ) -> (
359        Vec<crate::dispatch::ElevatorGroup>,
360        HashMap<StopId, EntityId>,
361        std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
362        std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
363    ) {
364        use crate::dispatch::ElevatorGroup;
365
366        let groups: Vec<ElevatorGroup> = self
367            .groups
368            .iter()
369            .map(|gs| {
370                let elevator_entities: Vec<EntityId> = gs
371                    .elevator_indices
372                    .iter()
373                    .filter_map(|&i| index_to_id.get(i).copied())
374                    .collect();
375                let stop_entities: Vec<EntityId> = gs
376                    .stop_indices
377                    .iter()
378                    .filter_map(|&i| index_to_id.get(i).copied())
379                    .collect();
380
381                let lines = if gs.lines.is_empty() {
382                    // Legacy snapshots have no per-line data; create a single
383                    // synthetic LineInfo containing all elevators and stops.
384                    vec![crate::dispatch::LineInfo::new(
385                        EntityId::default(),
386                        elevator_entities,
387                        stop_entities,
388                    )]
389                } else {
390                    gs.lines
391                        .iter()
392                        .filter_map(|lsi| {
393                            let entity = index_to_id.get(lsi.entity_index).copied()?;
394                            Some(crate::dispatch::LineInfo::new(
395                                entity,
396                                lsi.elevator_indices
397                                    .iter()
398                                    .filter_map(|&i| index_to_id.get(i).copied())
399                                    .collect(),
400                                lsi.stop_indices
401                                    .iter()
402                                    .filter_map(|&i| index_to_id.get(i).copied())
403                                    .collect(),
404                            ))
405                        })
406                        .collect()
407                };
408
409                ElevatorGroup::new(gs.id, gs.name.clone(), lines)
410            })
411            .collect();
412
413        let stop_lookup: HashMap<StopId, EntityId> = self
414            .stop_lookup
415            .iter()
416            .filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
417            .collect();
418
419        let mut dispatchers = std::collections::BTreeMap::new();
420        let mut strategy_ids = std::collections::BTreeMap::new();
421        for (gs, group) in self.groups.iter().zip(groups.iter()) {
422            let strategy: Box<dyn crate::dispatch::DispatchStrategy> = gs
423                .strategy
424                .instantiate()
425                .or_else(|| {
426                    if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
427                        custom_strategy_factory.and_then(|f| f(name))
428                    } else {
429                        None
430                    }
431                })
432                .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
433            dispatchers.insert(group.id(), strategy);
434            strategy_ids.insert(group.id(), gs.strategy.clone());
435        }
436
437        (groups, stop_lookup, dispatchers, strategy_ids)
438    }
439
440    /// Remap `EntityId`s in extension data using the old→new mapping.
441    fn remap_extensions(
442        extensions: &HashMap<String, HashMap<EntityId, String>>,
443        id_remap: &HashMap<EntityId, EntityId>,
444    ) -> HashMap<String, HashMap<EntityId, String>> {
445        extensions
446            .iter()
447            .map(|(name, entries)| {
448                let remapped: HashMap<EntityId, String> = entries
449                    .iter()
450                    .map(|(old_id, data)| {
451                        let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
452                        (new_id, data.clone())
453                    })
454                    .collect();
455                (name.clone(), remapped)
456            })
457            .collect()
458    }
459}
460
461impl crate::sim::Simulation {
462    /// Create a serializable snapshot of the current simulation state.
463    ///
464    /// The snapshot captures all entities, components, groups, metrics,
465    /// and the tick counter. Extension components and custom resources
466    /// are NOT included — games must serialize those separately.
467    #[must_use]
468    pub fn snapshot(&self) -> WorldSnapshot {
469        let world = self.world();
470
471        // Build entity index: map EntityId → position in vec.
472        let all_ids: Vec<EntityId> = world.alive.keys().collect();
473        let mut id_to_index: HashMap<EntityId, usize> = HashMap::new();
474        for (i, &eid) in all_ids.iter().enumerate() {
475            id_to_index.insert(eid, i);
476        }
477
478        // Snapshot each entity.
479        let entities: Vec<EntitySnapshot> = all_ids
480            .iter()
481            .map(|&eid| EntitySnapshot {
482                original_id: eid,
483                position: world.position(eid).copied(),
484                velocity: world.velocity(eid).copied(),
485                elevator: world.elevator(eid).cloned(),
486                stop: world.stop(eid).cloned(),
487                rider: world.rider(eid).cloned(),
488                route: world.route(eid).cloned(),
489                line: world.line(eid).cloned(),
490                patience: world.patience(eid).copied(),
491                preferences: world.preferences(eid).copied(),
492                access_control: world.access_control(eid).cloned(),
493                disabled: world.is_disabled(eid),
494                #[cfg(feature = "energy")]
495                energy_profile: world.energy_profile(eid).cloned(),
496                #[cfg(feature = "energy")]
497                energy_metrics: world.energy_metrics(eid).cloned(),
498                service_mode: world.service_mode(eid).copied(),
499            })
500            .collect();
501
502        // Snapshot groups (convert EntityIds to indices).
503        let groups: Vec<GroupSnapshot> = self
504            .groups()
505            .iter()
506            .map(|g| {
507                let lines: Vec<LineSnapshotInfo> = g
508                    .lines()
509                    .iter()
510                    .filter_map(|li| {
511                        let entity_index = id_to_index.get(&li.entity()).copied()?;
512                        Some(LineSnapshotInfo {
513                            entity_index,
514                            elevator_indices: li
515                                .elevators()
516                                .iter()
517                                .filter_map(|eid| id_to_index.get(eid).copied())
518                                .collect(),
519                            stop_indices: li
520                                .serves()
521                                .iter()
522                                .filter_map(|eid| id_to_index.get(eid).copied())
523                                .collect(),
524                        })
525                    })
526                    .collect();
527                GroupSnapshot {
528                    id: g.id(),
529                    name: g.name().to_owned(),
530                    elevator_indices: g
531                        .elevator_entities()
532                        .iter()
533                        .filter_map(|eid| id_to_index.get(eid).copied())
534                        .collect(),
535                    stop_indices: g
536                        .stop_entities()
537                        .iter()
538                        .filter_map(|eid| id_to_index.get(eid).copied())
539                        .collect(),
540                    strategy: self
541                        .strategy_id(g.id())
542                        .cloned()
543                        .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
544                    lines,
545                    reposition: self.reposition_id(g.id()).cloned(),
546                }
547            })
548            .collect();
549
550        // Snapshot stop lookup (convert EntityIds to indices).
551        let stop_lookup: HashMap<StopId, usize> = self
552            .stop_lookup_iter()
553            .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
554            .collect();
555
556        WorldSnapshot {
557            tick: self.current_tick(),
558            dt: self.dt(),
559            entities,
560            groups,
561            stop_lookup,
562            metrics: self.metrics().clone(),
563            metric_tags: self
564                .world()
565                .resource::<MetricTags>()
566                .cloned()
567                .unwrap_or_default(),
568            extensions: self.world().serialize_extensions(),
569            ticks_per_second: 1.0 / self.dt(),
570        }
571    }
572}