1use crate::components::{
12 AccessControl, CarCall, DestinationQueue, Elevator, HallCall, Line, Patience, Position,
13 Preferences, Rider, Route, Stop, Velocity,
14};
15use crate::entity::EntityId;
16use crate::ids::GroupId;
17use crate::metrics::Metrics;
18use crate::stop::StopId;
19use crate::tagged_metrics::MetricTags;
20use serde::{Deserialize, Serialize};
21use std::collections::{HashMap, HashSet};
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct EntitySnapshot {
26 pub original_id: EntityId,
28 pub position: Option<Position>,
30 pub velocity: Option<Velocity>,
32 pub elevator: Option<Elevator>,
34 pub stop: Option<Stop>,
36 pub rider: Option<Rider>,
38 pub route: Option<Route>,
40 #[serde(default)]
42 pub line: Option<Line>,
43 pub patience: Option<Patience>,
45 pub preferences: Option<Preferences>,
47 #[serde(default)]
49 pub access_control: Option<AccessControl>,
50 pub disabled: bool,
52 #[cfg(feature = "energy")]
54 #[serde(default)]
55 pub energy_profile: Option<crate::energy::EnergyProfile>,
56 #[cfg(feature = "energy")]
58 #[serde(default)]
59 pub energy_metrics: Option<crate::energy::EnergyMetrics>,
60 #[serde(default)]
62 pub service_mode: Option<crate::components::ServiceMode>,
63 #[serde(default)]
65 pub destination_queue: Option<DestinationQueue>,
66 #[serde(default)]
68 pub car_calls: Vec<CarCall>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct WorldSnapshot {
81 pub tick: u64,
83 pub dt: f64,
85 pub entities: Vec<EntitySnapshot>,
88 pub groups: Vec<GroupSnapshot>,
90 pub stop_lookup: HashMap<StopId, usize>,
92 pub metrics: Metrics,
94 pub metric_tags: MetricTags,
96 pub extensions: HashMap<String, HashMap<EntityId, String>>,
98 pub ticks_per_second: f64,
100 #[serde(default)]
102 pub hall_calls: Vec<HallCall>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct LineSnapshotInfo {
108 pub entity_index: usize,
110 pub elevator_indices: Vec<usize>,
112 pub stop_indices: Vec<usize>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct GroupSnapshot {
119 pub id: GroupId,
121 pub name: String,
123 pub elevator_indices: Vec<usize>,
125 pub stop_indices: Vec<usize>,
127 pub strategy: crate::dispatch::BuiltinStrategy,
129 #[serde(default)]
131 pub lines: Vec<LineSnapshotInfo>,
132 #[serde(default)]
134 pub reposition: Option<crate::dispatch::BuiltinReposition>,
135 #[serde(default)]
137 pub hall_call_mode: crate::dispatch::HallCallMode,
138 #[serde(default)]
140 pub ack_latency_ticks: u32,
141}
142
143pub(crate) struct PendingExtensions(pub(crate) HashMap<String, HashMap<EntityId, String>>);
149
150type CustomStrategyFactory<'a> =
152 Option<&'a dyn Fn(&str) -> Option<Box<dyn crate::dispatch::DispatchStrategy>>>;
153
154impl WorldSnapshot {
155 pub fn restore(
169 self,
170 custom_strategy_factory: CustomStrategyFactory<'_>,
171 ) -> Result<crate::sim::Simulation, crate::error::SimError> {
172 use crate::world::{SortedStops, World};
173
174 let mut world = World::new();
175
176 let (index_to_id, id_remap) = Self::spawn_entities(&mut world, &self.entities);
178
179 Self::attach_components(&mut world, &self.entities, &index_to_id, &id_remap);
181
182 self.attach_hall_calls(&mut world, &id_remap);
184
185 let mut sorted: Vec<(f64, EntityId)> = world
187 .iter_stops()
188 .map(|(eid, stop)| (stop.position, eid))
189 .collect();
190 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
191 world.insert_resource(SortedStops(sorted));
192
193 let (mut groups, stop_lookup, dispatchers, strategy_ids) =
195 self.rebuild_groups_and_dispatchers(&index_to_id, custom_strategy_factory)?;
196
197 for group in &mut groups {
200 let group_id = group.id();
201 let lines = group.lines_mut();
202 for line_info in lines.iter_mut() {
203 if line_info.entity() != EntityId::default() {
204 continue;
205 }
206 let (min_pos, max_pos) = line_info
208 .serves()
209 .iter()
210 .filter_map(|&sid| world.stop(sid).map(|s| s.position))
211 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
212 (lo.min(p), hi.max(p))
213 });
214 let line_eid = world.spawn();
215 world.set_line(
216 line_eid,
217 Line {
218 name: format!("Legacy-{group_id}"),
219 group: group_id,
220 orientation: crate::components::Orientation::Vertical,
221 position: None,
222 min_position: if min_pos.is_finite() { min_pos } else { 0.0 },
223 max_position: if max_pos.is_finite() { max_pos } else { 0.0 },
224 max_cars: None,
225 },
226 );
227 for &elev_eid in line_info.elevators() {
229 if let Some(car) = world.elevator_mut(elev_eid) {
230 car.line = line_eid;
231 }
232 }
233 line_info.set_entity(line_eid);
234 }
235 }
236
237 let remapped_exts = Self::remap_extensions(&self.extensions, &id_remap);
239 world.insert_resource(PendingExtensions(remapped_exts));
240
241 let mut tags = self.metric_tags;
243 tags.remap_entity_ids(&id_remap);
244 world.insert_resource(tags);
245
246 let mut sim = crate::sim::Simulation::from_parts(
247 world,
248 self.tick,
249 self.dt,
250 groups,
251 stop_lookup,
252 dispatchers,
253 strategy_ids,
254 self.metrics,
255 self.ticks_per_second,
256 );
257
258 for gs in &self.groups {
260 if let Some(ref repo_id) = gs.reposition
261 && let Some(strategy) = repo_id.instantiate()
262 {
263 sim.set_reposition(gs.id, strategy, repo_id.clone());
264 }
265 }
266
267 let snap_tick = self.tick;
270 let mut dangling_seen = HashSet::new();
271 let mut check_dangling = |old: EntityId| {
272 if !id_remap.contains_key(&old) && dangling_seen.insert(old) {
273 sim.push_event(crate::events::Event::SnapshotDanglingReference {
274 stale_id: old,
275 tick: snap_tick,
276 });
277 }
278 };
279 for snap in &self.entities {
280 Self::collect_referenced_ids(snap, &mut check_dangling);
281 }
282 for hc in &self.hall_calls {
283 check_dangling(hc.stop);
284 if let Some(car) = hc.assigned_car {
285 check_dangling(car);
286 }
287 if let Some(dest) = hc.destination {
288 check_dangling(dest);
289 }
290 for &rider in &hc.pending_riders {
291 check_dangling(rider);
292 }
293 }
294
295 Ok(sim)
296 }
297
298 fn spawn_entities(
300 world: &mut crate::world::World,
301 entities: &[EntitySnapshot],
302 ) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
303 let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
304 let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
305 for snap in entities {
306 let new_id = world.spawn();
307 index_to_id.push(new_id);
308 id_remap.insert(snap.original_id, new_id);
309 }
310 (index_to_id, id_remap)
311 }
312
313 fn attach_components(
315 world: &mut crate::world::World,
316 entities: &[EntitySnapshot],
317 index_to_id: &[EntityId],
318 id_remap: &HashMap<EntityId, EntityId>,
319 ) {
320 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
321 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
322
323 for (i, snap) in entities.iter().enumerate() {
324 let eid = index_to_id[i];
325
326 if let Some(pos) = snap.position {
327 world.set_position(eid, pos);
328 }
329 if let Some(vel) = snap.velocity {
330 world.set_velocity(eid, vel);
331 }
332 if let Some(ref elev) = snap.elevator {
333 let mut e = elev.clone();
334 e.riders = e.riders.iter().map(|&r| remap(r)).collect();
335 e.target_stop = remap_opt(e.target_stop);
336 e.line = remap(e.line);
337 e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
338 e.phase = match e.phase {
339 crate::components::ElevatorPhase::MovingToStop(s) => {
340 crate::components::ElevatorPhase::MovingToStop(remap(s))
341 }
342 crate::components::ElevatorPhase::Repositioning(s) => {
343 crate::components::ElevatorPhase::Repositioning(remap(s))
344 }
345 other => other,
346 };
347 world.set_elevator(eid, e);
348 }
349 if let Some(ref stop) = snap.stop {
350 world.set_stop(eid, stop.clone());
351 }
352 if let Some(ref rider) = snap.rider {
353 use crate::components::RiderPhase;
354 let mut r = rider.clone();
355 r.current_stop = remap_opt(r.current_stop);
356 r.phase = match r.phase {
357 RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
358 RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
359 RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
360 other => other,
361 };
362 world.set_rider(eid, r);
363 }
364 if let Some(ref route) = snap.route {
365 let mut rt = route.clone();
366 for leg in &mut rt.legs {
367 leg.from = remap(leg.from);
368 leg.to = remap(leg.to);
369 if let crate::components::TransportMode::Line(ref mut l) = leg.via {
370 *l = remap(*l);
371 }
372 }
373 world.set_route(eid, rt);
374 }
375 if let Some(ref line) = snap.line {
376 world.set_line(eid, line.clone());
377 }
378 if let Some(patience) = snap.patience {
379 world.set_patience(eid, patience);
380 }
381 if let Some(prefs) = snap.preferences {
382 world.set_preferences(eid, prefs);
383 }
384 if let Some(ref ac) = snap.access_control {
385 let remapped =
386 AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
387 world.set_access_control(eid, remapped);
388 }
389 if snap.disabled {
390 world.disable(eid);
391 }
392 #[cfg(feature = "energy")]
393 if let Some(ref profile) = snap.energy_profile {
394 world.set_energy_profile(eid, profile.clone());
395 }
396 #[cfg(feature = "energy")]
397 if let Some(ref em) = snap.energy_metrics {
398 world.set_energy_metrics(eid, em.clone());
399 }
400 if let Some(mode) = snap.service_mode {
401 world.set_service_mode(eid, mode);
402 }
403 if let Some(ref dq) = snap.destination_queue {
404 use crate::components::DestinationQueue as DQ;
405 let mut new_dq = DQ::new();
406 for &e in dq.queue() {
407 new_dq.push_back(remap(e));
408 }
409 world.set_destination_queue(eid, new_dq);
410 }
411 Self::attach_car_calls(world, eid, &snap.car_calls, id_remap);
412 }
413 }
414
415 fn attach_car_calls(
417 world: &mut crate::world::World,
418 car: EntityId,
419 car_calls: &[CarCall],
420 id_remap: &HashMap<EntityId, EntityId>,
421 ) {
422 if car_calls.is_empty() {
423 return;
424 }
425 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
426 let Some(slot) = world.car_calls_mut(car) else {
427 return;
428 };
429 for cc in car_calls {
430 let mut c = cc.clone();
431 c.car = car;
432 c.floor = remap(c.floor);
433 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
434 slot.push(c);
435 }
436 }
437
438 fn attach_hall_calls(
443 &self,
444 world: &mut crate::world::World,
445 id_remap: &HashMap<EntityId, EntityId>,
446 ) {
447 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
448 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
449 for hc in &self.hall_calls {
450 let mut c = hc.clone();
451 c.stop = remap(c.stop);
452 c.destination = remap_opt(c.destination);
453 c.assigned_car = remap_opt(c.assigned_car);
454 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
455 world.set_hall_call(c);
456 }
457 }
458
459 #[allow(clippy::type_complexity)]
461 fn rebuild_groups_and_dispatchers(
462 &self,
463 index_to_id: &[EntityId],
464 custom_strategy_factory: CustomStrategyFactory<'_>,
465 ) -> Result<
466 (
467 Vec<crate::dispatch::ElevatorGroup>,
468 HashMap<StopId, EntityId>,
469 std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
470 std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
471 ),
472 crate::error::SimError,
473 > {
474 use crate::dispatch::ElevatorGroup;
475
476 let groups: Vec<ElevatorGroup> = self
477 .groups
478 .iter()
479 .map(|gs| {
480 let elevator_entities: Vec<EntityId> = gs
481 .elevator_indices
482 .iter()
483 .filter_map(|&i| index_to_id.get(i).copied())
484 .collect();
485 let stop_entities: Vec<EntityId> = gs
486 .stop_indices
487 .iter()
488 .filter_map(|&i| index_to_id.get(i).copied())
489 .collect();
490
491 let lines = if gs.lines.is_empty() {
492 vec![crate::dispatch::LineInfo::new(
495 EntityId::default(),
496 elevator_entities,
497 stop_entities,
498 )]
499 } else {
500 gs.lines
501 .iter()
502 .filter_map(|lsi| {
503 let entity = index_to_id.get(lsi.entity_index).copied()?;
504 Some(crate::dispatch::LineInfo::new(
505 entity,
506 lsi.elevator_indices
507 .iter()
508 .filter_map(|&i| index_to_id.get(i).copied())
509 .collect(),
510 lsi.stop_indices
511 .iter()
512 .filter_map(|&i| index_to_id.get(i).copied())
513 .collect(),
514 ))
515 })
516 .collect()
517 };
518
519 ElevatorGroup::new(gs.id, gs.name.clone(), lines)
520 .with_hall_call_mode(gs.hall_call_mode)
521 .with_ack_latency_ticks(gs.ack_latency_ticks)
522 })
523 .collect();
524
525 let stop_lookup: HashMap<StopId, EntityId> = self
526 .stop_lookup
527 .iter()
528 .filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
529 .collect();
530
531 let mut dispatchers = std::collections::BTreeMap::new();
532 let mut strategy_ids = std::collections::BTreeMap::new();
533 for (gs, group) in self.groups.iter().zip(groups.iter()) {
534 let strategy: Box<dyn crate::dispatch::DispatchStrategy> =
535 if let Some(builtin) = gs.strategy.instantiate() {
536 builtin
537 } else if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
538 custom_strategy_factory
539 .and_then(|f| f(name))
540 .ok_or_else(|| crate::error::SimError::UnresolvedCustomStrategy {
541 name: name.clone(),
542 group: group.id(),
543 })?
544 } else {
545 Box::new(crate::dispatch::scan::ScanDispatch::new())
546 };
547 dispatchers.insert(group.id(), strategy);
548 strategy_ids.insert(group.id(), gs.strategy.clone());
549 }
550
551 Ok((groups, stop_lookup, dispatchers, strategy_ids))
552 }
553
554 fn remap_extensions(
556 extensions: &HashMap<String, HashMap<EntityId, String>>,
557 id_remap: &HashMap<EntityId, EntityId>,
558 ) -> HashMap<String, HashMap<EntityId, String>> {
559 extensions
560 .iter()
561 .map(|(name, entries)| {
562 let remapped: HashMap<EntityId, String> = entries
563 .iter()
564 .map(|(old_id, data)| {
565 let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
566 (new_id, data.clone())
567 })
568 .collect();
569 (name.clone(), remapped)
570 })
571 .collect()
572 }
573
574 fn collect_referenced_ids(snap: &EntitySnapshot, mut visit: impl FnMut(EntityId)) {
576 if let Some(ref elev) = snap.elevator {
577 for &r in &elev.riders {
578 visit(r);
579 }
580 if let Some(t) = elev.target_stop {
581 visit(t);
582 }
583 visit(elev.line);
584 match elev.phase {
585 crate::components::ElevatorPhase::MovingToStop(s)
586 | crate::components::ElevatorPhase::Repositioning(s) => visit(s),
587 _ => {}
588 }
589 for &s in &elev.restricted_stops {
590 visit(s);
591 }
592 }
593 if let Some(ref rider) = snap.rider {
594 if let Some(s) = rider.current_stop {
595 visit(s);
596 }
597 match rider.phase {
598 crate::components::RiderPhase::Boarding(e)
599 | crate::components::RiderPhase::Riding(e)
600 | crate::components::RiderPhase::Exiting(e) => visit(e),
601 _ => {}
602 }
603 }
604 if let Some(ref route) = snap.route {
605 for leg in &route.legs {
606 visit(leg.from);
607 visit(leg.to);
608 if let crate::components::TransportMode::Line(l) = leg.via {
609 visit(l);
610 }
611 }
612 }
613 if let Some(ref ac) = snap.access_control {
614 for &s in ac.allowed_stops() {
615 visit(s);
616 }
617 }
618 if let Some(ref dq) = snap.destination_queue {
619 for &e in dq.queue() {
620 visit(e);
621 }
622 }
623 for cc in &snap.car_calls {
624 visit(cc.floor);
625 for &r in &cc.pending_riders {
626 visit(r);
627 }
628 }
629 }
630}
631
632const SNAPSHOT_MAGIC: [u8; 8] = *b"ELEVSNAP";
634
635#[derive(Debug, Serialize, Deserialize)]
641struct SnapshotEnvelope {
642 magic: [u8; 8],
644 version: String,
646 payload: WorldSnapshot,
648}
649
650impl crate::sim::Simulation {
651 #[must_use]
657 pub fn snapshot(&self) -> WorldSnapshot {
658 let world = self.world();
659
660 let all_ids: Vec<EntityId> = world.alive.keys().collect();
662 let id_to_index: HashMap<EntityId, usize> = all_ids
663 .iter()
664 .copied()
665 .enumerate()
666 .map(|(i, e)| (e, i))
667 .collect();
668
669 let entities: Vec<EntitySnapshot> = all_ids
671 .iter()
672 .map(|&eid| EntitySnapshot {
673 original_id: eid,
674 position: world.position(eid).copied(),
675 velocity: world.velocity(eid).copied(),
676 elevator: world.elevator(eid).cloned(),
677 stop: world.stop(eid).cloned(),
678 rider: world.rider(eid).cloned(),
679 route: world.route(eid).cloned(),
680 line: world.line(eid).cloned(),
681 patience: world.patience(eid).copied(),
682 preferences: world.preferences(eid).copied(),
683 access_control: world.access_control(eid).cloned(),
684 disabled: world.is_disabled(eid),
685 #[cfg(feature = "energy")]
686 energy_profile: world.energy_profile(eid).cloned(),
687 #[cfg(feature = "energy")]
688 energy_metrics: world.energy_metrics(eid).cloned(),
689 service_mode: world.service_mode(eid).copied(),
690 destination_queue: world.destination_queue(eid).cloned(),
691 car_calls: world.car_calls(eid).to_vec(),
692 })
693 .collect();
694
695 let groups: Vec<GroupSnapshot> = self
697 .groups()
698 .iter()
699 .map(|g| {
700 let lines: Vec<LineSnapshotInfo> = g
701 .lines()
702 .iter()
703 .filter_map(|li| {
704 let entity_index = id_to_index.get(&li.entity()).copied()?;
705 Some(LineSnapshotInfo {
706 entity_index,
707 elevator_indices: li
708 .elevators()
709 .iter()
710 .filter_map(|eid| id_to_index.get(eid).copied())
711 .collect(),
712 stop_indices: li
713 .serves()
714 .iter()
715 .filter_map(|eid| id_to_index.get(eid).copied())
716 .collect(),
717 })
718 })
719 .collect();
720 GroupSnapshot {
721 id: g.id(),
722 name: g.name().to_owned(),
723 elevator_indices: g
724 .elevator_entities()
725 .iter()
726 .filter_map(|eid| id_to_index.get(eid).copied())
727 .collect(),
728 stop_indices: g
729 .stop_entities()
730 .iter()
731 .filter_map(|eid| id_to_index.get(eid).copied())
732 .collect(),
733 strategy: self
734 .strategy_id(g.id())
735 .cloned()
736 .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
737 lines,
738 reposition: self.reposition_id(g.id()).cloned(),
739 hall_call_mode: g.hall_call_mode(),
740 ack_latency_ticks: g.ack_latency_ticks(),
741 }
742 })
743 .collect();
744
745 let stop_lookup: HashMap<StopId, usize> = self
747 .stop_lookup_iter()
748 .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
749 .collect();
750
751 WorldSnapshot {
752 tick: self.current_tick(),
753 dt: self.dt(),
754 entities,
755 groups,
756 stop_lookup,
757 metrics: self.metrics().clone(),
758 metric_tags: self
759 .world()
760 .resource::<MetricTags>()
761 .cloned()
762 .unwrap_or_default(),
763 extensions: self.world().serialize_extensions(),
764 ticks_per_second: 1.0 / self.dt(),
765 hall_calls: world.iter_hall_calls().cloned().collect(),
766 }
767 }
768
769 pub fn snapshot_bytes(&self) -> Result<Vec<u8>, crate::error::SimError> {
790 let envelope = SnapshotEnvelope {
791 magic: SNAPSHOT_MAGIC,
792 version: env!("CARGO_PKG_VERSION").to_owned(),
793 payload: self.snapshot(),
794 };
795 postcard::to_allocvec(&envelope)
796 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))
797 }
798
799 pub fn restore_bytes(
814 bytes: &[u8],
815 custom_strategy_factory: CustomStrategyFactory<'_>,
816 ) -> Result<Self, crate::error::SimError> {
817 let (envelope, tail): (SnapshotEnvelope, &[u8]) = postcard::take_from_bytes(bytes)
818 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
819 if !tail.is_empty() {
820 return Err(crate::error::SimError::SnapshotFormat(format!(
821 "trailing bytes: {} unread of {}",
822 tail.len(),
823 bytes.len()
824 )));
825 }
826 if envelope.magic != SNAPSHOT_MAGIC {
827 return Err(crate::error::SimError::SnapshotFormat(
828 "magic bytes do not match".to_string(),
829 ));
830 }
831 let current = env!("CARGO_PKG_VERSION");
832 if envelope.version != current {
833 return Err(crate::error::SimError::SnapshotVersion {
834 saved: envelope.version,
835 current: current.to_owned(),
836 });
837 }
838 envelope.payload.restore(custom_strategy_factory)
839 }
840}