Skip to main content

elevator_core/
snapshot.rs

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