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 if let Some(strategy) = repo_id.instantiate() {
262 sim.set_reposition(gs.id, strategy, repo_id.clone());
263 } else {
264 sim.push_event(crate::events::Event::RepositionStrategyNotRestored {
265 group: gs.id,
266 });
267 }
268 }
269 }
270
271 Self::emit_dangling_warnings(
272 &self.entities,
273 &self.hall_calls,
274 &id_remap,
275 self.tick,
276 &mut sim,
277 );
278
279 Ok(sim)
280 }
281
282 fn spawn_entities(
284 world: &mut crate::world::World,
285 entities: &[EntitySnapshot],
286 ) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
287 let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
288 let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
289 for snap in entities {
290 let new_id = world.spawn();
291 index_to_id.push(new_id);
292 id_remap.insert(snap.original_id, new_id);
293 }
294 (index_to_id, id_remap)
295 }
296
297 fn attach_components(
299 world: &mut crate::world::World,
300 entities: &[EntitySnapshot],
301 index_to_id: &[EntityId],
302 id_remap: &HashMap<EntityId, EntityId>,
303 ) {
304 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
305 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
306
307 for (i, snap) in entities.iter().enumerate() {
308 let eid = index_to_id[i];
309
310 if let Some(pos) = snap.position {
311 world.set_position(eid, pos);
312 }
313 if let Some(vel) = snap.velocity {
314 world.set_velocity(eid, vel);
315 }
316 if let Some(ref elev) = snap.elevator {
317 let mut e = elev.clone();
318 e.riders = e.riders.iter().map(|&r| remap(r)).collect();
319 e.target_stop = remap_opt(e.target_stop);
320 e.line = remap(e.line);
321 e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
322 e.phase = match e.phase {
323 crate::components::ElevatorPhase::MovingToStop(s) => {
324 crate::components::ElevatorPhase::MovingToStop(remap(s))
325 }
326 crate::components::ElevatorPhase::Repositioning(s) => {
327 crate::components::ElevatorPhase::Repositioning(remap(s))
328 }
329 other => other,
330 };
331 world.set_elevator(eid, e);
332 }
333 if let Some(ref stop) = snap.stop {
334 world.set_stop(eid, stop.clone());
335 }
336 if let Some(ref rider) = snap.rider {
337 use crate::components::RiderPhase;
338 let mut r = rider.clone();
339 r.current_stop = remap_opt(r.current_stop);
340 r.phase = match r.phase {
341 RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
342 RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
343 RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
344 other => other,
345 };
346 world.set_rider(eid, r);
347 }
348 if let Some(ref route) = snap.route {
349 let mut rt = route.clone();
350 for leg in &mut rt.legs {
351 leg.from = remap(leg.from);
352 leg.to = remap(leg.to);
353 if let crate::components::TransportMode::Line(ref mut l) = leg.via {
354 *l = remap(*l);
355 }
356 }
357 world.set_route(eid, rt);
358 }
359 if let Some(ref line) = snap.line {
360 world.set_line(eid, line.clone());
361 }
362 if let Some(patience) = snap.patience {
363 world.set_patience(eid, patience);
364 }
365 if let Some(prefs) = snap.preferences {
366 world.set_preferences(eid, prefs);
367 }
368 if let Some(ref ac) = snap.access_control {
369 let remapped =
370 AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
371 world.set_access_control(eid, remapped);
372 }
373 if snap.disabled {
374 world.disable(eid);
375 }
376 #[cfg(feature = "energy")]
377 if let Some(ref profile) = snap.energy_profile {
378 world.set_energy_profile(eid, profile.clone());
379 }
380 #[cfg(feature = "energy")]
381 if let Some(ref em) = snap.energy_metrics {
382 world.set_energy_metrics(eid, em.clone());
383 }
384 if let Some(mode) = snap.service_mode {
385 world.set_service_mode(eid, mode);
386 }
387 if let Some(ref dq) = snap.destination_queue {
388 use crate::components::DestinationQueue as DQ;
389 let mut new_dq = DQ::new();
390 for &e in dq.queue() {
391 new_dq.push_back(remap(e));
392 }
393 world.set_destination_queue(eid, new_dq);
394 }
395 Self::attach_car_calls(world, eid, &snap.car_calls, id_remap);
396 }
397 }
398
399 fn attach_car_calls(
401 world: &mut crate::world::World,
402 car: EntityId,
403 car_calls: &[CarCall],
404 id_remap: &HashMap<EntityId, EntityId>,
405 ) {
406 if car_calls.is_empty() {
407 return;
408 }
409 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
410 let Some(slot) = world.car_calls_mut(car) else {
411 return;
412 };
413 for cc in car_calls {
414 let mut c = cc.clone();
415 c.car = car;
416 c.floor = remap(c.floor);
417 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
418 slot.push(c);
419 }
420 }
421
422 fn attach_hall_calls(
427 &self,
428 world: &mut crate::world::World,
429 id_remap: &HashMap<EntityId, EntityId>,
430 ) {
431 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
432 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
433 for hc in &self.hall_calls {
434 let mut c = hc.clone();
435 c.stop = remap(c.stop);
436 c.destination = remap_opt(c.destination);
437 c.assigned_car = remap_opt(c.assigned_car);
438 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
439 world.set_hall_call(c);
440 }
441 }
442
443 #[allow(clippy::type_complexity)]
445 fn rebuild_groups_and_dispatchers(
446 &self,
447 index_to_id: &[EntityId],
448 custom_strategy_factory: CustomStrategyFactory<'_>,
449 ) -> Result<
450 (
451 Vec<crate::dispatch::ElevatorGroup>,
452 HashMap<StopId, EntityId>,
453 std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
454 std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
455 ),
456 crate::error::SimError,
457 > {
458 use crate::dispatch::ElevatorGroup;
459
460 let groups: Vec<ElevatorGroup> = self
461 .groups
462 .iter()
463 .map(|gs| {
464 let elevator_entities: Vec<EntityId> = gs
465 .elevator_indices
466 .iter()
467 .filter_map(|&i| index_to_id.get(i).copied())
468 .collect();
469 let stop_entities: Vec<EntityId> = gs
470 .stop_indices
471 .iter()
472 .filter_map(|&i| index_to_id.get(i).copied())
473 .collect();
474
475 let lines = if gs.lines.is_empty() {
476 vec![crate::dispatch::LineInfo::new(
479 EntityId::default(),
480 elevator_entities,
481 stop_entities,
482 )]
483 } else {
484 gs.lines
485 .iter()
486 .filter_map(|lsi| {
487 let entity = index_to_id.get(lsi.entity_index).copied()?;
488 Some(crate::dispatch::LineInfo::new(
489 entity,
490 lsi.elevator_indices
491 .iter()
492 .filter_map(|&i| index_to_id.get(i).copied())
493 .collect(),
494 lsi.stop_indices
495 .iter()
496 .filter_map(|&i| index_to_id.get(i).copied())
497 .collect(),
498 ))
499 })
500 .collect()
501 };
502
503 ElevatorGroup::new(gs.id, gs.name.clone(), lines)
504 .with_hall_call_mode(gs.hall_call_mode)
505 .with_ack_latency_ticks(gs.ack_latency_ticks)
506 })
507 .collect();
508
509 let stop_lookup: HashMap<StopId, EntityId> = self
510 .stop_lookup
511 .iter()
512 .filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
513 .collect();
514
515 let mut dispatchers = std::collections::BTreeMap::new();
516 let mut strategy_ids = std::collections::BTreeMap::new();
517 for (gs, group) in self.groups.iter().zip(groups.iter()) {
518 let strategy: Box<dyn crate::dispatch::DispatchStrategy> =
519 if let Some(builtin) = gs.strategy.instantiate() {
520 builtin
521 } else if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
522 custom_strategy_factory
523 .and_then(|f| f(name))
524 .ok_or_else(|| crate::error::SimError::UnresolvedCustomStrategy {
525 name: name.clone(),
526 group: group.id(),
527 })?
528 } else {
529 Box::new(crate::dispatch::scan::ScanDispatch::new())
530 };
531 dispatchers.insert(group.id(), strategy);
532 strategy_ids.insert(group.id(), gs.strategy.clone());
533 }
534
535 Ok((groups, stop_lookup, dispatchers, strategy_ids))
536 }
537
538 fn remap_extensions(
540 extensions: &HashMap<String, HashMap<EntityId, String>>,
541 id_remap: &HashMap<EntityId, EntityId>,
542 ) -> HashMap<String, HashMap<EntityId, String>> {
543 extensions
544 .iter()
545 .map(|(name, entries)| {
546 let remapped: HashMap<EntityId, String> = entries
547 .iter()
548 .map(|(old_id, data)| {
549 let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
550 (new_id, data.clone())
551 })
552 .collect();
553 (name.clone(), remapped)
554 })
555 .collect()
556 }
557
558 fn emit_dangling_warnings(
560 entities: &[EntitySnapshot],
561 hall_calls: &[HallCall],
562 id_remap: &HashMap<EntityId, EntityId>,
563 tick: u64,
564 sim: &mut crate::sim::Simulation,
565 ) {
566 let mut seen = HashSet::new();
567 let mut check = |old: EntityId| {
568 if !id_remap.contains_key(&old) && seen.insert(old) {
569 sim.push_event(crate::events::Event::SnapshotDanglingReference {
570 stale_id: old,
571 tick,
572 });
573 }
574 };
575 for snap in entities {
576 Self::collect_referenced_ids(snap, &mut check);
577 }
578 for hc in hall_calls {
579 check(hc.stop);
580 if let Some(car) = hc.assigned_car {
581 check(car);
582 }
583 if let Some(dest) = hc.destination {
584 check(dest);
585 }
586 for &rider in &hc.pending_riders {
587 check(rider);
588 }
589 }
590 }
591
592 fn collect_referenced_ids(snap: &EntitySnapshot, mut visit: impl FnMut(EntityId)) {
594 if let Some(ref elev) = snap.elevator {
595 for &r in &elev.riders {
596 visit(r);
597 }
598 if let Some(t) = elev.target_stop {
599 visit(t);
600 }
601 visit(elev.line);
602 match elev.phase {
603 crate::components::ElevatorPhase::MovingToStop(s)
604 | crate::components::ElevatorPhase::Repositioning(s) => visit(s),
605 _ => {}
606 }
607 for &s in &elev.restricted_stops {
608 visit(s);
609 }
610 }
611 if let Some(ref rider) = snap.rider {
612 if let Some(s) = rider.current_stop {
613 visit(s);
614 }
615 match rider.phase {
616 crate::components::RiderPhase::Boarding(e)
617 | crate::components::RiderPhase::Riding(e)
618 | crate::components::RiderPhase::Exiting(e) => visit(e),
619 _ => {}
620 }
621 }
622 if let Some(ref route) = snap.route {
623 for leg in &route.legs {
624 visit(leg.from);
625 visit(leg.to);
626 if let crate::components::TransportMode::Line(l) = leg.via {
627 visit(l);
628 }
629 }
630 }
631 if let Some(ref ac) = snap.access_control {
632 for &s in ac.allowed_stops() {
633 visit(s);
634 }
635 }
636 if let Some(ref dq) = snap.destination_queue {
637 for &e in dq.queue() {
638 visit(e);
639 }
640 }
641 for cc in &snap.car_calls {
642 visit(cc.floor);
643 for &r in &cc.pending_riders {
644 visit(r);
645 }
646 }
647 }
648}
649
650const SNAPSHOT_MAGIC: [u8; 8] = *b"ELEVSNAP";
652
653#[derive(Debug, Serialize, Deserialize)]
659struct SnapshotEnvelope {
660 magic: [u8; 8],
662 version: String,
664 payload: WorldSnapshot,
666}
667
668impl crate::sim::Simulation {
669 #[must_use]
675 pub fn snapshot(&self) -> WorldSnapshot {
676 let world = self.world();
677
678 let all_ids: Vec<EntityId> = world.alive.keys().collect();
680 let id_to_index: HashMap<EntityId, usize> = all_ids
681 .iter()
682 .copied()
683 .enumerate()
684 .map(|(i, e)| (e, i))
685 .collect();
686
687 let entities: Vec<EntitySnapshot> = all_ids
689 .iter()
690 .map(|&eid| EntitySnapshot {
691 original_id: eid,
692 position: world.position(eid).copied(),
693 velocity: world.velocity(eid).copied(),
694 elevator: world.elevator(eid).cloned(),
695 stop: world.stop(eid).cloned(),
696 rider: world.rider(eid).cloned(),
697 route: world.route(eid).cloned(),
698 line: world.line(eid).cloned(),
699 patience: world.patience(eid).copied(),
700 preferences: world.preferences(eid).copied(),
701 access_control: world.access_control(eid).cloned(),
702 disabled: world.is_disabled(eid),
703 #[cfg(feature = "energy")]
704 energy_profile: world.energy_profile(eid).cloned(),
705 #[cfg(feature = "energy")]
706 energy_metrics: world.energy_metrics(eid).cloned(),
707 service_mode: world.service_mode(eid).copied(),
708 destination_queue: world.destination_queue(eid).cloned(),
709 car_calls: world.car_calls(eid).to_vec(),
710 })
711 .collect();
712
713 let groups: Vec<GroupSnapshot> = self
715 .groups()
716 .iter()
717 .map(|g| {
718 let lines: Vec<LineSnapshotInfo> = g
719 .lines()
720 .iter()
721 .filter_map(|li| {
722 let entity_index = id_to_index.get(&li.entity()).copied()?;
723 Some(LineSnapshotInfo {
724 entity_index,
725 elevator_indices: li
726 .elevators()
727 .iter()
728 .filter_map(|eid| id_to_index.get(eid).copied())
729 .collect(),
730 stop_indices: li
731 .serves()
732 .iter()
733 .filter_map(|eid| id_to_index.get(eid).copied())
734 .collect(),
735 })
736 })
737 .collect();
738 GroupSnapshot {
739 id: g.id(),
740 name: g.name().to_owned(),
741 elevator_indices: g
742 .elevator_entities()
743 .iter()
744 .filter_map(|eid| id_to_index.get(eid).copied())
745 .collect(),
746 stop_indices: g
747 .stop_entities()
748 .iter()
749 .filter_map(|eid| id_to_index.get(eid).copied())
750 .collect(),
751 strategy: self
752 .strategy_id(g.id())
753 .cloned()
754 .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
755 lines,
756 reposition: self.reposition_id(g.id()).cloned(),
757 hall_call_mode: g.hall_call_mode(),
758 ack_latency_ticks: g.ack_latency_ticks(),
759 }
760 })
761 .collect();
762
763 let stop_lookup: HashMap<StopId, usize> = self
765 .stop_lookup_iter()
766 .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
767 .collect();
768
769 WorldSnapshot {
770 tick: self.current_tick(),
771 dt: self.dt(),
772 entities,
773 groups,
774 stop_lookup,
775 metrics: self.metrics().clone(),
776 metric_tags: self
777 .world()
778 .resource::<MetricTags>()
779 .cloned()
780 .unwrap_or_default(),
781 extensions: self.world().serialize_extensions(),
782 ticks_per_second: 1.0 / self.dt(),
783 hall_calls: world.iter_hall_calls().cloned().collect(),
784 }
785 }
786
787 pub fn snapshot_bytes(&self) -> Result<Vec<u8>, crate::error::SimError> {
808 let envelope = SnapshotEnvelope {
809 magic: SNAPSHOT_MAGIC,
810 version: env!("CARGO_PKG_VERSION").to_owned(),
811 payload: self.snapshot(),
812 };
813 postcard::to_allocvec(&envelope)
814 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))
815 }
816
817 pub fn restore_bytes(
832 bytes: &[u8],
833 custom_strategy_factory: CustomStrategyFactory<'_>,
834 ) -> Result<Self, crate::error::SimError> {
835 let (envelope, tail): (SnapshotEnvelope, &[u8]) = postcard::take_from_bytes(bytes)
836 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
837 if !tail.is_empty() {
838 return Err(crate::error::SimError::SnapshotFormat(format!(
839 "trailing bytes: {} unread of {}",
840 tail.len(),
841 bytes.len()
842 )));
843 }
844 if envelope.magic != SNAPSHOT_MAGIC {
845 return Err(crate::error::SimError::SnapshotFormat(
846 "magic bytes do not match".to_string(),
847 ));
848 }
849 let current = env!("CARGO_PKG_VERSION");
850 if envelope.version != current {
851 return Err(crate::error::SimError::SnapshotVersion {
852 saved: envelope.version,
853 current: current.to_owned(),
854 });
855 }
856 envelope.payload.restore(custom_strategy_factory)
857 }
858}