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;
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 Ok(sim)
268 }
269
270 fn spawn_entities(
272 world: &mut crate::world::World,
273 entities: &[EntitySnapshot],
274 ) -> (Vec<EntityId>, HashMap<EntityId, EntityId>) {
275 let mut index_to_id: Vec<EntityId> = Vec::with_capacity(entities.len());
276 let mut id_remap: HashMap<EntityId, EntityId> = HashMap::new();
277 for snap in entities {
278 let new_id = world.spawn();
279 index_to_id.push(new_id);
280 id_remap.insert(snap.original_id, new_id);
281 }
282 (index_to_id, id_remap)
283 }
284
285 fn attach_components(
287 world: &mut crate::world::World,
288 entities: &[EntitySnapshot],
289 index_to_id: &[EntityId],
290 id_remap: &HashMap<EntityId, EntityId>,
291 ) {
292 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
293 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
294
295 for (i, snap) in entities.iter().enumerate() {
296 let eid = index_to_id[i];
297
298 if let Some(pos) = snap.position {
299 world.set_position(eid, pos);
300 }
301 if let Some(vel) = snap.velocity {
302 world.set_velocity(eid, vel);
303 }
304 if let Some(ref elev) = snap.elevator {
305 let mut e = elev.clone();
306 e.riders = e.riders.iter().map(|&r| remap(r)).collect();
307 e.target_stop = remap_opt(e.target_stop);
308 e.line = remap(e.line);
309 e.restricted_stops = e.restricted_stops.iter().map(|&s| remap(s)).collect();
310 e.phase = match e.phase {
311 crate::components::ElevatorPhase::MovingToStop(s) => {
312 crate::components::ElevatorPhase::MovingToStop(remap(s))
313 }
314 crate::components::ElevatorPhase::Repositioning(s) => {
315 crate::components::ElevatorPhase::Repositioning(remap(s))
316 }
317 other => other,
318 };
319 world.set_elevator(eid, e);
320 }
321 if let Some(ref stop) = snap.stop {
322 world.set_stop(eid, stop.clone());
323 }
324 if let Some(ref rider) = snap.rider {
325 use crate::components::RiderPhase;
326 let mut r = rider.clone();
327 r.current_stop = remap_opt(r.current_stop);
328 r.phase = match r.phase {
329 RiderPhase::Boarding(e) => RiderPhase::Boarding(remap(e)),
330 RiderPhase::Riding(e) => RiderPhase::Riding(remap(e)),
331 RiderPhase::Exiting(e) => RiderPhase::Exiting(remap(e)),
332 other => other,
333 };
334 world.set_rider(eid, r);
335 }
336 if let Some(ref route) = snap.route {
337 let mut rt = route.clone();
338 for leg in &mut rt.legs {
339 leg.from = remap(leg.from);
340 leg.to = remap(leg.to);
341 if let crate::components::TransportMode::Line(ref mut l) = leg.via {
342 *l = remap(*l);
343 }
344 }
345 world.set_route(eid, rt);
346 }
347 if let Some(ref line) = snap.line {
348 world.set_line(eid, line.clone());
349 }
350 if let Some(patience) = snap.patience {
351 world.set_patience(eid, patience);
352 }
353 if let Some(prefs) = snap.preferences {
354 world.set_preferences(eid, prefs);
355 }
356 if let Some(ref ac) = snap.access_control {
357 let remapped =
358 AccessControl::new(ac.allowed_stops().iter().map(|&s| remap(s)).collect());
359 world.set_access_control(eid, remapped);
360 }
361 if snap.disabled {
362 world.disable(eid);
363 }
364 #[cfg(feature = "energy")]
365 if let Some(ref profile) = snap.energy_profile {
366 world.set_energy_profile(eid, profile.clone());
367 }
368 #[cfg(feature = "energy")]
369 if let Some(ref em) = snap.energy_metrics {
370 world.set_energy_metrics(eid, em.clone());
371 }
372 if let Some(mode) = snap.service_mode {
373 world.set_service_mode(eid, mode);
374 }
375 if let Some(ref dq) = snap.destination_queue {
376 use crate::components::DestinationQueue as DQ;
377 let mut new_dq = DQ::new();
378 for &e in dq.queue() {
379 new_dq.push_back(remap(e));
380 }
381 world.set_destination_queue(eid, new_dq);
382 }
383 Self::attach_car_calls(world, eid, &snap.car_calls, id_remap);
384 }
385 }
386
387 fn attach_car_calls(
389 world: &mut crate::world::World,
390 car: EntityId,
391 car_calls: &[CarCall],
392 id_remap: &HashMap<EntityId, EntityId>,
393 ) {
394 if car_calls.is_empty() {
395 return;
396 }
397 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
398 let Some(slot) = world.car_calls_mut(car) else {
399 return;
400 };
401 for cc in car_calls {
402 let mut c = cc.clone();
403 c.car = car;
404 c.floor = remap(c.floor);
405 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
406 slot.push(c);
407 }
408 }
409
410 fn attach_hall_calls(
415 &self,
416 world: &mut crate::world::World,
417 id_remap: &HashMap<EntityId, EntityId>,
418 ) {
419 let remap = |old: EntityId| -> EntityId { id_remap.get(&old).copied().unwrap_or(old) };
420 let remap_opt = |old: Option<EntityId>| -> Option<EntityId> { old.map(&remap) };
421 for hc in &self.hall_calls {
422 let mut c = hc.clone();
423 c.stop = remap(c.stop);
424 c.destination = remap_opt(c.destination);
425 c.assigned_car = remap_opt(c.assigned_car);
426 c.pending_riders = c.pending_riders.iter().map(|&r| remap(r)).collect();
427 world.set_hall_call(c);
428 }
429 }
430
431 #[allow(clippy::type_complexity)]
433 fn rebuild_groups_and_dispatchers(
434 &self,
435 index_to_id: &[EntityId],
436 custom_strategy_factory: CustomStrategyFactory<'_>,
437 ) -> Result<
438 (
439 Vec<crate::dispatch::ElevatorGroup>,
440 HashMap<StopId, EntityId>,
441 std::collections::BTreeMap<GroupId, Box<dyn crate::dispatch::DispatchStrategy>>,
442 std::collections::BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
443 ),
444 crate::error::SimError,
445 > {
446 use crate::dispatch::ElevatorGroup;
447
448 let groups: Vec<ElevatorGroup> = self
449 .groups
450 .iter()
451 .map(|gs| {
452 let elevator_entities: Vec<EntityId> = gs
453 .elevator_indices
454 .iter()
455 .filter_map(|&i| index_to_id.get(i).copied())
456 .collect();
457 let stop_entities: Vec<EntityId> = gs
458 .stop_indices
459 .iter()
460 .filter_map(|&i| index_to_id.get(i).copied())
461 .collect();
462
463 let lines = if gs.lines.is_empty() {
464 vec![crate::dispatch::LineInfo::new(
467 EntityId::default(),
468 elevator_entities,
469 stop_entities,
470 )]
471 } else {
472 gs.lines
473 .iter()
474 .filter_map(|lsi| {
475 let entity = index_to_id.get(lsi.entity_index).copied()?;
476 Some(crate::dispatch::LineInfo::new(
477 entity,
478 lsi.elevator_indices
479 .iter()
480 .filter_map(|&i| index_to_id.get(i).copied())
481 .collect(),
482 lsi.stop_indices
483 .iter()
484 .filter_map(|&i| index_to_id.get(i).copied())
485 .collect(),
486 ))
487 })
488 .collect()
489 };
490
491 ElevatorGroup::new(gs.id, gs.name.clone(), lines)
492 .with_hall_call_mode(gs.hall_call_mode)
493 .with_ack_latency_ticks(gs.ack_latency_ticks)
494 })
495 .collect();
496
497 let stop_lookup: HashMap<StopId, EntityId> = self
498 .stop_lookup
499 .iter()
500 .filter_map(|(sid, &idx)| index_to_id.get(idx).map(|&eid| (*sid, eid)))
501 .collect();
502
503 let mut dispatchers = std::collections::BTreeMap::new();
504 let mut strategy_ids = std::collections::BTreeMap::new();
505 for (gs, group) in self.groups.iter().zip(groups.iter()) {
506 let strategy: Box<dyn crate::dispatch::DispatchStrategy> =
507 if let Some(builtin) = gs.strategy.instantiate() {
508 builtin
509 } else if let crate::dispatch::BuiltinStrategy::Custom(ref name) = gs.strategy {
510 custom_strategy_factory
511 .and_then(|f| f(name))
512 .ok_or_else(|| crate::error::SimError::UnresolvedCustomStrategy {
513 name: name.clone(),
514 group: group.id(),
515 })?
516 } else {
517 Box::new(crate::dispatch::scan::ScanDispatch::new())
518 };
519 dispatchers.insert(group.id(), strategy);
520 strategy_ids.insert(group.id(), gs.strategy.clone());
521 }
522
523 Ok((groups, stop_lookup, dispatchers, strategy_ids))
524 }
525
526 fn remap_extensions(
528 extensions: &HashMap<String, HashMap<EntityId, String>>,
529 id_remap: &HashMap<EntityId, EntityId>,
530 ) -> HashMap<String, HashMap<EntityId, String>> {
531 extensions
532 .iter()
533 .map(|(name, entries)| {
534 let remapped: HashMap<EntityId, String> = entries
535 .iter()
536 .map(|(old_id, data)| {
537 let new_id = id_remap.get(old_id).copied().unwrap_or(*old_id);
538 (new_id, data.clone())
539 })
540 .collect();
541 (name.clone(), remapped)
542 })
543 .collect()
544 }
545}
546
547const SNAPSHOT_MAGIC: [u8; 8] = *b"ELEVSNAP";
549
550#[derive(Debug, Serialize, Deserialize)]
556struct SnapshotEnvelope {
557 magic: [u8; 8],
559 version: String,
561 payload: WorldSnapshot,
563}
564
565impl crate::sim::Simulation {
566 #[must_use]
572 pub fn snapshot(&self) -> WorldSnapshot {
573 let world = self.world();
574
575 let all_ids: Vec<EntityId> = world.alive.keys().collect();
577 let id_to_index: HashMap<EntityId, usize> = all_ids
578 .iter()
579 .copied()
580 .enumerate()
581 .map(|(i, e)| (e, i))
582 .collect();
583
584 let entities: Vec<EntitySnapshot> = all_ids
586 .iter()
587 .map(|&eid| EntitySnapshot {
588 original_id: eid,
589 position: world.position(eid).copied(),
590 velocity: world.velocity(eid).copied(),
591 elevator: world.elevator(eid).cloned(),
592 stop: world.stop(eid).cloned(),
593 rider: world.rider(eid).cloned(),
594 route: world.route(eid).cloned(),
595 line: world.line(eid).cloned(),
596 patience: world.patience(eid).copied(),
597 preferences: world.preferences(eid).copied(),
598 access_control: world.access_control(eid).cloned(),
599 disabled: world.is_disabled(eid),
600 #[cfg(feature = "energy")]
601 energy_profile: world.energy_profile(eid).cloned(),
602 #[cfg(feature = "energy")]
603 energy_metrics: world.energy_metrics(eid).cloned(),
604 service_mode: world.service_mode(eid).copied(),
605 destination_queue: world.destination_queue(eid).cloned(),
606 car_calls: world.car_calls(eid).to_vec(),
607 })
608 .collect();
609
610 let groups: Vec<GroupSnapshot> = self
612 .groups()
613 .iter()
614 .map(|g| {
615 let lines: Vec<LineSnapshotInfo> = g
616 .lines()
617 .iter()
618 .filter_map(|li| {
619 let entity_index = id_to_index.get(&li.entity()).copied()?;
620 Some(LineSnapshotInfo {
621 entity_index,
622 elevator_indices: li
623 .elevators()
624 .iter()
625 .filter_map(|eid| id_to_index.get(eid).copied())
626 .collect(),
627 stop_indices: li
628 .serves()
629 .iter()
630 .filter_map(|eid| id_to_index.get(eid).copied())
631 .collect(),
632 })
633 })
634 .collect();
635 GroupSnapshot {
636 id: g.id(),
637 name: g.name().to_owned(),
638 elevator_indices: g
639 .elevator_entities()
640 .iter()
641 .filter_map(|eid| id_to_index.get(eid).copied())
642 .collect(),
643 stop_indices: g
644 .stop_entities()
645 .iter()
646 .filter_map(|eid| id_to_index.get(eid).copied())
647 .collect(),
648 strategy: self
649 .strategy_id(g.id())
650 .cloned()
651 .unwrap_or(crate::dispatch::BuiltinStrategy::Scan),
652 lines,
653 reposition: self.reposition_id(g.id()).cloned(),
654 hall_call_mode: g.hall_call_mode(),
655 ack_latency_ticks: g.ack_latency_ticks(),
656 }
657 })
658 .collect();
659
660 let stop_lookup: HashMap<StopId, usize> = self
662 .stop_lookup_iter()
663 .filter_map(|(sid, eid)| id_to_index.get(eid).map(|&idx| (*sid, idx)))
664 .collect();
665
666 WorldSnapshot {
667 tick: self.current_tick(),
668 dt: self.dt(),
669 entities,
670 groups,
671 stop_lookup,
672 metrics: self.metrics().clone(),
673 metric_tags: self
674 .world()
675 .resource::<MetricTags>()
676 .cloned()
677 .unwrap_or_default(),
678 extensions: self.world().serialize_extensions(),
679 ticks_per_second: 1.0 / self.dt(),
680 hall_calls: world.iter_hall_calls().cloned().collect(),
681 }
682 }
683
684 pub fn snapshot_bytes(&self) -> Result<Vec<u8>, crate::error::SimError> {
704 let envelope = SnapshotEnvelope {
705 magic: SNAPSHOT_MAGIC,
706 version: env!("CARGO_PKG_VERSION").to_owned(),
707 payload: self.snapshot(),
708 };
709 postcard::to_allocvec(&envelope)
710 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))
711 }
712
713 pub fn restore_bytes(
728 bytes: &[u8],
729 custom_strategy_factory: CustomStrategyFactory<'_>,
730 ) -> Result<Self, crate::error::SimError> {
731 let (envelope, tail): (SnapshotEnvelope, &[u8]) = postcard::take_from_bytes(bytes)
732 .map_err(|e| crate::error::SimError::SnapshotFormat(e.to_string()))?;
733 if !tail.is_empty() {
734 return Err(crate::error::SimError::SnapshotFormat(format!(
735 "trailing bytes: {} unread of {}",
736 tail.len(),
737 bytes.len()
738 )));
739 }
740 if envelope.magic != SNAPSHOT_MAGIC {
741 return Err(crate::error::SimError::SnapshotFormat(
742 "magic bytes do not match".to_string(),
743 ));
744 }
745 let current = env!("CARGO_PKG_VERSION");
746 if envelope.version != current {
747 return Err(crate::error::SimError::SnapshotVersion {
748 saved: envelope.version,
749 current: current.to_owned(),
750 });
751 }
752 envelope.payload.restore(custom_strategy_factory)
753 }
754}