1use crate::components::{
11 AccessControl, CarCall, DestinationQueue, Elevator, HallCall, Line, Patience, Position,
12 Preferences, Rider, Route, 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#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct EntitySnapshot {
25 pub original_id: EntityId,
27 pub position: Option<Position>,
29 pub velocity: Option<Velocity>,
31 pub elevator: Option<Elevator>,
33 pub stop: Option<Stop>,
35 pub rider: Option<Rider>,
37 pub route: Option<Route>,
39 #[serde(default)]
41 pub line: Option<Line>,
42 pub patience: Option<Patience>,
44 pub preferences: Option<Preferences>,
46 #[serde(default)]
48 pub access_control: Option<AccessControl>,
49 pub disabled: bool,
51 #[cfg(feature = "energy")]
53 #[serde(default)]
54 pub energy_profile: Option<crate::energy::EnergyProfile>,
55 #[cfg(feature = "energy")]
57 #[serde(default)]
58 pub energy_metrics: Option<crate::energy::EnergyMetrics>,
59 #[serde(default)]
61 pub service_mode: Option<crate::components::ServiceMode>,
62 #[serde(default)]
64 pub destination_queue: Option<DestinationQueue>,
65 #[serde(default)]
67 pub car_calls: Vec<CarCall>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct WorldSnapshot {
80 pub tick: u64,
82 pub dt: f64,
84 pub entities: Vec<EntitySnapshot>,
87 pub groups: Vec<GroupSnapshot>,
89 pub stop_lookup: HashMap<StopId, usize>,
91 pub metrics: Metrics,
93 pub metric_tags: MetricTags,
95 pub extensions: HashMap<String, HashMap<EntityId, String>>,
97 pub ticks_per_second: f64,
99 #[serde(default)]
101 pub hall_calls: Vec<HallCall>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct LineSnapshotInfo {
107 pub entity_index: usize,
109 pub elevator_indices: Vec<usize>,
111 pub stop_indices: Vec<usize>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct GroupSnapshot {
118 pub id: GroupId,
120 pub name: String,
122 pub elevator_indices: Vec<usize>,
124 pub stop_indices: Vec<usize>,
126 pub strategy: crate::dispatch::BuiltinStrategy,
128 #[serde(default)]
130 pub lines: Vec<LineSnapshotInfo>,
131 #[serde(default)]
133 pub reposition: Option<crate::dispatch::BuiltinReposition>,
134 #[serde(default)]
136 pub hall_call_mode: crate::dispatch::HallCallMode,
137 #[serde(default)]
139 pub ack_latency_ticks: u32,
140}
141
142pub(crate) struct PendingExtensions(pub(crate) HashMap<String, HashMap<EntityId, String>>);
148
149type CustomStrategyFactory<'a> =
151 Option<&'a dyn Fn(&str) -> Option<Box<dyn crate::dispatch::DispatchStrategy>>>;
152
153impl WorldSnapshot {
154 #[must_use]
165 pub fn restore(
166 self,
167 custom_strategy_factory: CustomStrategyFactory<'_>,
168 ) -> crate::sim::Simulation {
169 use crate::world::{SortedStops, World};
170
171 let mut world = World::new();
172
173 let (index_to_id, id_remap) = Self::spawn_entities(&mut world, &self.entities);
175
176 Self::attach_components(&mut world, &self.entities, &index_to_id, &id_remap);
178
179 self.attach_hall_calls(&mut world, &id_remap);
181
182 let mut sorted: Vec<(f64, EntityId)> = world
184 .iter_stops()
185 .map(|(eid, stop)| (stop.position, eid))
186 .collect();
187 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
188 world.insert_resource(SortedStops(sorted));
189
190 let (mut groups, stop_lookup, dispatchers, strategy_ids) =
192 self.rebuild_groups_and_dispatchers(&index_to_id, custom_strategy_factory);
193
194 for group in &mut groups {
197 let group_id = group.id();
198 let lines = group.lines_mut();
199 for line_info in lines.iter_mut() {
200 if line_info.entity() != EntityId::default() {
201 continue;
202 }
203 let (min_pos, max_pos) = line_info
205 .serves()
206 .iter()
207 .filter_map(|&sid| world.stop(sid).map(|s| s.position))
208 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| {
209 (lo.min(p), hi.max(p))
210 });
211 let line_eid = world.spawn();
212 world.set_line(
213 line_eid,
214 Line {
215 name: format!("Legacy-{group_id}"),
216 group: group_id,
217 orientation: crate::components::Orientation::Vertical,
218 position: None,
219 min_position: if min_pos.is_finite() { min_pos } else { 0.0 },
220 max_position: if max_pos.is_finite() { max_pos } else { 0.0 },
221 max_cars: None,
222 },
223 );
224 for &elev_eid in line_info.elevators() {
226 if let Some(car) = world.elevator_mut(elev_eid) {
227 car.line = line_eid;
228 }
229 }
230 line_info.set_entity(line_eid);
231 }
232 }
233
234 let remapped_exts = Self::remap_extensions(&self.extensions, &id_remap);
236 world.insert_resource(PendingExtensions(remapped_exts));
237
238 let mut tags = self.metric_tags;
240 tags.remap_entity_ids(&id_remap);
241 world.insert_resource(tags);
242
243 let mut sim = crate::sim::Simulation::from_parts(
244 world,
245 self.tick,
246 self.dt,
247 groups,
248 stop_lookup,
249 dispatchers,
250 strategy_ids,
251 self.metrics,
252 self.ticks_per_second,
253 );
254
255 for gs in &self.groups {
257 if let Some(ref repo_id) = gs.reposition
258 && let Some(strategy) = repo_id.instantiate()
259 {
260 sim.set_reposition(gs.id, strategy, repo_id.clone());
261 }
262 }
263
264 sim
265 }
266
267 fn spawn_entities(
269 world: &mut crate::world::World,
270 entities: &[EntitySnapshot],
271 ) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
272 let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
273 let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
274 for snap in entities {
275 let new_id = world.spawn();
276 index_to_id.push(new_id);
277 id_remap.insert(snap.original_id, new_id);
278 }
279 (index_to_id, id_remap)
280 }
281
282 fn attach_components(
284 world: &mut crate::world::World,
285 entities: &[EntitySnapshot],
286 index_to_id: &[EntityId],
287 id_remap: &HashMap<EntityId, EntityId>,
288 ) {
289 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
290 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
291
292 for (i, snap) in entities.iter().enumerate() {
293 let eid = index_to_id[i];
294
295 if let Some(pos) = snap.position {
296 world.set_position(eid, pos);
297 }
298 if let Some(vel) = snap.velocity {
299 world.set_velocity(eid, vel);
300 }
301 if let Some(ref elev) = snap.elevator {
302 let mut e = elev.clone();
303 e.riders = e.riders.iter().map(|&r| remap(r)).collect();
304 e.target_stop = remap_opt(e.target_stop);
305 e.line = remap(e.line);
306 e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
307 e.phase = match e.phase {
308 crate::components::ElevatorPhase::MovingToStop(s) => {
309 crate::components::ElevatorPhase::MovingToStop(remap(s))
310 }
311 crate::components::ElevatorPhase::Repositioning(s) => {
312 crate::components::ElevatorPhase::Repositioning(remap(s))
313 }
314 other => other,
315 };
316 world.set_elevator(eid, e);
317 }
318 if let Some(ref stop) = snap.stop {
319 world.set_stop(eid, stop.clone());
320 }
321 if let Some(ref rider) = snap.rider {
322 use crate::components::RiderPhase;
323 let mut r = rider.clone();
324 r.current_stop = remap_opt(r.current_stop);
325 r.phase = match r.phase {
326 RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
327 RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
328 RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
329 other => other,
330 };
331 world.set_rider(eid, r);
332 }
333 if let Some(ref route) = snap.route {
334 let mut rt = route.clone();
335 for leg in &mut rt.legs {
336 leg.from = remap(leg.from);
337 leg.to = remap(leg.to);
338 if let crate::components::TransportMode::Line(ref mut l) = leg.via {
339 *l = remap(*l);
340 }
341 }
342 world.set_route(eid, rt);
343 }
344 if let Some(ref line) = snap.line {
345 world.set_line(eid, line.clone());
346 }
347 if let Some(patience) = snap.patience {
348 world.set_patience(eid, patience);
349 }
350 if let Some(prefs) = snap.preferences {
351 world.set_preferences(eid, prefs);
352 }
353 if let Some(ref ac) = snap.access_control {
354 let remapped =
355 AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
356 world.set_access_control(eid, remapped);
357 }
358 if snap.disabled {
359 world.disable(eid);
360 }
361 #[cfg(feature = "energy")]
362 if let Some(ref profile) = snap.energy_profile {
363 world.set_energy_profile(eid, profile.clone());
364 }
365 #[cfg(feature = "energy")]
366 if let Some(ref em) = snap.energy_metrics {
367 world.set_energy_metrics(eid, em.clone());
368 }
369 if let Some(mode) = snap.service_mode {
370 world.set_service_mode(eid, mode);
371 }
372 if let Some(ref dq) = snap.destination_queue {
373 use crate::components::DestinationQueue as DQ;
374 let mut new_dq = DQ::new();
375 for &e in dq.queue() {
376 new_dq.push_back(remap(e));
377 }
378 world.set_destination_queue(eid, new_dq);
379 }
380 Self::attach_car_calls(world, eid, &snap.car_calls, id_remap);
381 }
382 }
383
384 fn attach_car_calls(
386 world: &mut crate::world::World,
387 car: EntityId,
388 car_calls: &[CarCall],
389 id_remap: &HashMap<EntityId, EntityId>,
390 ) {
391 if car_calls.is_empty() {
392 return;
393 }
394 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
395 let Some(slot) = world.car_calls_mut(car) else {
396 return;
397 };
398 for cc in car_calls {
399 let mut c = cc.clone();
400 c.car = car;
401 c.floor = remap(c.floor);
402 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
403 slot.push(c);
404 }
405 }
406
407 fn attach_hall_calls(
412 &self,
413 world: &mut crate::world::World,
414 id_remap: &HashMap<EntityId, EntityId>,
415 ) {
416 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
417 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
418 for hc in &self.hall_calls {
419 let mut c = hc.clone();
420 c.stop = remap(c.stop);
421 c.destination = remap_opt(c.destination);
422 c.assigned_car = remap_opt(c.assigned_car);
423 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
424 world.set_hall_call(c);
425 }
426 }
427
428 #[allow(clippy::type_complexity)]
430 fn rebuild_groups_and_dispatchers(
431 &self,
432 index_to_id: &[EntityId],
433 custom_strategy_factory: CustomStrategyFactory<'_>,
434 ) -> (
435 Vec<crate::dispatch::ElevatorGroup>,
436 HashMap<StopId, EntityId>,
437 std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
438 std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
439 ) {
440 use crate::dispatch::ElevatorGroup;
441
442 let groups: Vec<ElevatorGroup> = self
443 .groups
444 .iter()
445 .map(|gs| {
446 let elevator_entities: Vec<EntityId> = gs
447 .elevator_indices
448 .iter()
449 .filter_map(|&i| index_to_id.get(i).copied())
450 .collect();
451 let stop_entities: Vec<EntityId> = gs
452 .stop_indices
453 .iter()
454 .filter_map(|&i| index_to_id.get(i).copied())
455 .collect();
456
457 let lines = if gs.lines.is_empty() {
458 vec![crate::dispatch::LineInfo::new(
461 EntityId::default(),
462 elevator_entities,
463 stop_entities,
464 )]
465 } else {
466 gs.lines
467 .iter()
468 .filter_map(|lsi| {
469 let entity = index_to_id.get(lsi.entity_index).copied()?;
470 Some(crate::dispatch::LineInfo::new(
471 entity,
472 lsi.elevator_indices
473 .iter()
474 .filter_map(|&i| index_to_id.get(i).copied())
475 .collect(),
476 lsi.stop_indices
477 .iter()
478 .filter_map(|&i| index_to_id.get(i).copied())
479 .collect(),
480 ))
481 })
482 .collect()
483 };
484
485 ElevatorGroup::new(gs.id, gs.name.clone(), lines)
486 .with_hall_call_mode(gs.hall_call_mode)
487 .with_ack_latency_ticks(gs.ack_latency_ticks)
488 })
489 .collect();
490
491 let stop_lookup: HashMap<StopId, EntityId> = self
492 .stop_lookup
493 .iter()
494 .filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
495 .collect();
496
497 let mut dispatchers = std::collections::BTreeMap::new();
498 let mut strategy_ids = std::collections::BTreeMap::new();
499 for (gs, group) in self.groups.iter().zip(groups.iter()) {
500 let strategy: Box<dyn crate::dispatch::DispatchStrategy> = gs
501 .strategy
502 .instantiate()
503 .or_else(|| {
504 if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
505 custom_strategy_factory.and_then(|f| f(name))
506 } else {
507 None
508 }
509 })
510 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
511 dispatchers.insert(group.id(), strategy);
512 strategy_ids.insert(group.id(), gs.strategy.clone());
513 }
514
515 (groups, stop_lookup, dispatchers, strategy_ids)
516 }
517
518 fn remap_extensions(
520 extensions: &HashMap<String, HashMap<EntityId, String>>,
521 id_remap: &HashMap<EntityId, EntityId>,
522 ) -> HashMap<String, HashMap<EntityId, String>> {
523 extensions
524 .iter()
525 .map(|(name, entries)| {
526 let remapped: HashMap<EntityId, String> = entries
527 .iter()
528 .map(|(old_id, data)| {
529 let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
530 (new_id, data.clone())
531 })
532 .collect();
533 (name.clone(), remapped)
534 })
535 .collect()
536 }
537}
538
539const SNAPSHOT_MAGIC: [u8; 8] = *b"ELEVSNAP";
541
542#[derive(Debug, Serialize, Deserialize)]
548struct SnapshotEnvelope {
549 magic: [u8; 8],
551 version: String,
553 payload: WorldSnapshot,
555}
556
557impl crate::sim::Simulation {
558 #[must_use]
564 pub fn snapshot(&self) -> WorldSnapshot {
565 let world = self.world();
566
567 let all_ids: Vec<EntityId> = world.alive.keys().collect();
569 let id_to_index: HashMap<EntityId, usize> = all_ids
570 .iter()
571 .copied()
572 .enumerate()
573 .map(|(i, e)| (e, i))
574 .collect();
575
576 let entities: Vec<EntitySnapshot> = all_ids
578 .iter()
579 .map(|&eid| EntitySnapshot {
580 original_id: eid,
581 position: world.position(eid).copied(),
582 velocity: world.velocity(eid).copied(),
583 elevator: world.elevator(eid).cloned(),
584 stop: world.stop(eid).cloned(),
585 rider: world.rider(eid).cloned(),
586 route: world.route(eid).cloned(),
587 line: world.line(eid).cloned(),
588 patience: world.patience(eid).copied(),
589 preferences: world.preferences(eid).copied(),
590 access_control: world.access_control(eid).cloned(),
591 disabled: world.is_disabled(eid),
592 #[cfg(feature = "energy")]
593 energy_profile: world.energy_profile(eid).cloned(),
594 #[cfg(feature = "energy")]
595 energy_metrics: world.energy_metrics(eid).cloned(),
596 service_mode: world.service_mode(eid).copied(),
597 destination_queue: world.destination_queue(eid).cloned(),
598 car_calls: world.car_calls(eid).to_vec(),
599 })
600 .collect();
601
602 let groups: Vec<GroupSnapshot> = self
604 .groups()
605 .iter()
606 .map(|g| {
607 let lines: Vec<LineSnapshotInfo> = g
608 .lines()
609 .iter()
610 .filter_map(|li| {
611 let entity_index = id_to_index.get(&li.entity()).copied()?;
612 Some(LineSnapshotInfo {
613 entity_index,
614 elevator_indices: li
615 .elevators()
616 .iter()
617 .filter_map(|eid| id_to_index.get(eid).copied())
618 .collect(),
619 stop_indices: li
620 .serves()
621 .iter()
622 .filter_map(|eid| id_to_index.get(eid).copied())
623 .collect(),
624 })
625 })
626 .collect();
627 GroupSnapshot {
628 id: g.id(),
629 name: g.name().to_owned(),
630 elevator_indices: g
631 .elevator_entities()
632 .iter()
633 .filter_map(|eid| id_to_index.get(eid).copied())
634 .collect(),
635 stop_indices: g
636 .stop_entities()
637 .iter()
638 .filter_map(|eid| id_to_index.get(eid).copied())
639 .collect(),
640 strategy: self
641 .strategy_id(g.id())
642 .cloned()
643 .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
644 lines,
645 reposition: self.reposition_id(g.id()).cloned(),
646 hall_call_mode: g.hall_call_mode(),
647 ack_latency_ticks: g.ack_latency_ticks(),
648 }
649 })
650 .collect();
651
652 let stop_lookup: HashMap<StopId, usize> = self
654 .stop_lookup_iter()
655 .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
656 .collect();
657
658 WorldSnapshot {
659 tick: self.current_tick(),
660 dt: self.dt(),
661 entities,
662 groups,
663 stop_lookup,
664 metrics: self.metrics().clone(),
665 metric_tags: self
666 .world()
667 .resource::<MetricTags>()
668 .cloned()
669 .unwrap_or_default(),
670 extensions: self.world().serialize_extensions(),
671 ticks_per_second: 1.0 / self.dt(),
672 hall_calls: world.iter_hall_calls().cloned().collect(),
673 }
674 }
675
676 pub fn snapshot_bytes(&self) -> Result<Vec<u8>, crate::error::SimError> {
697 let envelope = SnapshotEnvelope {
698 magic: SNAPSHOT_MAGIC,
699 version: env!("CARGO_PKG_VERSION").to_owned(),
700 payload: self.snapshot(),
701 };
702 postcard::to_allocvec(&envelope)
703 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))
704 }
705
706 pub fn restore_bytes(
719 bytes: &[u8],
720 custom_strategy_factory: CustomStrategyFactory<'_>,
721 ) -> Result<Self, crate::error::SimError> {
722 let (envelope, tail): (SnapshotEnvelope, &[u8]) = postcard::take_from_bytes(bytes)
723 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
724 if !tail.is_empty() {
725 return Err(crate::error::SimError::SnapshotFormat(format!(
726 "trailing bytes: {} unread of {}",
727 tail.len(),
728 bytes.len()
729 )));
730 }
731 if envelope.magic != SNAPSHOT_MAGIC {
732 return Err(crate::error::SimError::SnapshotFormat(
733 "magic bytes do not match".to_string(),
734 ));
735 }
736 let current = env!("CARGO_PKG_VERSION");
737 if envelope.version != current {
738 return Err(crate::error::SimError::SnapshotVersion {
739 saved: envelope.version,
740 current: current.to_owned(),
741 });
742 }
743 Ok(envelope.payload.restore(custom_strategy_factory))
744 }
745}