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
477/// Magic bytes identifying a bincode snapshot blob.
478const SNAPSHOT_MAGIC: [u8; 8] = *b"ELEVSNAP";
479
480/// Byte-level snapshot envelope: magic + crate version + payload.
481///
482/// Serialized via bincode. The magic and version fields are checked on
483/// restore to reject blobs from other tools or from a different
484/// `elevator-core` version.
485#[derive(Debug, Serialize, Deserialize)]
486struct SnapshotEnvelope {
487    /// Magic bytes; must equal [`SNAPSHOT_MAGIC`] or the blob is rejected.
488    magic: [u8; 8],
489    /// `elevator-core` crate version that produced the blob.
490    version: String,
491    /// The captured simulation state.
492    payload: WorldSnapshot,
493}
494
495impl crate::sim::Simulation {
496    /// Create a serializable snapshot of the current simulation state.
497    ///
498    /// The snapshot captures all entities, components, groups, metrics,
499    /// and the tick counter. Extension components and custom resources
500    /// are NOT included — games must serialize those separately.
501    #[must_use]
502    pub fn snapshot(&self) -> WorldSnapshot {
503        let world = self.world();
504
505        // Build entity index: map EntityId → position in vec.
506        let all_ids: Vec<EntityId> = world.alive.keys().collect();
507        let id_to_index: HashMap<EntityId, usize> = all_ids
508            .iter()
509            .copied()
510            .enumerate()
511            .map(|(i, e)| (e, i))
512            .collect();
513
514        // Snapshot each entity.
515        let entities: Vec<EntitySnapshot> = all_ids
516            .iter()
517            .map(|&eid| EntitySnapshot {
518                original_id: eid,
519                position: world.position(eid).copied(),
520                velocity: world.velocity(eid).copied(),
521                elevator: world.elevator(eid).cloned(),
522                stop: world.stop(eid).cloned(),
523                rider: world.rider(eid).cloned(),
524                route: world.route(eid).cloned(),
525                line: world.line(eid).cloned(),
526                patience: world.patience(eid).copied(),
527                preferences: world.preferences(eid).copied(),
528                access_control: world.access_control(eid).cloned(),
529                disabled: world.is_disabled(eid),
530                #[cfg(feature = "energy")]
531                energy_profile: world.energy_profile(eid).cloned(),
532                #[cfg(feature = "energy")]
533                energy_metrics: world.energy_metrics(eid).cloned(),
534                service_mode: world.service_mode(eid).copied(),
535                destination_queue: world.destination_queue(eid).cloned(),
536            })
537            .collect();
538
539        // Snapshot groups (convert EntityIds to indices).
540        let groups: Vec<GroupSnapshot> = self
541            .groups()
542            .iter()
543            .map(|g| {
544                let lines: Vec<LineSnapshotInfo> = g
545                    .lines()
546                    .iter()
547                    .filter_map(|li| {
548                        let entity_index = id_to_index.get(&li.entity()).copied()?;
549                        Some(LineSnapshotInfo {
550                            entity_index,
551                            elevator_indices: li
552                                .elevators()
553                                .iter()
554                                .filter_map(|eid| id_to_index.get(eid).copied())
555                                .collect(),
556                            stop_indices: li
557                                .serves()
558                                .iter()
559                                .filter_map(|eid| id_to_index.get(eid).copied())
560                                .collect(),
561                        })
562                    })
563                    .collect();
564                GroupSnapshot {
565                    id: g.id(),
566                    name: g.name().to_owned(),
567                    elevator_indices: g
568                        .elevator_entities()
569                        .iter()
570                        .filter_map(|eid| id_to_index.get(eid).copied())
571                        .collect(),
572                    stop_indices: g
573                        .stop_entities()
574                        .iter()
575                        .filter_map(|eid| id_to_index.get(eid).copied())
576                        .collect(),
577                    strategy: self
578                        .strategy_id(g.id())
579                        .cloned()
580                        .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
581                    lines,
582                    reposition: self.reposition_id(g.id()).cloned(),
583                }
584            })
585            .collect();
586
587        // Snapshot stop lookup (convert EntityIds to indices).
588        let stop_lookup: HashMap<StopId, usize> = self
589            .stop_lookup_iter()
590            .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
591            .collect();
592
593        WorldSnapshot {
594            tick: self.current_tick(),
595            dt: self.dt(),
596            entities,
597            groups,
598            stop_lookup,
599            metrics: self.metrics().clone(),
600            metric_tags: self
601                .world()
602                .resource::<MetricTags>()
603                .cloned()
604                .unwrap_or_default(),
605            extensions: self.world().serialize_extensions(),
606            ticks_per_second: 1.0 / self.dt(),
607        }
608    }
609
610    /// Serialize the current state to a self-describing byte blob.
611    ///
612    /// The blob is postcard-encoded and carries a magic prefix plus the
613    /// `elevator-core` crate version. Use [`Simulation::restore_bytes`]
614    /// on the receiving end. Determinism is bit-exact across builds of
615    /// the same crate version; cross-version restores return
616    /// [`SimError::SnapshotVersion`](crate::error::SimError::SnapshotVersion).
617    ///
618    /// Extension component *data* is serialized (identical to
619    /// [`Simulation::snapshot`]); after restore you must still call
620    /// `world.register_ext::<T>(name)` for each extension type and then
621    /// [`Simulation::load_extensions`] to materialize them. Custom
622    /// dispatch strategies and arbitrary `World` resources are not
623    /// included.
624    ///
625    /// # Errors
626    /// Returns [`SimError::SnapshotFormat`](crate::error::SimError::SnapshotFormat)
627    /// if postcard encoding fails. This is unreachable for well-formed
628    /// `WorldSnapshot` values (all fields derive `Serialize`), so callers
629    /// that don't care can `unwrap`.
630    pub fn snapshot_bytes(&self) -> Result<Vec<u8>, crate::error::SimError> {
631        let envelope = SnapshotEnvelope {
632            magic: SNAPSHOT_MAGIC,
633            version: env!("CARGO_PKG_VERSION").to_owned(),
634            payload: self.snapshot(),
635        };
636        postcard::to_allocvec(&envelope)
637            .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))
638    }
639
640    /// Restore a simulation from bytes produced by [`Simulation::snapshot_bytes`].
641    ///
642    /// Built-in dispatch strategies are auto-restored. For groups using
643    /// [`BuiltinStrategy::Custom`](crate::dispatch::BuiltinStrategy::Custom),
644    /// provide a factory; pass `None` otherwise.
645    ///
646    /// # Errors
647    /// - [`SimError::SnapshotFormat`](crate::error::SimError::SnapshotFormat)
648    ///   if the bytes are not a valid envelope or the magic prefix does
649    ///   not match.
650    /// - [`SimError::SnapshotVersion`](crate::error::SimError::SnapshotVersion)
651    ///   if the blob was produced by a different crate version.
652    pub fn restore_bytes(
653        bytes: &[u8],
654        custom_strategy_factory: CustomStrategyFactory<'_>,
655    ) -> Result<Self, crate::error::SimError> {
656        let (envelope, tail): (SnapshotEnvelope, &[u8]) = postcard::take_from_bytes(bytes)
657            .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
658        if !tail.is_empty() {
659            return Err(crate::error::SimError::SnapshotFormat(format!(
660                "trailing bytes: {} unread of {}",
661                tail.len(),
662                bytes.len()
663            )));
664        }
665        if envelope.magic != SNAPSHOT_MAGIC {
666            return Err(crate::error::SimError::SnapshotFormat(
667                "magic bytes do not match".to_string(),
668            ));
669        }
670        let current = env!("CARGO_PKG_VERSION");
671        if envelope.version != current {
672            return Err(crate::error::SimError::SnapshotVersion {
673                saved: envelope.version,
674                current: current.to_owned(),
675            });
676        }
677        Ok(envelope.payload.restore(custom_strategy_factory))
678    }
679}