1use crate::components::{
4 AccessControl, Elevator, ElevatorPhase, FloorPosition, Line, Orientation, Patience, Position,
5 Preferences, Rider, RiderPhase, Route, Stop, Velocity,
6};
7use crate::config::SimConfig;
8use crate::dispatch::{
9 BuiltinReposition, BuiltinStrategy, DispatchStrategy, ElevatorGroup, LineInfo,
10 RepositionStrategy,
11};
12use crate::door::DoorState;
13use crate::entity::EntityId;
14use crate::error::SimError;
15use crate::events::{Event, EventBus};
16use crate::hooks::{Phase, PhaseHooks};
17use crate::ids::GroupId;
18use crate::metrics::Metrics;
19use crate::rider_index::RiderIndex;
20use crate::stop::StopId;
21use crate::systems::PhaseContext;
22use crate::time::TimeAdapter;
23use crate::topology::TopologyGraph;
24use crate::world::World;
25use std::collections::{BTreeMap, HashMap, HashSet};
26use std::fmt;
27use std::sync::Mutex;
28
29#[derive(Debug, Clone)]
31pub struct ElevatorParams {
32 pub max_speed: f64,
34 pub acceleration: f64,
36 pub deceleration: f64,
38 pub weight_capacity: f64,
40 pub door_transition_ticks: u32,
42 pub door_open_ticks: u32,
44 pub restricted_stops: HashSet<EntityId>,
46 pub inspection_speed_factor: f64,
48}
49
50impl Default for ElevatorParams {
51 fn default() -> Self {
52 Self {
53 max_speed: 2.0,
54 acceleration: 1.5,
55 deceleration: 2.0,
56 weight_capacity: 800.0,
57 door_transition_ticks: 5,
58 door_open_ticks: 10,
59 restricted_stops: HashSet::new(),
60 inspection_speed_factor: 0.25,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct LineParams {
68 pub name: String,
70 pub group: GroupId,
72 pub orientation: Orientation,
74 pub min_position: f64,
76 pub max_position: f64,
78 pub position: Option<FloorPosition>,
80 pub max_cars: Option<usize>,
82}
83
84impl LineParams {
85 pub fn new(name: impl Into<String>, group: GroupId) -> Self {
88 Self {
89 name: name.into(),
90 group,
91 orientation: Orientation::default(),
92 min_position: 0.0,
93 max_position: 0.0,
94 position: None,
95 max_cars: None,
96 }
97 }
98}
99
100pub struct RiderBuilder<'a> {
115 sim: &'a mut Simulation,
117 origin: EntityId,
119 destination: EntityId,
121 weight: f64,
123 group: Option<GroupId>,
125 route: Option<Route>,
127 patience: Option<u64>,
129 preferences: Option<Preferences>,
131 access_control: Option<AccessControl>,
133}
134
135impl RiderBuilder<'_> {
136 #[must_use]
138 pub const fn weight(mut self, weight: f64) -> Self {
139 self.weight = weight;
140 self
141 }
142
143 #[must_use]
145 pub const fn group(mut self, group: GroupId) -> Self {
146 self.group = Some(group);
147 self
148 }
149
150 #[must_use]
152 pub fn route(mut self, route: Route) -> Self {
153 self.route = Some(route);
154 self
155 }
156
157 #[must_use]
159 pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
160 self.patience = Some(max_wait_ticks);
161 self
162 }
163
164 #[must_use]
166 pub const fn preferences(mut self, prefs: Preferences) -> Self {
167 self.preferences = Some(prefs);
168 self
169 }
170
171 #[must_use]
173 pub fn access_control(mut self, ac: AccessControl) -> Self {
174 self.access_control = Some(ac);
175 self
176 }
177
178 pub fn spawn(self) -> Result<EntityId, SimError> {
186 let route = if let Some(route) = self.route {
187 route
188 } else if let Some(group) = self.group {
189 if !self.sim.groups.iter().any(|g| g.id() == group) {
190 return Err(SimError::GroupNotFound(group));
191 }
192 Route::direct(self.origin, self.destination, group)
193 } else {
194 let matching: Vec<GroupId> = self
196 .sim
197 .groups
198 .iter()
199 .filter(|g| {
200 g.stop_entities().contains(&self.origin)
201 && g.stop_entities().contains(&self.destination)
202 })
203 .map(ElevatorGroup::id)
204 .collect();
205
206 match matching.len() {
207 0 => {
208 let origin_groups: Vec<GroupId> = self
209 .sim
210 .groups
211 .iter()
212 .filter(|g| g.stop_entities().contains(&self.origin))
213 .map(ElevatorGroup::id)
214 .collect();
215 let destination_groups: Vec<GroupId> = self
216 .sim
217 .groups
218 .iter()
219 .filter(|g| g.stop_entities().contains(&self.destination))
220 .map(ElevatorGroup::id)
221 .collect();
222 return Err(SimError::NoRoute {
223 origin: self.origin,
224 destination: self.destination,
225 origin_groups,
226 destination_groups,
227 });
228 }
229 1 => Route::direct(self.origin, self.destination, matching[0]),
230 _ => {
231 return Err(SimError::AmbiguousRoute {
232 origin: self.origin,
233 destination: self.destination,
234 groups: matching,
235 });
236 }
237 }
238 };
239
240 let eid = self
241 .sim
242 .spawn_rider_inner(self.origin, self.destination, self.weight, route);
243
244 if let Some(max_wait) = self.patience {
246 self.sim.world.set_patience(
247 eid,
248 Patience {
249 max_wait_ticks: max_wait,
250 waited_ticks: 0,
251 },
252 );
253 }
254 if let Some(prefs) = self.preferences {
255 self.sim.world.set_preferences(eid, prefs);
256 }
257 if let Some(ac) = self.access_control {
258 self.sim.world.set_access_control(eid, ac);
259 }
260
261 Ok(eid)
262 }
263}
264
265type TopologyResult = (
267 Vec<ElevatorGroup>,
268 BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
269 BTreeMap<GroupId, BuiltinStrategy>,
270);
271
272pub struct Simulation {
274 world: World,
276 events: EventBus,
278 pending_output: Vec<Event>,
280 tick: u64,
282 dt: f64,
284 groups: Vec<ElevatorGroup>,
286 stop_lookup: HashMap<StopId, EntityId>,
288 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
290 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
292 repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>>,
294 reposition_ids: BTreeMap<GroupId, BuiltinReposition>,
296 metrics: Metrics,
298 time: TimeAdapter,
300 hooks: PhaseHooks,
302 elevator_ids_buf: Vec<EntityId>,
304 topo_graph: Mutex<TopologyGraph>,
306 rider_index: RiderIndex,
308}
309
310impl Simulation {
311 pub fn new(
322 config: &SimConfig,
323 dispatch: impl DispatchStrategy + 'static,
324 ) -> Result<Self, SimError> {
325 let mut dispatchers = BTreeMap::new();
326 dispatchers.insert(GroupId(0), Box::new(dispatch) as Box<dyn DispatchStrategy>);
327 Self::new_with_hooks(config, dispatchers, PhaseHooks::default())
328 }
329
330 #[allow(clippy::too_many_lines)]
334 pub(crate) fn new_with_hooks(
335 config: &SimConfig,
336 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
337 hooks: PhaseHooks,
338 ) -> Result<Self, SimError> {
339 Self::validate_config(config)?;
340
341 let mut world = World::new();
342
343 let mut stop_lookup: HashMap<StopId, EntityId> = HashMap::new();
345 for sc in &config.building.stops {
346 let eid = world.spawn();
347 world.set_stop(
348 eid,
349 Stop {
350 name: sc.name.clone(),
351 position: sc.position,
352 },
353 );
354 world.set_position(eid, Position { value: sc.position });
355 stop_lookup.insert(sc.id, eid);
356 }
357
358 let mut sorted: Vec<(f64, EntityId)> = world
360 .iter_stops()
361 .map(|(eid, stop)| (stop.position, eid))
362 .collect();
363 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
364 world.insert_resource(crate::world::SortedStops(sorted));
365
366 let (groups, dispatchers, strategy_ids) = if let Some(line_configs) = &config.building.lines
367 {
368 Self::build_explicit_topology(
369 &mut world,
370 config,
371 line_configs,
372 &stop_lookup,
373 builder_dispatchers,
374 )
375 } else {
376 Self::build_legacy_topology(&mut world, config, &stop_lookup, builder_dispatchers)
377 };
378
379 let dt = 1.0 / config.simulation.ticks_per_second;
380
381 world.insert_resource(crate::tagged_metrics::MetricTags::default());
382
383 let line_tag_info: Vec<(EntityId, String, Vec<EntityId>)> = groups
386 .iter()
387 .flat_map(|group| {
388 group.lines().iter().filter_map(|li| {
389 let line_comp = world.line(li.entity())?;
390 Some((li.entity(), line_comp.name.clone(), li.elevators().to_vec()))
391 })
392 })
393 .collect();
394
395 if let Some(tags) = world.resource_mut::<crate::tagged_metrics::MetricTags>() {
397 for (line_eid, name, elevators) in &line_tag_info {
398 let tag = format!("line:{name}");
399 tags.tag(*line_eid, tag.clone());
400 for elev_eid in elevators {
401 tags.tag(*elev_eid, tag.clone());
402 }
403 }
404 }
405
406 let mut repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>> = BTreeMap::new();
408 let mut reposition_ids: BTreeMap<GroupId, BuiltinReposition> = BTreeMap::new();
409 if let Some(group_configs) = &config.building.groups {
410 for gc in group_configs {
411 if let Some(ref repo_id) = gc.reposition {
412 if let Some(strategy) = repo_id.instantiate() {
413 let gid = GroupId(gc.id);
414 repositioners.insert(gid, strategy);
415 reposition_ids.insert(gid, repo_id.clone());
416 }
417 }
418 }
419 }
420
421 Ok(Self {
422 world,
423 events: EventBus::default(),
424 pending_output: Vec::new(),
425 tick: 0,
426 dt,
427 groups,
428 stop_lookup,
429 dispatchers,
430 strategy_ids,
431 repositioners,
432 reposition_ids,
433 metrics: Metrics::new(),
434 time: TimeAdapter::new(config.simulation.ticks_per_second),
435 hooks,
436 elevator_ids_buf: Vec::new(),
437 topo_graph: Mutex::new(TopologyGraph::new()),
438 rider_index: RiderIndex::default(),
439 })
440 }
441
442 fn build_legacy_topology(
444 world: &mut World,
445 config: &SimConfig,
446 stop_lookup: &HashMap<StopId, EntityId>,
447 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
448 ) -> TopologyResult {
449 let all_stop_entities: Vec<EntityId> = stop_lookup.values().copied().collect();
450 let stop_positions: Vec<f64> = config.building.stops.iter().map(|s| s.position).collect();
451 let min_pos = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
452 let max_pos = stop_positions
453 .iter()
454 .copied()
455 .fold(f64::NEG_INFINITY, f64::max);
456
457 let default_line_eid = world.spawn();
458 world.set_line(
459 default_line_eid,
460 Line {
461 name: "Default".into(),
462 group: GroupId(0),
463 orientation: Orientation::Vertical,
464 position: None,
465 min_position: min_pos,
466 max_position: max_pos,
467 max_cars: None,
468 },
469 );
470
471 let mut elevator_entities = Vec::new();
472 for ec in &config.elevators {
473 let eid = world.spawn();
474 let start_pos = config
475 .building
476 .stops
477 .iter()
478 .find(|s| s.id == ec.starting_stop)
479 .map_or(0.0, |s| s.position);
480 world.set_position(eid, Position { value: start_pos });
481 world.set_velocity(eid, Velocity { value: 0.0 });
482 let restricted: HashSet<EntityId> = ec
483 .restricted_stops
484 .iter()
485 .filter_map(|sid| stop_lookup.get(sid).copied())
486 .collect();
487 world.set_elevator(
488 eid,
489 Elevator {
490 phase: ElevatorPhase::Idle,
491 door: DoorState::Closed,
492 max_speed: ec.max_speed,
493 acceleration: ec.acceleration,
494 deceleration: ec.deceleration,
495 weight_capacity: ec.weight_capacity,
496 current_load: 0.0,
497 riders: Vec::new(),
498 target_stop: None,
499 door_transition_ticks: ec.door_transition_ticks,
500 door_open_ticks: ec.door_open_ticks,
501 line: default_line_eid,
502 repositioning: false,
503 restricted_stops: restricted,
504 inspection_speed_factor: ec.inspection_speed_factor,
505 going_up: true,
506 going_down: true,
507 move_count: 0,
508 },
509 );
510 #[cfg(feature = "energy")]
511 if let Some(ref profile) = ec.energy_profile {
512 world.set_energy_profile(eid, profile.clone());
513 world.set_energy_metrics(eid, crate::energy::EnergyMetrics::default());
514 }
515 if let Some(mode) = ec.service_mode {
516 world.set_service_mode(eid, mode);
517 }
518 elevator_entities.push(eid);
519 }
520
521 let default_line_info =
522 LineInfo::new(default_line_eid, elevator_entities, all_stop_entities);
523
524 let group = ElevatorGroup::new(GroupId(0), "Default".into(), vec![default_line_info]);
525
526 let mut dispatchers = BTreeMap::new();
528 let dispatch = builder_dispatchers.into_iter().next().map_or_else(
529 || Box::new(crate::dispatch::scan::ScanDispatch::new()) as Box<dyn DispatchStrategy>,
530 |(_, d)| d,
531 );
532 dispatchers.insert(GroupId(0), dispatch);
533
534 let mut strategy_ids = BTreeMap::new();
535 strategy_ids.insert(GroupId(0), BuiltinStrategy::Scan);
536
537 (vec![group], dispatchers, strategy_ids)
538 }
539
540 #[allow(clippy::too_many_lines)]
542 fn build_explicit_topology(
543 world: &mut World,
544 config: &SimConfig,
545 line_configs: &[crate::config::LineConfig],
546 stop_lookup: &HashMap<StopId, EntityId>,
547 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
548 ) -> TopologyResult {
549 let mut line_map: HashMap<u32, (EntityId, LineInfo)> = HashMap::new();
551
552 for lc in line_configs {
553 let served_entities: Vec<EntityId> = lc
555 .serves
556 .iter()
557 .filter_map(|sid| stop_lookup.get(sid).copied())
558 .collect();
559
560 let stop_positions: Vec<f64> = lc
562 .serves
563 .iter()
564 .filter_map(|sid| {
565 config
566 .building
567 .stops
568 .iter()
569 .find(|s| s.id == *sid)
570 .map(|s| s.position)
571 })
572 .collect();
573 let auto_min = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
574 let auto_max = stop_positions
575 .iter()
576 .copied()
577 .fold(f64::NEG_INFINITY, f64::max);
578
579 let min_pos = lc.min_position.unwrap_or(auto_min);
580 let max_pos = lc.max_position.unwrap_or(auto_max);
581
582 let line_eid = world.spawn();
583 world.set_line(
586 line_eid,
587 Line {
588 name: lc.name.clone(),
589 group: GroupId(0),
590 orientation: lc.orientation,
591 position: lc.position,
592 min_position: min_pos,
593 max_position: max_pos,
594 max_cars: lc.max_cars,
595 },
596 );
597
598 let mut elevator_entities = Vec::new();
600 for ec in &lc.elevators {
601 let eid = world.spawn();
602 let start_pos = config
603 .building
604 .stops
605 .iter()
606 .find(|s| s.id == ec.starting_stop)
607 .map_or(0.0, |s| s.position);
608 world.set_position(eid, Position { value: start_pos });
609 world.set_velocity(eid, Velocity { value: 0.0 });
610 let restricted: HashSet<EntityId> = ec
611 .restricted_stops
612 .iter()
613 .filter_map(|sid| stop_lookup.get(sid).copied())
614 .collect();
615 world.set_elevator(
616 eid,
617 Elevator {
618 phase: ElevatorPhase::Idle,
619 door: DoorState::Closed,
620 max_speed: ec.max_speed,
621 acceleration: ec.acceleration,
622 deceleration: ec.deceleration,
623 weight_capacity: ec.weight_capacity,
624 current_load: 0.0,
625 riders: Vec::new(),
626 target_stop: None,
627 door_transition_ticks: ec.door_transition_ticks,
628 door_open_ticks: ec.door_open_ticks,
629 line: line_eid,
630 repositioning: false,
631 restricted_stops: restricted,
632 inspection_speed_factor: ec.inspection_speed_factor,
633 going_up: true,
634 going_down: true,
635 move_count: 0,
636 },
637 );
638 #[cfg(feature = "energy")]
639 if let Some(ref profile) = ec.energy_profile {
640 world.set_energy_profile(eid, profile.clone());
641 world.set_energy_metrics(eid, crate::energy::EnergyMetrics::default());
642 }
643 if let Some(mode) = ec.service_mode {
644 world.set_service_mode(eid, mode);
645 }
646 elevator_entities.push(eid);
647 }
648
649 let line_info = LineInfo::new(line_eid, elevator_entities, served_entities);
650 line_map.insert(lc.id, (line_eid, line_info));
651 }
652
653 let group_configs = config.building.groups.as_deref();
655 let mut groups = Vec::new();
656 let mut dispatchers = BTreeMap::new();
657 let mut strategy_ids = BTreeMap::new();
658
659 if let Some(gcs) = group_configs {
660 for gc in gcs {
661 let group_id = GroupId(gc.id);
662
663 let mut group_lines = Vec::new();
664
665 for &lid in &gc.lines {
666 if let Some((line_eid, li)) = line_map.get(&lid) {
667 if let Some(line_comp) = world.line_mut(*line_eid) {
669 line_comp.group = group_id;
670 }
671 group_lines.push(li.clone());
672 }
673 }
674
675 let group = ElevatorGroup::new(group_id, gc.name.clone(), group_lines);
676 groups.push(group);
677
678 let dispatch: Box<dyn DispatchStrategy> = gc
680 .dispatch
681 .instantiate()
682 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
683 dispatchers.insert(group_id, dispatch);
684 strategy_ids.insert(group_id, gc.dispatch.clone());
685 }
686 } else {
687 let group_id = GroupId(0);
689 let mut group_lines = Vec::new();
690
691 for (line_eid, li) in line_map.values() {
692 if let Some(line_comp) = world.line_mut(*line_eid) {
693 line_comp.group = group_id;
694 }
695 group_lines.push(li.clone());
696 }
697
698 let group = ElevatorGroup::new(group_id, "Default".into(), group_lines);
699 groups.push(group);
700
701 let dispatch: Box<dyn DispatchStrategy> =
702 Box::new(crate::dispatch::scan::ScanDispatch::new());
703 dispatchers.insert(group_id, dispatch);
704 strategy_ids.insert(group_id, BuiltinStrategy::Scan);
705 }
706
707 for (gid, d) in builder_dispatchers {
709 dispatchers.insert(gid, d);
710 }
711
712 (groups, dispatchers, strategy_ids)
713 }
714
715 #[allow(clippy::too_many_arguments)]
717 pub(crate) fn from_parts(
718 world: World,
719 tick: u64,
720 dt: f64,
721 groups: Vec<ElevatorGroup>,
722 stop_lookup: HashMap<StopId, EntityId>,
723 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
724 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
725 metrics: Metrics,
726 ticks_per_second: f64,
727 ) -> Self {
728 let mut rider_index = RiderIndex::default();
729 rider_index.rebuild(&world);
730 Self {
731 world,
732 events: EventBus::default(),
733 pending_output: Vec::new(),
734 tick,
735 dt,
736 groups,
737 stop_lookup,
738 dispatchers,
739 strategy_ids,
740 repositioners: BTreeMap::new(),
741 reposition_ids: BTreeMap::new(),
742 metrics,
743 time: TimeAdapter::new(ticks_per_second),
744 hooks: PhaseHooks::default(),
745 elevator_ids_buf: Vec::new(),
746 topo_graph: Mutex::new(TopologyGraph::new()),
747 rider_index,
748 }
749 }
750
751 pub(crate) fn validate_config(config: &SimConfig) -> Result<(), SimError> {
753 if config.building.stops.is_empty() {
754 return Err(SimError::InvalidConfig {
755 field: "building.stops",
756 reason: "at least one stop is required".into(),
757 });
758 }
759
760 let mut seen_ids = HashSet::new();
762 for stop in &config.building.stops {
763 if !seen_ids.insert(stop.id) {
764 return Err(SimError::InvalidConfig {
765 field: "building.stops",
766 reason: format!("duplicate {}", stop.id),
767 });
768 }
769 }
770
771 let stop_ids: HashSet<StopId> = config.building.stops.iter().map(|s| s.id).collect();
772
773 if let Some(line_configs) = &config.building.lines {
774 Self::validate_explicit_topology(line_configs, &stop_ids, &config.building)?;
776 } else {
777 Self::validate_legacy_elevators(&config.elevators, &config.building)?;
779 }
780
781 if config.simulation.ticks_per_second <= 0.0 {
782 return Err(SimError::InvalidConfig {
783 field: "simulation.ticks_per_second",
784 reason: format!(
785 "must be positive, got {}",
786 config.simulation.ticks_per_second
787 ),
788 });
789 }
790
791 Ok(())
792 }
793
794 fn validate_legacy_elevators(
796 elevators: &[crate::config::ElevatorConfig],
797 building: &crate::config::BuildingConfig,
798 ) -> Result<(), SimError> {
799 if elevators.is_empty() {
800 return Err(SimError::InvalidConfig {
801 field: "elevators",
802 reason: "at least one elevator is required".into(),
803 });
804 }
805
806 for elev in elevators {
807 Self::validate_elevator_config(elev, building)?;
808 }
809
810 Ok(())
811 }
812
813 fn validate_elevator_config(
815 elev: &crate::config::ElevatorConfig,
816 building: &crate::config::BuildingConfig,
817 ) -> Result<(), SimError> {
818 if elev.max_speed <= 0.0 {
819 return Err(SimError::InvalidConfig {
820 field: "elevators.max_speed",
821 reason: format!("must be positive, got {}", elev.max_speed),
822 });
823 }
824 if elev.acceleration <= 0.0 {
825 return Err(SimError::InvalidConfig {
826 field: "elevators.acceleration",
827 reason: format!("must be positive, got {}", elev.acceleration),
828 });
829 }
830 if elev.deceleration <= 0.0 {
831 return Err(SimError::InvalidConfig {
832 field: "elevators.deceleration",
833 reason: format!("must be positive, got {}", elev.deceleration),
834 });
835 }
836 if elev.weight_capacity <= 0.0 {
837 return Err(SimError::InvalidConfig {
838 field: "elevators.weight_capacity",
839 reason: format!("must be positive, got {}", elev.weight_capacity),
840 });
841 }
842 if elev.inspection_speed_factor <= 0.0 {
843 return Err(SimError::InvalidConfig {
844 field: "elevators.inspection_speed_factor",
845 reason: format!("must be positive, got {}", elev.inspection_speed_factor),
846 });
847 }
848 if !building.stops.iter().any(|s| s.id == elev.starting_stop) {
849 return Err(SimError::InvalidConfig {
850 field: "elevators.starting_stop",
851 reason: format!("references non-existent {}", elev.starting_stop),
852 });
853 }
854 Ok(())
855 }
856
857 fn validate_explicit_topology(
859 line_configs: &[crate::config::LineConfig],
860 stop_ids: &HashSet<StopId>,
861 building: &crate::config::BuildingConfig,
862 ) -> Result<(), SimError> {
863 let mut seen_line_ids = HashSet::new();
865 for lc in line_configs {
866 if !seen_line_ids.insert(lc.id) {
867 return Err(SimError::InvalidConfig {
868 field: "building.lines",
869 reason: format!("duplicate line id {}", lc.id),
870 });
871 }
872 }
873
874 for lc in line_configs {
876 for sid in &lc.serves {
877 if !stop_ids.contains(sid) {
878 return Err(SimError::InvalidConfig {
879 field: "building.lines.serves",
880 reason: format!("line {} references non-existent {}", lc.id, sid),
881 });
882 }
883 }
884 for ec in &lc.elevators {
886 Self::validate_elevator_config(ec, building)?;
887 }
888
889 if let Some(max) = lc.max_cars {
891 if lc.elevators.len() > max {
892 return Err(SimError::InvalidConfig {
893 field: "building.lines.max_cars",
894 reason: format!(
895 "line {} has {} elevators but max_cars is {max}",
896 lc.id,
897 lc.elevators.len()
898 ),
899 });
900 }
901 }
902 }
903
904 let has_elevator = line_configs.iter().any(|lc| !lc.elevators.is_empty());
906 if !has_elevator {
907 return Err(SimError::InvalidConfig {
908 field: "building.lines",
909 reason: "at least one line must have at least one elevator".into(),
910 });
911 }
912
913 let served: HashSet<StopId> = line_configs
915 .iter()
916 .flat_map(|lc| lc.serves.iter().copied())
917 .collect();
918 for sid in stop_ids {
919 if !served.contains(sid) {
920 return Err(SimError::InvalidConfig {
921 field: "building.lines",
922 reason: format!("orphaned stop {sid} not served by any line"),
923 });
924 }
925 }
926
927 if let Some(group_configs) = &building.groups {
929 let line_id_set: HashSet<u32> = line_configs.iter().map(|lc| lc.id).collect();
930
931 let mut seen_group_ids = HashSet::new();
932 for gc in group_configs {
933 if !seen_group_ids.insert(gc.id) {
934 return Err(SimError::InvalidConfig {
935 field: "building.groups",
936 reason: format!("duplicate group id {}", gc.id),
937 });
938 }
939 for &lid in &gc.lines {
940 if !line_id_set.contains(&lid) {
941 return Err(SimError::InvalidConfig {
942 field: "building.groups.lines",
943 reason: format!(
944 "group {} references non-existent line id {}",
945 gc.id, lid
946 ),
947 });
948 }
949 }
950 }
951
952 let referenced_line_ids: HashSet<u32> = group_configs
954 .iter()
955 .flat_map(|g| g.lines.iter().copied())
956 .collect();
957 for lc in line_configs {
958 if !referenced_line_ids.contains(&lc.id) {
959 return Err(SimError::InvalidConfig {
960 field: "building.lines",
961 reason: format!("line {} is not assigned to any group", lc.id),
962 });
963 }
964 }
965 }
966
967 Ok(())
968 }
969
970 #[must_use]
974 pub const fn world(&self) -> &World {
975 &self.world
976 }
977
978 pub const fn world_mut(&mut self) -> &mut World {
984 &mut self.world
985 }
986
987 #[must_use]
989 pub const fn current_tick(&self) -> u64 {
990 self.tick
991 }
992
993 #[must_use]
995 pub const fn dt(&self) -> f64 {
996 self.dt
997 }
998
999 #[must_use]
1001 pub const fn metrics(&self) -> &Metrics {
1002 &self.metrics
1003 }
1004
1005 #[must_use]
1007 pub const fn time(&self) -> &TimeAdapter {
1008 &self.time
1009 }
1010
1011 #[must_use]
1013 pub fn groups(&self) -> &[ElevatorGroup] {
1014 &self.groups
1015 }
1016
1017 #[must_use]
1019 pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
1020 self.stop_lookup.get(&id).copied()
1021 }
1022
1023 #[must_use]
1025 pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
1026 self.strategy_ids.get(&group)
1027 }
1028
1029 pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
1031 self.stop_lookup.iter()
1032 }
1033
1034 #[must_use]
1036 pub fn pending_events(&self) -> &[Event] {
1037 &self.pending_output
1038 }
1039
1040 pub fn set_dispatch(
1047 &mut self,
1048 group: GroupId,
1049 strategy: Box<dyn DispatchStrategy>,
1050 id: crate::dispatch::BuiltinStrategy,
1051 ) {
1052 self.dispatchers.insert(group, strategy);
1053 self.strategy_ids.insert(group, id);
1054 }
1055
1056 pub fn set_reposition(
1063 &mut self,
1064 group: GroupId,
1065 strategy: Box<dyn RepositionStrategy>,
1066 id: BuiltinReposition,
1067 ) {
1068 self.repositioners.insert(group, strategy);
1069 self.reposition_ids.insert(group, id);
1070 }
1071
1072 pub fn remove_reposition(&mut self, group: GroupId) {
1074 self.repositioners.remove(&group);
1075 self.reposition_ids.remove(&group);
1076 }
1077
1078 #[must_use]
1080 pub fn reposition_id(&self, group: GroupId) -> Option<&BuiltinReposition> {
1081 self.reposition_ids.get(&group)
1082 }
1083
1084 pub fn tag_entity(&mut self, id: EntityId, tag: impl Into<String>) {
1091 if let Some(tags) = self
1092 .world
1093 .resource_mut::<crate::tagged_metrics::MetricTags>()
1094 {
1095 tags.tag(id, tag);
1096 }
1097 }
1098
1099 pub fn untag_entity(&mut self, id: EntityId, tag: &str) {
1101 if let Some(tags) = self
1102 .world
1103 .resource_mut::<crate::tagged_metrics::MetricTags>()
1104 {
1105 tags.untag(id, tag);
1106 }
1107 }
1108
1109 #[must_use]
1111 pub fn metrics_for_tag(&self, tag: &str) -> Option<&crate::tagged_metrics::TaggedMetric> {
1112 self.world
1113 .resource::<crate::tagged_metrics::MetricTags>()
1114 .and_then(|tags| tags.metric(tag))
1115 }
1116
1117 pub fn all_tags(&self) -> Vec<&str> {
1119 self.world
1120 .resource::<crate::tagged_metrics::MetricTags>()
1121 .map_or_else(Vec::new, |tags| tags.all_tags().collect())
1122 }
1123
1124 pub const fn build_rider(
1140 &mut self,
1141 origin: EntityId,
1142 destination: EntityId,
1143 ) -> RiderBuilder<'_> {
1144 RiderBuilder {
1145 sim: self,
1146 origin,
1147 destination,
1148 weight: 75.0,
1149 group: None,
1150 route: None,
1151 patience: None,
1152 preferences: None,
1153 access_control: None,
1154 }
1155 }
1156
1157 pub fn build_rider_by_stop_id(
1174 &mut self,
1175 origin: StopId,
1176 destination: StopId,
1177 ) -> Result<RiderBuilder<'_>, SimError> {
1178 let origin_eid = self
1179 .stop_lookup
1180 .get(&origin)
1181 .copied()
1182 .ok_or(SimError::StopNotFound(origin))?;
1183 let dest_eid = self
1184 .stop_lookup
1185 .get(&destination)
1186 .copied()
1187 .ok_or(SimError::StopNotFound(destination))?;
1188 Ok(RiderBuilder {
1189 sim: self,
1190 origin: origin_eid,
1191 destination: dest_eid,
1192 weight: 75.0,
1193 group: None,
1194 route: None,
1195 patience: None,
1196 preferences: None,
1197 access_control: None,
1198 })
1199 }
1200
1201 pub fn spawn_rider(
1211 &mut self,
1212 origin: EntityId,
1213 destination: EntityId,
1214 weight: f64,
1215 ) -> Result<EntityId, SimError> {
1216 let matching: Vec<GroupId> = self
1217 .groups
1218 .iter()
1219 .filter(|g| {
1220 g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
1221 })
1222 .map(ElevatorGroup::id)
1223 .collect();
1224
1225 let group = match matching.len() {
1226 0 => {
1227 let origin_groups: Vec<GroupId> = self
1228 .groups
1229 .iter()
1230 .filter(|g| g.stop_entities().contains(&origin))
1231 .map(ElevatorGroup::id)
1232 .collect();
1233 let destination_groups: Vec<GroupId> = self
1234 .groups
1235 .iter()
1236 .filter(|g| g.stop_entities().contains(&destination))
1237 .map(ElevatorGroup::id)
1238 .collect();
1239 return Err(SimError::NoRoute {
1240 origin,
1241 destination,
1242 origin_groups,
1243 destination_groups,
1244 });
1245 }
1246 1 => matching[0],
1247 _ => {
1248 return Err(SimError::AmbiguousRoute {
1249 origin,
1250 destination,
1251 groups: matching,
1252 });
1253 }
1254 };
1255
1256 let route = Route::direct(origin, destination, group);
1257 Ok(self.spawn_rider_inner(origin, destination, weight, route))
1258 }
1259
1260 pub fn spawn_rider_with_route(
1271 &mut self,
1272 origin: EntityId,
1273 destination: EntityId,
1274 weight: f64,
1275 route: Route,
1276 ) -> Result<EntityId, SimError> {
1277 if self.world.stop(origin).is_none() {
1278 return Err(SimError::EntityNotFound(origin));
1279 }
1280 if let Some(leg) = route.current() {
1281 if leg.from != origin {
1282 return Err(SimError::InvalidState {
1283 entity: origin,
1284 reason: format!(
1285 "origin {origin:?} does not match route first leg from {:?}",
1286 leg.from
1287 ),
1288 });
1289 }
1290 }
1291 Ok(self.spawn_rider_inner(origin, destination, weight, route))
1292 }
1293
1294 fn spawn_rider_inner(
1296 &mut self,
1297 origin: EntityId,
1298 destination: EntityId,
1299 weight: f64,
1300 route: Route,
1301 ) -> EntityId {
1302 let eid = self.world.spawn();
1303 self.world.set_rider(
1304 eid,
1305 Rider {
1306 weight,
1307 phase: RiderPhase::Waiting,
1308 current_stop: Some(origin),
1309 spawn_tick: self.tick,
1310 board_tick: None,
1311 },
1312 );
1313 self.world.set_route(eid, route);
1314 self.rider_index.insert_waiting(origin, eid);
1315 self.events.emit(Event::RiderSpawned {
1316 rider: eid,
1317 origin,
1318 destination,
1319 tick: self.tick,
1320 });
1321
1322 let stop_tag = self
1324 .world
1325 .stop(origin)
1326 .map(|s| format!("stop:{}", s.name()));
1327
1328 if let Some(tags_res) = self
1330 .world
1331 .resource_mut::<crate::tagged_metrics::MetricTags>()
1332 {
1333 let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
1334 for tag in origin_tags {
1335 tags_res.tag(eid, tag);
1336 }
1337 if let Some(tag) = stop_tag {
1339 tags_res.tag(eid, tag);
1340 }
1341 }
1342
1343 eid
1344 }
1345
1346 pub fn spawn_rider_by_stop_id(
1366 &mut self,
1367 origin: StopId,
1368 destination: StopId,
1369 weight: f64,
1370 ) -> Result<EntityId, SimError> {
1371 let origin_eid = self
1372 .stop_lookup
1373 .get(&origin)
1374 .copied()
1375 .ok_or(SimError::StopNotFound(origin))?;
1376 let dest_eid = self
1377 .stop_lookup
1378 .get(&destination)
1379 .copied()
1380 .ok_or(SimError::StopNotFound(destination))?;
1381 self.spawn_rider(origin_eid, dest_eid, weight)
1382 }
1383
1384 pub fn spawn_rider_in_group(
1394 &mut self,
1395 origin: EntityId,
1396 destination: EntityId,
1397 weight: f64,
1398 group: GroupId,
1399 ) -> Result<EntityId, SimError> {
1400 if !self.groups.iter().any(|g| g.id() == group) {
1401 return Err(SimError::GroupNotFound(group));
1402 }
1403 let route = Route::direct(origin, destination, group);
1404 Ok(self.spawn_rider_inner(origin, destination, weight, route))
1405 }
1406
1407 pub fn spawn_rider_in_group_by_stop_id(
1414 &mut self,
1415 origin: StopId,
1416 destination: StopId,
1417 weight: f64,
1418 group: GroupId,
1419 ) -> Result<EntityId, SimError> {
1420 let origin_eid = self
1421 .stop_lookup
1422 .get(&origin)
1423 .copied()
1424 .ok_or(SimError::StopNotFound(origin))?;
1425 let dest_eid = self
1426 .stop_lookup
1427 .get(&destination)
1428 .copied()
1429 .ok_or(SimError::StopNotFound(destination))?;
1430 self.spawn_rider_in_group(origin_eid, dest_eid, weight, group)
1431 }
1432
1433 pub fn drain_events(&mut self) -> Vec<Event> {
1452 self.pending_output.extend(self.events.drain());
1454 std::mem::take(&mut self.pending_output)
1455 }
1456
1457 pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
1475 self.pending_output.extend(self.events.drain());
1477
1478 let mut matched = Vec::new();
1479 let mut remaining = Vec::new();
1480 for event in std::mem::take(&mut self.pending_output) {
1481 if predicate(&event) {
1482 matched.push(event);
1483 } else {
1484 remaining.push(event);
1485 }
1486 }
1487 self.pending_output = remaining;
1488 matched
1489 }
1490
1491 fn find_line(&self, line: EntityId) -> Result<(usize, usize), SimError> {
1495 self.groups
1496 .iter()
1497 .enumerate()
1498 .find_map(|(gi, g)| {
1499 g.lines()
1500 .iter()
1501 .position(|li| li.entity() == line)
1502 .map(|li_idx| (gi, li_idx))
1503 })
1504 .ok_or(SimError::LineNotFound(line))
1505 }
1506
1507 pub fn add_stop(
1517 &mut self,
1518 name: String,
1519 position: f64,
1520 line: EntityId,
1521 ) -> Result<EntityId, SimError> {
1522 let group_id = self
1523 .world
1524 .line(line)
1525 .map(|l| l.group)
1526 .ok_or(SimError::LineNotFound(line))?;
1527
1528 let (group_idx, line_idx) = self.find_line(line)?;
1529
1530 let eid = self.world.spawn();
1531 self.world.set_stop(eid, Stop { name, position });
1532 self.world.set_position(eid, Position { value: position });
1533
1534 self.groups[group_idx].lines_mut()[line_idx]
1536 .serves_mut()
1537 .push(eid);
1538
1539 self.groups[group_idx].push_stop(eid);
1541
1542 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
1544 let idx = sorted.0.partition_point(|&(p, _)| p < position);
1545 sorted.0.insert(idx, (position, eid));
1546 }
1547
1548 if let Ok(mut g) = self.topo_graph.lock() {
1549 g.mark_dirty();
1550 }
1551 self.events.emit(Event::StopAdded {
1552 stop: eid,
1553 line,
1554 group: group_id,
1555 tick: self.tick,
1556 });
1557 Ok(eid)
1558 }
1559
1560 pub fn add_elevator(
1566 &mut self,
1567 params: &ElevatorParams,
1568 line: EntityId,
1569 starting_position: f64,
1570 ) -> Result<EntityId, SimError> {
1571 let group_id = self
1572 .world
1573 .line(line)
1574 .map(|l| l.group)
1575 .ok_or(SimError::LineNotFound(line))?;
1576
1577 let (group_idx, line_idx) = self.find_line(line)?;
1578
1579 if let Some(max) = self.world.line(line).and_then(Line::max_cars) {
1581 let current_count = self.groups[group_idx].lines()[line_idx].elevators().len();
1582 if current_count >= max {
1583 return Err(SimError::InvalidConfig {
1584 field: "line.max_cars",
1585 reason: format!("line already has {current_count} cars (max {max})"),
1586 });
1587 }
1588 }
1589
1590 let eid = self.world.spawn();
1591 self.world.set_position(
1592 eid,
1593 Position {
1594 value: starting_position,
1595 },
1596 );
1597 self.world.set_velocity(eid, Velocity { value: 0.0 });
1598 self.world.set_elevator(
1599 eid,
1600 Elevator {
1601 phase: ElevatorPhase::Idle,
1602 door: DoorState::Closed,
1603 max_speed: params.max_speed,
1604 acceleration: params.acceleration,
1605 deceleration: params.deceleration,
1606 weight_capacity: params.weight_capacity,
1607 current_load: 0.0,
1608 riders: Vec::new(),
1609 target_stop: None,
1610 door_transition_ticks: params.door_transition_ticks,
1611 door_open_ticks: params.door_open_ticks,
1612 line,
1613 repositioning: false,
1614 restricted_stops: params.restricted_stops.clone(),
1615 inspection_speed_factor: params.inspection_speed_factor,
1616 going_up: true,
1617 going_down: true,
1618 move_count: 0,
1619 },
1620 );
1621 self.groups[group_idx].lines_mut()[line_idx]
1622 .elevators_mut()
1623 .push(eid);
1624 self.groups[group_idx].push_elevator(eid);
1625
1626 let line_name = self.world.line(line).map(|l| l.name.clone());
1628 if let Some(name) = line_name {
1629 if let Some(tags) = self
1630 .world
1631 .resource_mut::<crate::tagged_metrics::MetricTags>()
1632 {
1633 tags.tag(eid, format!("line:{name}"));
1634 }
1635 }
1636
1637 if let Ok(mut g) = self.topo_graph.lock() {
1638 g.mark_dirty();
1639 }
1640 self.events.emit(Event::ElevatorAdded {
1641 elevator: eid,
1642 line,
1643 group: group_id,
1644 tick: self.tick,
1645 });
1646 Ok(eid)
1647 }
1648
1649 pub fn add_line(&mut self, params: &LineParams) -> Result<EntityId, SimError> {
1657 let group_id = params.group;
1658 let group = self
1659 .groups
1660 .iter_mut()
1661 .find(|g| g.id() == group_id)
1662 .ok_or(SimError::GroupNotFound(group_id))?;
1663
1664 let line_tag = format!("line:{}", params.name);
1665
1666 let eid = self.world.spawn();
1667 self.world.set_line(
1668 eid,
1669 Line {
1670 name: params.name.clone(),
1671 group: group_id,
1672 orientation: params.orientation,
1673 position: params.position,
1674 min_position: params.min_position,
1675 max_position: params.max_position,
1676 max_cars: params.max_cars,
1677 },
1678 );
1679
1680 group
1681 .lines_mut()
1682 .push(LineInfo::new(eid, Vec::new(), Vec::new()));
1683
1684 if let Some(tags) = self
1686 .world
1687 .resource_mut::<crate::tagged_metrics::MetricTags>()
1688 {
1689 tags.tag(eid, line_tag);
1690 }
1691
1692 if let Ok(mut g) = self.topo_graph.lock() {
1693 g.mark_dirty();
1694 }
1695 self.events.emit(Event::LineAdded {
1696 line: eid,
1697 group: group_id,
1698 tick: self.tick,
1699 });
1700 Ok(eid)
1701 }
1702
1703 pub fn remove_line(&mut self, line: EntityId) -> Result<(), SimError> {
1713 let (group_idx, line_idx) = self.find_line(line)?;
1714
1715 let group_id = self.groups[group_idx].id();
1716
1717 let elevator_ids: Vec<EntityId> = self.groups[group_idx].lines()[line_idx]
1719 .elevators()
1720 .to_vec();
1721
1722 for eid in &elevator_ids {
1724 let _ = self.disable(*eid);
1726 }
1727
1728 self.groups[group_idx].lines_mut().remove(line_idx);
1730
1731 self.groups[group_idx].rebuild_caches();
1733
1734 self.world.remove_line(line);
1736
1737 if let Ok(mut g) = self.topo_graph.lock() {
1738 g.mark_dirty();
1739 }
1740 self.events.emit(Event::LineRemoved {
1741 line,
1742 group: group_id,
1743 tick: self.tick,
1744 });
1745 Ok(())
1746 }
1747
1748 pub fn remove_elevator(&mut self, elevator: EntityId) -> Result<(), SimError> {
1757 let line = self
1758 .world
1759 .elevator(elevator)
1760 .ok_or(SimError::EntityNotFound(elevator))?
1761 .line();
1762
1763 let _ = self.disable(elevator);
1765
1766 let mut group_id = GroupId(0);
1768 if let Ok((group_idx, line_idx)) = self.find_line(line) {
1769 self.groups[group_idx].lines_mut()[line_idx]
1770 .elevators_mut()
1771 .retain(|&e| e != elevator);
1772 self.groups[group_idx].rebuild_caches();
1773
1774 group_id = self.groups[group_idx].id();
1776 if let Some(dispatcher) = self.dispatchers.get_mut(&group_id) {
1777 dispatcher.notify_removed(elevator);
1778 }
1779 }
1780
1781 self.events.emit(Event::ElevatorRemoved {
1782 elevator,
1783 line,
1784 group: group_id,
1785 tick: self.tick,
1786 });
1787
1788 self.world.despawn(elevator);
1790
1791 if let Ok(mut g) = self.topo_graph.lock() {
1792 g.mark_dirty();
1793 }
1794 Ok(())
1795 }
1796
1797 pub fn remove_stop(&mut self, stop: EntityId) -> Result<(), SimError> {
1806 if self.world.stop(stop).is_none() {
1807 return Err(SimError::EntityNotFound(stop));
1808 }
1809
1810 let _ = self.disable(stop);
1812
1813 for group in &mut self.groups {
1815 for line_info in group.lines_mut() {
1816 line_info.serves_mut().retain(|&s| s != stop);
1817 }
1818 group.rebuild_caches();
1819 }
1820
1821 if let Some(sorted) = self.world.resource_mut::<crate::world::SortedStops>() {
1823 sorted.0.retain(|&(_, s)| s != stop);
1824 }
1825
1826 self.stop_lookup.retain(|_, &mut eid| eid != stop);
1828
1829 self.events.emit(Event::StopRemoved {
1830 stop,
1831 tick: self.tick,
1832 });
1833
1834 self.world.despawn(stop);
1836
1837 if let Ok(mut g) = self.topo_graph.lock() {
1838 g.mark_dirty();
1839 }
1840 Ok(())
1841 }
1842
1843 pub fn add_group(
1845 &mut self,
1846 name: impl Into<String>,
1847 dispatch: impl DispatchStrategy + 'static,
1848 ) -> GroupId {
1849 let next_id = self
1850 .groups
1851 .iter()
1852 .map(|g| g.id().0)
1853 .max()
1854 .map_or(0, |m| m + 1);
1855 let group_id = GroupId(next_id);
1856
1857 self.groups
1858 .push(ElevatorGroup::new(group_id, name.into(), Vec::new()));
1859
1860 self.dispatchers.insert(group_id, Box::new(dispatch));
1861 self.strategy_ids.insert(group_id, BuiltinStrategy::Scan);
1862 if let Ok(mut g) = self.topo_graph.lock() {
1863 g.mark_dirty();
1864 }
1865 group_id
1866 }
1867
1868 pub fn assign_line_to_group(
1875 &mut self,
1876 line: EntityId,
1877 new_group: GroupId,
1878 ) -> Result<GroupId, SimError> {
1879 let (old_group_idx, line_idx) = self.find_line(line)?;
1880
1881 if !self.groups.iter().any(|g| g.id() == new_group) {
1883 return Err(SimError::GroupNotFound(new_group));
1884 }
1885
1886 let old_group_id = self.groups[old_group_idx].id();
1887
1888 let line_info = self.groups[old_group_idx].lines_mut().remove(line_idx);
1890 self.groups[old_group_idx].rebuild_caches();
1891
1892 let new_group_idx = self
1897 .groups
1898 .iter()
1899 .position(|g| g.id() == new_group)
1900 .ok_or(SimError::GroupNotFound(new_group))?;
1901 self.groups[new_group_idx].lines_mut().push(line_info);
1902 self.groups[new_group_idx].rebuild_caches();
1903
1904 if let Some(line_comp) = self.world.line_mut(line) {
1906 line_comp.group = new_group;
1907 }
1908
1909 if let Ok(mut g) = self.topo_graph.lock() {
1910 g.mark_dirty();
1911 }
1912 self.events.emit(Event::LineReassigned {
1913 line,
1914 old_group: old_group_id,
1915 new_group,
1916 tick: self.tick,
1917 });
1918
1919 Ok(old_group_id)
1920 }
1921
1922 pub fn reassign_elevator_to_line(
1933 &mut self,
1934 elevator: EntityId,
1935 new_line: EntityId,
1936 ) -> Result<(), SimError> {
1937 let old_line = self
1938 .world
1939 .elevator(elevator)
1940 .ok_or(SimError::EntityNotFound(elevator))?
1941 .line();
1942
1943 if old_line == new_line {
1944 return Ok(());
1945 }
1946
1947 let (old_group_idx, old_line_idx) = self.find_line(old_line)?;
1949 let (new_group_idx, new_line_idx) = self.find_line(new_line)?;
1950
1951 if let Some(max) = self.world.line(new_line).and_then(Line::max_cars) {
1953 let current_count = self.groups[new_group_idx].lines()[new_line_idx]
1954 .elevators()
1955 .len();
1956 if current_count >= max {
1957 return Err(SimError::InvalidConfig {
1958 field: "line.max_cars",
1959 reason: format!("target line already has {current_count} cars (max {max})"),
1960 });
1961 }
1962 }
1963
1964 self.groups[old_group_idx].lines_mut()[old_line_idx]
1965 .elevators_mut()
1966 .retain(|&e| e != elevator);
1967 self.groups[new_group_idx].lines_mut()[new_line_idx]
1968 .elevators_mut()
1969 .push(elevator);
1970
1971 if let Some(car) = self.world.elevator_mut(elevator) {
1972 car.line = new_line;
1973 }
1974
1975 self.groups[old_group_idx].rebuild_caches();
1976 if new_group_idx != old_group_idx {
1977 self.groups[new_group_idx].rebuild_caches();
1978 }
1979
1980 if let Ok(mut g) = self.topo_graph.lock() {
1981 g.mark_dirty();
1982 }
1983
1984 self.events.emit(Event::ElevatorReassigned {
1985 elevator,
1986 old_line,
1987 new_line,
1988 tick: self.tick,
1989 });
1990
1991 Ok(())
1992 }
1993
1994 pub fn add_stop_to_line(&mut self, stop: EntityId, line: EntityId) -> Result<(), SimError> {
2001 if self.world.stop(stop).is_none() {
2003 return Err(SimError::EntityNotFound(stop));
2004 }
2005
2006 let (group_idx, line_idx) = self.find_line(line)?;
2007
2008 let li = &mut self.groups[group_idx].lines_mut()[line_idx];
2009 if !li.serves().contains(&stop) {
2010 li.serves_mut().push(stop);
2011 }
2012
2013 self.groups[group_idx].push_stop(stop);
2014
2015 if let Ok(mut g) = self.topo_graph.lock() {
2016 g.mark_dirty();
2017 }
2018 Ok(())
2019 }
2020
2021 pub fn remove_stop_from_line(
2027 &mut self,
2028 stop: EntityId,
2029 line: EntityId,
2030 ) -> Result<(), SimError> {
2031 let (group_idx, line_idx) = self.find_line(line)?;
2032
2033 self.groups[group_idx].lines_mut()[line_idx]
2034 .serves_mut()
2035 .retain(|&s| s != stop);
2036
2037 self.groups[group_idx].rebuild_caches();
2039
2040 if let Ok(mut g) = self.topo_graph.lock() {
2041 g.mark_dirty();
2042 }
2043 Ok(())
2044 }
2045
2046 #[must_use]
2050 pub fn all_lines(&self) -> Vec<EntityId> {
2051 self.groups
2052 .iter()
2053 .flat_map(|g| g.lines().iter().map(LineInfo::entity))
2054 .collect()
2055 }
2056
2057 #[must_use]
2059 pub fn line_count(&self) -> usize {
2060 self.groups.iter().map(|g| g.lines().len()).sum()
2061 }
2062
2063 #[must_use]
2065 pub fn lines_in_group(&self, group: GroupId) -> Vec<EntityId> {
2066 self.groups
2067 .iter()
2068 .find(|g| g.id() == group)
2069 .map_or_else(Vec::new, |g| {
2070 g.lines().iter().map(LineInfo::entity).collect()
2071 })
2072 }
2073
2074 #[must_use]
2076 pub fn elevators_on_line(&self, line: EntityId) -> Vec<EntityId> {
2077 self.groups
2078 .iter()
2079 .flat_map(ElevatorGroup::lines)
2080 .find(|li| li.entity() == line)
2081 .map_or_else(Vec::new, |li| li.elevators().to_vec())
2082 }
2083
2084 #[must_use]
2086 pub fn stops_served_by_line(&self, line: EntityId) -> Vec<EntityId> {
2087 self.groups
2088 .iter()
2089 .flat_map(ElevatorGroup::lines)
2090 .find(|li| li.entity() == line)
2091 .map_or_else(Vec::new, |li| li.serves().to_vec())
2092 }
2093
2094 #[must_use]
2096 pub fn line_for_elevator(&self, elevator: EntityId) -> Option<EntityId> {
2097 self.groups
2098 .iter()
2099 .flat_map(ElevatorGroup::lines)
2100 .find(|li| li.elevators().contains(&elevator))
2101 .map(LineInfo::entity)
2102 }
2103
2104 pub fn iter_repositioning_elevators(&self) -> impl Iterator<Item = EntityId> + '_ {
2106 self.world
2107 .iter_elevators()
2108 .filter_map(|(id, _pos, car)| if car.repositioning() { Some(id) } else { None })
2109 }
2110
2111 #[must_use]
2113 pub fn lines_serving_stop(&self, stop: EntityId) -> Vec<EntityId> {
2114 self.groups
2115 .iter()
2116 .flat_map(ElevatorGroup::lines)
2117 .filter(|li| li.serves().contains(&stop))
2118 .map(LineInfo::entity)
2119 .collect()
2120 }
2121
2122 #[must_use]
2124 pub fn groups_serving_stop(&self, stop: EntityId) -> Vec<GroupId> {
2125 self.groups
2126 .iter()
2127 .filter(|g| g.stop_entities().contains(&stop))
2128 .map(ElevatorGroup::id)
2129 .collect()
2130 }
2131
2132 fn ensure_graph_built(&self) {
2136 if let Ok(mut graph) = self.topo_graph.lock() {
2137 if graph.is_dirty() {
2138 graph.rebuild(&self.groups);
2139 }
2140 }
2141 }
2142
2143 pub fn reachable_stops_from(&self, stop: EntityId) -> Vec<EntityId> {
2145 self.ensure_graph_built();
2146 self.topo_graph
2147 .lock()
2148 .map_or_else(|_| Vec::new(), |g| g.reachable_stops_from(stop))
2149 }
2150
2151 pub fn transfer_points(&self) -> Vec<EntityId> {
2153 self.ensure_graph_built();
2154 TopologyGraph::transfer_points(&self.groups)
2155 }
2156
2157 pub fn shortest_route(&self, from: EntityId, to: EntityId) -> Option<Route> {
2159 self.ensure_graph_built();
2160 self.topo_graph
2161 .lock()
2162 .ok()
2163 .and_then(|g| g.shortest_route(from, to))
2164 }
2165
2166 pub fn load_extensions(&mut self) {
2179 if let Some(pending) = self
2180 .world
2181 .remove_resource::<crate::snapshot::PendingExtensions>()
2182 {
2183 self.world.deserialize_extensions(&pending.0);
2184 }
2185 }
2186
2187 fn group_from_route(&self, route: Option<&Route>) -> GroupId {
2194 if let Some(route) = route {
2195 for leg in route.legs.iter().skip(route.current_leg) {
2197 match leg.via {
2198 crate::components::TransportMode::Group(g) => return g,
2199 crate::components::TransportMode::Line(l) => {
2200 if let Some(line) = self.world.line(l) {
2201 return line.group();
2202 }
2203 }
2204 crate::components::TransportMode::Walk => {}
2205 }
2206 }
2207 }
2208 GroupId(0)
2209 }
2210
2211 pub fn reroute(&mut self, rider: EntityId, new_destination: EntityId) -> Result<(), SimError> {
2227 let r = self
2228 .world
2229 .rider(rider)
2230 .ok_or(SimError::EntityNotFound(rider))?;
2231
2232 if r.phase != RiderPhase::Waiting {
2233 return Err(SimError::InvalidState {
2234 entity: rider,
2235 reason: "can only reroute riders in Waiting phase".into(),
2236 });
2237 }
2238
2239 let origin = r.current_stop.ok_or_else(|| SimError::InvalidState {
2240 entity: rider,
2241 reason: "rider has no current stop for reroute".into(),
2242 })?;
2243
2244 let group = self.group_from_route(self.world.route(rider));
2245 self.world
2246 .set_route(rider, Route::direct(origin, new_destination, group));
2247
2248 self.events.emit(Event::RiderRerouted {
2249 rider,
2250 new_destination,
2251 tick: self.tick,
2252 });
2253
2254 Ok(())
2255 }
2256
2257 pub fn set_rider_route(&mut self, rider: EntityId, route: Route) -> Result<(), SimError> {
2263 if self.world.rider(rider).is_none() {
2264 return Err(SimError::EntityNotFound(rider));
2265 }
2266 self.world.set_route(rider, route);
2267 Ok(())
2268 }
2269
2270 pub fn settle_rider(&mut self, id: EntityId) -> Result<(), SimError> {
2285 let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
2286
2287 let old_phase = rider.phase;
2288 match old_phase {
2289 RiderPhase::Arrived | RiderPhase::Abandoned => {}
2290 _ => {
2291 return Err(SimError::InvalidState {
2292 entity: id,
2293 reason: format!(
2294 "cannot settle rider in {old_phase} phase, expected Arrived or Abandoned"
2295 ),
2296 });
2297 }
2298 }
2299
2300 let stop = rider.current_stop.ok_or_else(|| SimError::InvalidState {
2301 entity: id,
2302 reason: "rider has no current_stop".into(),
2303 })?;
2304
2305 if old_phase == RiderPhase::Abandoned {
2307 self.rider_index.remove_abandoned(stop, id);
2308 }
2309 self.rider_index.insert_resident(stop, id);
2310
2311 if let Some(r) = self.world.rider_mut(id) {
2312 r.phase = RiderPhase::Resident;
2313 }
2314
2315 self.metrics.record_settle();
2316 self.events.emit(Event::RiderSettled {
2317 rider: id,
2318 stop,
2319 tick: self.tick,
2320 });
2321 Ok(())
2322 }
2323
2324 pub fn reroute_rider(&mut self, id: EntityId, route: Route) -> Result<(), SimError> {
2337 let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
2338
2339 if rider.phase != RiderPhase::Resident {
2340 return Err(SimError::InvalidState {
2341 entity: id,
2342 reason: format!(
2343 "cannot reroute rider in {} phase, expected Resident",
2344 rider.phase
2345 ),
2346 });
2347 }
2348
2349 let stop = rider.current_stop.ok_or_else(|| SimError::InvalidState {
2350 entity: id,
2351 reason: "resident rider has no current_stop".into(),
2352 })?;
2353
2354 let new_destination = route
2355 .final_destination()
2356 .ok_or_else(|| SimError::InvalidState {
2357 entity: id,
2358 reason: "route has no legs".into(),
2359 })?;
2360
2361 if let Some(leg) = route.current() {
2363 if leg.from != stop {
2364 return Err(SimError::InvalidState {
2365 entity: id,
2366 reason: format!(
2367 "route origin {:?} does not match rider current_stop {:?}",
2368 leg.from, stop
2369 ),
2370 });
2371 }
2372 }
2373
2374 self.rider_index.remove_resident(stop, id);
2375 self.rider_index.insert_waiting(stop, id);
2376
2377 if let Some(r) = self.world.rider_mut(id) {
2378 r.phase = RiderPhase::Waiting;
2379 }
2380 self.world.set_route(id, route);
2381
2382 if let Some(p) = self.world.patience_mut(id) {
2384 p.waited_ticks = 0;
2385 }
2386
2387 self.metrics.record_reroute();
2388 self.events.emit(Event::RiderRerouted {
2389 rider: id,
2390 new_destination,
2391 tick: self.tick,
2392 });
2393 Ok(())
2394 }
2395
2396 pub fn despawn_rider(&mut self, id: EntityId) -> Result<(), SimError> {
2409 let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
2410
2411 if let Some(stop) = rider.current_stop {
2413 match rider.phase {
2414 RiderPhase::Waiting => self.rider_index.remove_waiting(stop, id),
2415 RiderPhase::Resident => self.rider_index.remove_resident(stop, id),
2416 RiderPhase::Abandoned => self.rider_index.remove_abandoned(stop, id),
2417 _ => {} }
2419 }
2420
2421 if let Some(tags) = self
2422 .world
2423 .resource_mut::<crate::tagged_metrics::MetricTags>()
2424 {
2425 tags.remove_entity(id);
2426 }
2427
2428 self.world.despawn(id);
2429
2430 self.events.emit(Event::RiderDespawned {
2431 rider: id,
2432 tick: self.tick,
2433 });
2434 Ok(())
2435 }
2436
2437 pub fn set_rider_access(
2449 &mut self,
2450 rider: EntityId,
2451 allowed_stops: HashSet<EntityId>,
2452 ) -> Result<(), SimError> {
2453 if self.world.rider(rider).is_none() {
2454 return Err(SimError::EntityNotFound(rider));
2455 }
2456 self.world
2457 .set_access_control(rider, crate::components::AccessControl::new(allowed_stops));
2458 Ok(())
2459 }
2460
2461 pub fn set_elevator_restricted_stops(
2471 &mut self,
2472 elevator: EntityId,
2473 restricted_stops: HashSet<EntityId>,
2474 ) -> Result<(), SimError> {
2475 let car = self
2476 .world
2477 .elevator_mut(elevator)
2478 .ok_or(SimError::EntityNotFound(elevator))?;
2479 car.restricted_stops = restricted_stops;
2480 Ok(())
2481 }
2482
2483 pub fn residents_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
2487 self.rider_index.residents_at(stop).iter().copied()
2488 }
2489
2490 #[must_use]
2492 pub fn resident_count_at(&self, stop: EntityId) -> usize {
2493 self.rider_index.resident_count_at(stop)
2494 }
2495
2496 pub fn waiting_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
2498 self.rider_index.waiting_at(stop).iter().copied()
2499 }
2500
2501 #[must_use]
2503 pub fn waiting_count_at(&self, stop: EntityId) -> usize {
2504 self.rider_index.waiting_count_at(stop)
2505 }
2506
2507 pub fn abandoned_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
2509 self.rider_index.abandoned_at(stop).iter().copied()
2510 }
2511
2512 #[must_use]
2514 pub fn abandoned_count_at(&self, stop: EntityId) -> usize {
2515 self.rider_index.abandoned_count_at(stop)
2516 }
2517
2518 #[must_use]
2522 pub fn riders_on(&self, elevator: EntityId) -> &[EntityId] {
2523 self.world
2524 .elevator(elevator)
2525 .map_or(&[], |car| car.riders())
2526 }
2527
2528 #[must_use]
2532 pub fn occupancy(&self, elevator: EntityId) -> usize {
2533 self.world
2534 .elevator(elevator)
2535 .map_or(0, |car| car.riders().len())
2536 }
2537
2538 pub fn disable(&mut self, id: EntityId) -> Result<(), SimError> {
2557 if !self.world.is_alive(id) {
2558 return Err(SimError::EntityNotFound(id));
2559 }
2560 if let Some(car) = self.world.elevator(id) {
2562 let rider_ids = car.riders.clone();
2563 let pos = self.world.position(id).map_or(0.0, |p| p.value);
2564 let nearest_stop = self.world.find_nearest_stop(pos);
2565
2566 for rid in &rider_ids {
2567 if let Some(r) = self.world.rider_mut(*rid) {
2568 r.phase = RiderPhase::Waiting;
2569 r.current_stop = nearest_stop;
2570 r.board_tick = None;
2571 }
2572 if let Some(stop) = nearest_stop {
2573 self.rider_index.insert_waiting(stop, *rid);
2574 self.events.emit(Event::RiderEjected {
2575 rider: *rid,
2576 elevator: id,
2577 stop,
2578 tick: self.tick,
2579 });
2580 }
2581 }
2582
2583 let had_load = self
2584 .world
2585 .elevator(id)
2586 .is_some_and(|c| c.current_load > 0.0);
2587 let capacity = self.world.elevator(id).map(|c| c.weight_capacity);
2588 if let Some(car) = self.world.elevator_mut(id) {
2589 car.riders.clear();
2590 car.current_load = 0.0;
2591 car.phase = ElevatorPhase::Idle;
2592 car.target_stop = None;
2593 }
2594 if had_load {
2595 if let Some(cap) = capacity {
2596 self.events.emit(Event::CapacityChanged {
2597 elevator: id,
2598 current_load: ordered_float::OrderedFloat(0.0),
2599 capacity: ordered_float::OrderedFloat(cap),
2600 tick: self.tick,
2601 });
2602 }
2603 }
2604 }
2605 if let Some(vel) = self.world.velocity_mut(id) {
2606 vel.value = 0.0;
2607 }
2608
2609 if self.world.stop(id).is_some() {
2611 self.invalidate_routes_for_stop(id);
2612 }
2613
2614 self.world.disable(id);
2615 self.events.emit(Event::EntityDisabled {
2616 entity: id,
2617 tick: self.tick,
2618 });
2619 Ok(())
2620 }
2621
2622 pub fn enable(&mut self, id: EntityId) -> Result<(), SimError> {
2631 if !self.world.is_alive(id) {
2632 return Err(SimError::EntityNotFound(id));
2633 }
2634 self.world.enable(id);
2635 self.events.emit(Event::EntityEnabled {
2636 entity: id,
2637 tick: self.tick,
2638 });
2639 Ok(())
2640 }
2641
2642 fn invalidate_routes_for_stop(&mut self, disabled_stop: EntityId) {
2647 use crate::events::RouteInvalidReason;
2648
2649 let group_stops: Vec<EntityId> = self
2651 .groups
2652 .iter()
2653 .filter(|g| g.stop_entities().contains(&disabled_stop))
2654 .flat_map(|g| g.stop_entities().iter().copied())
2655 .filter(|&s| s != disabled_stop && !self.world.is_disabled(s))
2656 .collect();
2657
2658 let rider_ids: Vec<EntityId> = self.world.rider_ids();
2661 for rid in rider_ids {
2662 let is_waiting = self
2663 .world
2664 .rider(rid)
2665 .is_some_and(|r| r.phase == RiderPhase::Waiting);
2666
2667 if !is_waiting {
2668 continue;
2669 }
2670
2671 let references_stop = self.world.route(rid).is_some_and(|route| {
2672 route
2673 .legs
2674 .iter()
2675 .skip(route.current_leg)
2676 .any(|leg| leg.to == disabled_stop || leg.from == disabled_stop)
2677 });
2678
2679 if !references_stop {
2680 continue;
2681 }
2682
2683 let rider_current_stop = self.world.rider(rid).and_then(|r| r.current_stop);
2685
2686 let disabled_stop_pos = self.world.stop(disabled_stop).map_or(0.0, |s| s.position);
2687
2688 let alternative = group_stops
2689 .iter()
2690 .filter(|&&s| Some(s) != rider_current_stop)
2691 .filter_map(|&s| {
2692 self.world
2693 .stop(s)
2694 .map(|stop| (s, (stop.position - disabled_stop_pos).abs()))
2695 })
2696 .min_by(|a, b| a.1.total_cmp(&b.1))
2697 .map(|(s, _)| s);
2698
2699 if let Some(alt_stop) = alternative {
2700 let origin = rider_current_stop.unwrap_or(alt_stop);
2702 let group = self.group_from_route(self.world.route(rid));
2703 self.world
2704 .set_route(rid, Route::direct(origin, alt_stop, group));
2705 self.events.emit(Event::RouteInvalidated {
2706 rider: rid,
2707 affected_stop: disabled_stop,
2708 reason: RouteInvalidReason::StopDisabled,
2709 tick: self.tick,
2710 });
2711 } else {
2712 let abandon_stop = rider_current_stop.unwrap_or(disabled_stop);
2714 self.events.emit(Event::RouteInvalidated {
2715 rider: rid,
2716 affected_stop: disabled_stop,
2717 reason: RouteInvalidReason::NoAlternative,
2718 tick: self.tick,
2719 });
2720 if let Some(r) = self.world.rider_mut(rid) {
2721 r.phase = RiderPhase::Abandoned;
2722 }
2723 if let Some(stop) = rider_current_stop {
2724 self.rider_index.remove_waiting(stop, rid);
2725 self.rider_index.insert_abandoned(stop, rid);
2726 }
2727 self.events.emit(Event::RiderAbandoned {
2728 rider: rid,
2729 stop: abandon_stop,
2730 tick: self.tick,
2731 });
2732 }
2733 }
2734 }
2735
2736 #[must_use]
2738 pub fn is_disabled(&self, id: EntityId) -> bool {
2739 self.world.is_disabled(id)
2740 }
2741
2742 #[must_use]
2755 pub fn is_elevator(&self, id: EntityId) -> bool {
2756 self.world.elevator(id).is_some()
2757 }
2758
2759 #[must_use]
2761 pub fn is_rider(&self, id: EntityId) -> bool {
2762 self.world.rider(id).is_some()
2763 }
2764
2765 #[must_use]
2767 pub fn is_stop(&self, id: EntityId) -> bool {
2768 self.world.stop(id).is_some()
2769 }
2770
2771 #[must_use]
2784 pub fn idle_elevator_count(&self) -> usize {
2785 self.world.iter_idle_elevators().count()
2786 }
2787
2788 #[must_use]
2799 pub fn elevator_load(&self, id: EntityId) -> Option<f64> {
2800 self.world.elevator(id).map(|e| e.current_load)
2801 }
2802
2803 #[must_use]
2808 pub fn elevator_going_up(&self, id: EntityId) -> Option<bool> {
2809 self.world.elevator(id).map(Elevator::going_up)
2810 }
2811
2812 #[must_use]
2817 pub fn elevator_going_down(&self, id: EntityId) -> Option<bool> {
2818 self.world.elevator(id).map(Elevator::going_down)
2819 }
2820
2821 #[must_use]
2825 pub fn elevator_move_count(&self, id: EntityId) -> Option<u64> {
2826 self.world.elevator(id).map(Elevator::move_count)
2827 }
2828
2829 #[must_use]
2841 pub fn elevators_in_phase(&self, phase: ElevatorPhase) -> usize {
2842 self.world
2843 .iter_elevators()
2844 .filter(|(id, _, e)| e.phase() == phase && !self.world.is_disabled(*id))
2845 .count()
2846 }
2847
2848 pub fn set_service_mode(
2858 &mut self,
2859 elevator: EntityId,
2860 mode: crate::components::ServiceMode,
2861 ) -> Result<(), SimError> {
2862 if self.world.elevator(elevator).is_none() {
2863 return Err(SimError::EntityNotFound(elevator));
2864 }
2865 let old = self
2866 .world
2867 .service_mode(elevator)
2868 .copied()
2869 .unwrap_or_default();
2870 if old == mode {
2871 return Ok(());
2872 }
2873 self.world.set_service_mode(elevator, mode);
2874 self.events.emit(Event::ServiceModeChanged {
2875 elevator,
2876 from: old,
2877 to: mode,
2878 tick: self.tick,
2879 });
2880 Ok(())
2881 }
2882
2883 #[must_use]
2885 pub fn service_mode(&self, elevator: EntityId) -> crate::components::ServiceMode {
2886 self.world
2887 .service_mode(elevator)
2888 .copied()
2889 .unwrap_or_default()
2890 }
2891
2892 #[must_use]
2896 pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
2897 &self.dispatchers
2898 }
2899
2900 pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
2902 &mut self.dispatchers
2903 }
2904
2905 pub const fn events_mut(&mut self) -> &mut EventBus {
2907 &mut self.events
2908 }
2909
2910 pub const fn metrics_mut(&mut self) -> &mut Metrics {
2912 &mut self.metrics
2913 }
2914
2915 #[must_use]
2917 pub const fn phase_context(&self) -> PhaseContext {
2918 PhaseContext {
2919 tick: self.tick,
2920 dt: self.dt,
2921 }
2922 }
2923
2924 pub fn run_advance_transient(&mut self) {
2926 self.hooks
2927 .run_before(Phase::AdvanceTransient, &mut self.world);
2928 for group in &self.groups {
2929 self.hooks
2930 .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
2931 }
2932 let ctx = self.phase_context();
2933 crate::systems::advance_transient::run(
2934 &mut self.world,
2935 &mut self.events,
2936 &ctx,
2937 &mut self.rider_index,
2938 );
2939 for group in &self.groups {
2940 self.hooks
2941 .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
2942 }
2943 self.hooks
2944 .run_after(Phase::AdvanceTransient, &mut self.world);
2945 }
2946
2947 pub fn run_dispatch(&mut self) {
2949 self.hooks.run_before(Phase::Dispatch, &mut self.world);
2950 for group in &self.groups {
2951 self.hooks
2952 .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
2953 }
2954 let ctx = self.phase_context();
2955 crate::systems::dispatch::run(
2956 &mut self.world,
2957 &mut self.events,
2958 &ctx,
2959 &self.groups,
2960 &mut self.dispatchers,
2961 &self.rider_index,
2962 );
2963 for group in &self.groups {
2964 self.hooks
2965 .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
2966 }
2967 self.hooks.run_after(Phase::Dispatch, &mut self.world);
2968 }
2969
2970 pub fn run_movement(&mut self) {
2972 self.hooks.run_before(Phase::Movement, &mut self.world);
2973 for group in &self.groups {
2974 self.hooks
2975 .run_before_group(Phase::Movement, group.id(), &mut self.world);
2976 }
2977 let ctx = self.phase_context();
2978 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
2979 crate::systems::movement::run(
2980 &mut self.world,
2981 &mut self.events,
2982 &ctx,
2983 &self.elevator_ids_buf,
2984 &mut self.metrics,
2985 );
2986 for group in &self.groups {
2987 self.hooks
2988 .run_after_group(Phase::Movement, group.id(), &mut self.world);
2989 }
2990 self.hooks.run_after(Phase::Movement, &mut self.world);
2991 }
2992
2993 pub fn run_doors(&mut self) {
2995 self.hooks.run_before(Phase::Doors, &mut self.world);
2996 for group in &self.groups {
2997 self.hooks
2998 .run_before_group(Phase::Doors, group.id(), &mut self.world);
2999 }
3000 let ctx = self.phase_context();
3001 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
3002 crate::systems::doors::run(
3003 &mut self.world,
3004 &mut self.events,
3005 &ctx,
3006 &self.elevator_ids_buf,
3007 );
3008 for group in &self.groups {
3009 self.hooks
3010 .run_after_group(Phase::Doors, group.id(), &mut self.world);
3011 }
3012 self.hooks.run_after(Phase::Doors, &mut self.world);
3013 }
3014
3015 pub fn run_loading(&mut self) {
3017 self.hooks.run_before(Phase::Loading, &mut self.world);
3018 for group in &self.groups {
3019 self.hooks
3020 .run_before_group(Phase::Loading, group.id(), &mut self.world);
3021 }
3022 let ctx = self.phase_context();
3023 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
3024 crate::systems::loading::run(
3025 &mut self.world,
3026 &mut self.events,
3027 &ctx,
3028 &self.elevator_ids_buf,
3029 &mut self.rider_index,
3030 );
3031 for group in &self.groups {
3032 self.hooks
3033 .run_after_group(Phase::Loading, group.id(), &mut self.world);
3034 }
3035 self.hooks.run_after(Phase::Loading, &mut self.world);
3036 }
3037
3038 pub fn run_reposition(&mut self) {
3044 if self.repositioners.is_empty() {
3045 return;
3046 }
3047 self.hooks.run_before(Phase::Reposition, &mut self.world);
3048 for group in &self.groups {
3050 if self.repositioners.contains_key(&group.id()) {
3051 self.hooks
3052 .run_before_group(Phase::Reposition, group.id(), &mut self.world);
3053 }
3054 }
3055 let ctx = self.phase_context();
3056 crate::systems::reposition::run(
3057 &mut self.world,
3058 &mut self.events,
3059 &ctx,
3060 &self.groups,
3061 &mut self.repositioners,
3062 );
3063 for group in &self.groups {
3064 if self.repositioners.contains_key(&group.id()) {
3065 self.hooks
3066 .run_after_group(Phase::Reposition, group.id(), &mut self.world);
3067 }
3068 }
3069 self.hooks.run_after(Phase::Reposition, &mut self.world);
3070 }
3071
3072 #[cfg(feature = "energy")]
3074 fn run_energy(&mut self) {
3075 let ctx = self.phase_context();
3076 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
3077 crate::systems::energy::run(
3078 &mut self.world,
3079 &mut self.events,
3080 &ctx,
3081 &self.elevator_ids_buf,
3082 );
3083 }
3084
3085 pub fn run_metrics(&mut self) {
3087 self.hooks.run_before(Phase::Metrics, &mut self.world);
3088 for group in &self.groups {
3089 self.hooks
3090 .run_before_group(Phase::Metrics, group.id(), &mut self.world);
3091 }
3092 let ctx = self.phase_context();
3093 crate::systems::metrics::run(
3094 &mut self.world,
3095 &self.events,
3096 &mut self.metrics,
3097 &ctx,
3098 &self.groups,
3099 );
3100 for group in &self.groups {
3101 self.hooks
3102 .run_after_group(Phase::Metrics, group.id(), &mut self.world);
3103 }
3104 self.hooks.run_after(Phase::Metrics, &mut self.world);
3105 }
3106
3107 pub fn add_before_hook(
3112 &mut self,
3113 phase: Phase,
3114 hook: impl Fn(&mut World) + Send + Sync + 'static,
3115 ) {
3116 self.hooks.add_before(phase, Box::new(hook));
3117 }
3118
3119 pub fn add_after_hook(
3124 &mut self,
3125 phase: Phase,
3126 hook: impl Fn(&mut World) + Send + Sync + 'static,
3127 ) {
3128 self.hooks.add_after(phase, Box::new(hook));
3129 }
3130
3131 pub fn add_before_group_hook(
3133 &mut self,
3134 phase: Phase,
3135 group: GroupId,
3136 hook: impl Fn(&mut World) + Send + Sync + 'static,
3137 ) {
3138 self.hooks.add_before_group(phase, group, Box::new(hook));
3139 }
3140
3141 pub fn add_after_group_hook(
3143 &mut self,
3144 phase: Phase,
3145 group: GroupId,
3146 hook: impl Fn(&mut World) + Send + Sync + 'static,
3147 ) {
3148 self.hooks.add_after_group(phase, group, Box::new(hook));
3149 }
3150
3151 pub fn advance_tick(&mut self) {
3156 self.pending_output.extend(self.events.drain());
3157 self.tick += 1;
3158 }
3159
3160 pub fn step(&mut self) {
3174 self.run_advance_transient();
3175 self.run_dispatch();
3176 self.run_reposition();
3177 self.run_movement();
3178 self.run_doors();
3179 self.run_loading();
3180 #[cfg(feature = "energy")]
3181 self.run_energy();
3182 self.run_metrics();
3183 self.advance_tick();
3184 }
3185}
3186
3187impl fmt::Debug for Simulation {
3188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3189 f.debug_struct("Simulation")
3190 .field("tick", &self.tick)
3191 .field("dt", &self.dt)
3192 .field("groups", &self.groups.len())
3193 .field("entities", &self.world.entity_count())
3194 .finish_non_exhaustive()
3195 }
3196}