1use std::collections::{BTreeMap, HashMap, HashSet};
15use std::sync::Mutex;
16
17use crate::components::{Elevator, ElevatorPhase, Line, Orientation, Position, Stop, Velocity};
18use crate::config::SimConfig;
19use crate::dispatch::{
20 BuiltinReposition, BuiltinStrategy, DispatchStrategy, ElevatorGroup, LineInfo,
21 RepositionStrategy,
22};
23use crate::door::DoorState;
24use crate::entity::EntityId;
25use crate::error::SimError;
26use crate::events::EventBus;
27use crate::hooks::{Phase, PhaseHooks};
28use crate::ids::GroupId;
29use crate::metrics::Metrics;
30use crate::rider_index::RiderIndex;
31use crate::stop::StopId;
32use crate::time::TimeAdapter;
33use crate::topology::TopologyGraph;
34use crate::world::World;
35
36use super::Simulation;
37
38type TopologyResult = (
40 Vec<ElevatorGroup>,
41 BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
42 BTreeMap<GroupId, BuiltinStrategy>,
43);
44
45pub(super) fn validate_elevator_physics(
51 max_speed: f64,
52 acceleration: f64,
53 deceleration: f64,
54 weight_capacity: f64,
55 inspection_speed_factor: f64,
56 door_transition_ticks: u32,
57 door_open_ticks: u32,
58) -> Result<(), SimError> {
59 if !max_speed.is_finite() || max_speed <= 0.0 {
60 return Err(SimError::InvalidConfig {
61 field: "elevators.max_speed",
62 reason: format!("must be finite and positive, got {max_speed}"),
63 });
64 }
65 if !acceleration.is_finite() || acceleration <= 0.0 {
66 return Err(SimError::InvalidConfig {
67 field: "elevators.acceleration",
68 reason: format!("must be finite and positive, got {acceleration}"),
69 });
70 }
71 if !deceleration.is_finite() || deceleration <= 0.0 {
72 return Err(SimError::InvalidConfig {
73 field: "elevators.deceleration",
74 reason: format!("must be finite and positive, got {deceleration}"),
75 });
76 }
77 if !weight_capacity.is_finite() || weight_capacity <= 0.0 {
78 return Err(SimError::InvalidConfig {
79 field: "elevators.weight_capacity",
80 reason: format!("must be finite and positive, got {weight_capacity}"),
81 });
82 }
83 if !inspection_speed_factor.is_finite() || inspection_speed_factor <= 0.0 {
84 return Err(SimError::InvalidConfig {
85 field: "elevators.inspection_speed_factor",
86 reason: format!("must be finite and positive, got {inspection_speed_factor}"),
87 });
88 }
89 if door_transition_ticks == 0 {
90 return Err(SimError::InvalidConfig {
91 field: "elevators.door_transition_ticks",
92 reason: "must be > 0".into(),
93 });
94 }
95 if door_open_ticks == 0 {
96 return Err(SimError::InvalidConfig {
97 field: "elevators.door_open_ticks",
98 reason: "must be > 0".into(),
99 });
100 }
101 Ok(())
102}
103
104impl Simulation {
105 pub fn new(
116 config: &SimConfig,
117 dispatch: impl DispatchStrategy + 'static,
118 ) -> Result<Self, SimError> {
119 let mut dispatchers = BTreeMap::new();
120 dispatchers.insert(GroupId(0), Box::new(dispatch) as Box<dyn DispatchStrategy>);
121 Self::new_with_hooks(config, dispatchers, PhaseHooks::default())
122 }
123
124 #[allow(clippy::too_many_lines)]
128 pub(crate) fn new_with_hooks(
129 config: &SimConfig,
130 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
131 hooks: PhaseHooks,
132 ) -> Result<Self, SimError> {
133 Self::validate_config(config)?;
134
135 let mut world = World::new();
136
137 let mut stop_lookup: HashMap<StopId, EntityId> = HashMap::new();
139 for sc in &config.building.stops {
140 let eid = world.spawn();
141 world.set_stop(
142 eid,
143 Stop {
144 name: sc.name.clone(),
145 position: sc.position,
146 },
147 );
148 world.set_position(eid, Position { value: sc.position });
149 stop_lookup.insert(sc.id, eid);
150 }
151
152 let mut sorted: Vec<(f64, EntityId)> = world
154 .iter_stops()
155 .map(|(eid, stop)| (stop.position, eid))
156 .collect();
157 sorted.sort_by(|a, b| a.0.total_cmp(&b.0));
158 world.insert_resource(crate::world::SortedStops(sorted));
159
160 let (groups, dispatchers, strategy_ids) = if let Some(line_configs) = &config.building.lines
161 {
162 Self::build_explicit_topology(
163 &mut world,
164 config,
165 line_configs,
166 &stop_lookup,
167 builder_dispatchers,
168 )
169 } else {
170 Self::build_legacy_topology(&mut world, config, &stop_lookup, builder_dispatchers)
171 };
172
173 let dt = 1.0 / config.simulation.ticks_per_second;
174
175 world.insert_resource(crate::tagged_metrics::MetricTags::default());
176
177 let line_tag_info: Vec<(EntityId, String, Vec<EntityId>)> = groups
180 .iter()
181 .flat_map(|group| {
182 group.lines().iter().filter_map(|li| {
183 let line_comp = world.line(li.entity())?;
184 Some((li.entity(), line_comp.name.clone(), li.elevators().to_vec()))
185 })
186 })
187 .collect();
188
189 if let Some(tags) = world.resource_mut::<crate::tagged_metrics::MetricTags>() {
191 for (line_eid, name, elevators) in &line_tag_info {
192 let tag = format!("line:{name}");
193 tags.tag(*line_eid, tag.clone());
194 for elev_eid in elevators {
195 tags.tag(*elev_eid, tag.clone());
196 }
197 }
198 }
199
200 let mut repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>> = BTreeMap::new();
202 let mut reposition_ids: BTreeMap<GroupId, BuiltinReposition> = BTreeMap::new();
203 if let Some(group_configs) = &config.building.groups {
204 for gc in group_configs {
205 if let Some(ref repo_id) = gc.reposition
206 && let Some(strategy) = repo_id.instantiate()
207 {
208 let gid = GroupId(gc.id);
209 repositioners.insert(gid, strategy);
210 reposition_ids.insert(gid, repo_id.clone());
211 }
212 }
213 }
214
215 Ok(Self {
216 world,
217 events: EventBus::default(),
218 pending_output: Vec::new(),
219 tick: 0,
220 dt,
221 groups,
222 stop_lookup,
223 dispatchers,
224 strategy_ids,
225 repositioners,
226 reposition_ids,
227 metrics: Metrics::new(),
228 time: TimeAdapter::new(config.simulation.ticks_per_second),
229 hooks,
230 elevator_ids_buf: Vec::new(),
231 reposition_buf: Vec::new(),
232 topo_graph: Mutex::new(TopologyGraph::new()),
233 rider_index: RiderIndex::default(),
234 tick_in_progress: false,
235 })
236 }
237
238 fn spawn_elevator_entity(
244 world: &mut World,
245 ec: &crate::config::ElevatorConfig,
246 line: EntityId,
247 stop_lookup: &HashMap<StopId, EntityId>,
248 start_pos_lookup: &[crate::stop::StopConfig],
249 ) -> EntityId {
250 let eid = world.spawn();
251 let start_pos = start_pos_lookup
252 .iter()
253 .find(|s| s.id == ec.starting_stop)
254 .map_or(0.0, |s| s.position);
255 world.set_position(eid, Position { value: start_pos });
256 world.set_velocity(eid, Velocity { value: 0.0 });
257 let restricted: HashSet<EntityId> = ec
258 .restricted_stops
259 .iter()
260 .filter_map(|sid| stop_lookup.get(sid).copied())
261 .collect();
262 world.set_elevator(
263 eid,
264 Elevator {
265 phase: ElevatorPhase::Idle,
266 door: DoorState::Closed,
267 max_speed: ec.max_speed,
268 acceleration: ec.acceleration,
269 deceleration: ec.deceleration,
270 weight_capacity: ec.weight_capacity,
271 current_load: crate::components::Weight::ZERO,
272 riders: Vec::new(),
273 target_stop: None,
274 door_transition_ticks: ec.door_transition_ticks,
275 door_open_ticks: ec.door_open_ticks,
276 line,
277 repositioning: false,
278 restricted_stops: restricted,
279 inspection_speed_factor: ec.inspection_speed_factor,
280 going_up: true,
281 going_down: true,
282 move_count: 0,
283 door_command_queue: Vec::new(),
284 manual_target_velocity: None,
285 },
286 );
287 #[cfg(feature = "energy")]
288 if let Some(ref profile) = ec.energy_profile {
289 world.set_energy_profile(eid, profile.clone());
290 world.set_energy_metrics(eid, crate::energy::EnergyMetrics::default());
291 }
292 if let Some(mode) = ec.service_mode {
293 world.set_service_mode(eid, mode);
294 }
295 world.set_destination_queue(eid, crate::components::DestinationQueue::new());
296 eid
297 }
298
299 fn build_legacy_topology(
301 world: &mut World,
302 config: &SimConfig,
303 stop_lookup: &HashMap<StopId, EntityId>,
304 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
305 ) -> TopologyResult {
306 let all_stop_entities: Vec<EntityId> = stop_lookup.values().copied().collect();
307 let stop_positions: Vec<f64> = config.building.stops.iter().map(|s| s.position).collect();
308 let min_pos = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
309 let max_pos = stop_positions
310 .iter()
311 .copied()
312 .fold(f64::NEG_INFINITY, f64::max);
313
314 let default_line_eid = world.spawn();
315 world.set_line(
316 default_line_eid,
317 Line {
318 name: "Default".into(),
319 group: GroupId(0),
320 orientation: Orientation::Vertical,
321 position: None,
322 min_position: min_pos,
323 max_position: max_pos,
324 max_cars: None,
325 },
326 );
327
328 let mut elevator_entities = Vec::new();
329 for ec in &config.elevators {
330 let eid = Self::spawn_elevator_entity(
331 world,
332 ec,
333 default_line_eid,
334 stop_lookup,
335 &config.building.stops,
336 );
337 elevator_entities.push(eid);
338 }
339
340 let default_line_info =
341 LineInfo::new(default_line_eid, elevator_entities, all_stop_entities);
342
343 let group = ElevatorGroup::new(GroupId(0), "Default".into(), vec![default_line_info]);
344
345 let mut dispatchers = BTreeMap::new();
352 let mut strategy_ids = BTreeMap::new();
353 let user_dispatcher = builder_dispatchers
354 .into_iter()
355 .find_map(|(gid, d)| if gid == GroupId(0) { Some(d) } else { None });
356 if let Some(d) = user_dispatcher {
357 dispatchers.insert(GroupId(0), d);
358 } else {
359 dispatchers.insert(
360 GroupId(0),
361 Box::new(crate::dispatch::scan::ScanDispatch::new()) as Box<dyn DispatchStrategy>,
362 );
363 }
364 strategy_ids.insert(GroupId(0), BuiltinStrategy::Scan);
370
371 (vec![group], dispatchers, strategy_ids)
372 }
373
374 #[allow(clippy::too_many_lines)]
376 fn build_explicit_topology(
377 world: &mut World,
378 config: &SimConfig,
379 line_configs: &[crate::config::LineConfig],
380 stop_lookup: &HashMap<StopId, EntityId>,
381 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
382 ) -> TopologyResult {
383 let mut line_map: HashMap<u32, (EntityId, LineInfo)> = HashMap::new();
385
386 for lc in line_configs {
387 let served_entities: Vec<EntityId> = lc
389 .serves
390 .iter()
391 .filter_map(|sid| stop_lookup.get(sid).copied())
392 .collect();
393
394 let stop_positions: Vec<f64> = lc
396 .serves
397 .iter()
398 .filter_map(|sid| {
399 config
400 .building
401 .stops
402 .iter()
403 .find(|s| s.id == *sid)
404 .map(|s| s.position)
405 })
406 .collect();
407 let auto_min = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
408 let auto_max = stop_positions
409 .iter()
410 .copied()
411 .fold(f64::NEG_INFINITY, f64::max);
412
413 let min_pos = lc.min_position.unwrap_or(auto_min);
414 let max_pos = lc.max_position.unwrap_or(auto_max);
415
416 let line_eid = world.spawn();
417 world.set_line(
420 line_eid,
421 Line {
422 name: lc.name.clone(),
423 group: GroupId(0),
424 orientation: lc.orientation,
425 position: lc.position,
426 min_position: min_pos,
427 max_position: max_pos,
428 max_cars: lc.max_cars,
429 },
430 );
431
432 let mut elevator_entities = Vec::new();
434 for ec in &lc.elevators {
435 let eid = Self::spawn_elevator_entity(
436 world,
437 ec,
438 line_eid,
439 stop_lookup,
440 &config.building.stops,
441 );
442 elevator_entities.push(eid);
443 }
444
445 let line_info = LineInfo::new(line_eid, elevator_entities, served_entities);
446 line_map.insert(lc.id, (line_eid, line_info));
447 }
448
449 let group_configs = config.building.groups.as_deref();
451 let mut groups = Vec::new();
452 let mut dispatchers = BTreeMap::new();
453 let mut strategy_ids = BTreeMap::new();
454
455 if let Some(gcs) = group_configs {
456 for gc in gcs {
457 let group_id = GroupId(gc.id);
458
459 let mut group_lines = Vec::new();
460
461 for &lid in &gc.lines {
462 if let Some((line_eid, li)) = line_map.get(&lid) {
463 if let Some(line_comp) = world.line_mut(*line_eid) {
465 line_comp.group = group_id;
466 }
467 group_lines.push(li.clone());
468 }
469 }
470
471 let mut group = ElevatorGroup::new(group_id, gc.name.clone(), group_lines);
472 if let Some(mode) = gc.hall_call_mode {
473 group.set_hall_call_mode(mode);
474 }
475 if let Some(ticks) = gc.ack_latency_ticks {
476 group.set_ack_latency_ticks(ticks);
477 }
478 groups.push(group);
479
480 let dispatch: Box<dyn DispatchStrategy> = gc
482 .dispatch
483 .instantiate()
484 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
485 dispatchers.insert(group_id, dispatch);
486 strategy_ids.insert(group_id, gc.dispatch.clone());
487 }
488 } else {
489 let group_id = GroupId(0);
491 let mut group_lines = Vec::new();
492
493 for (line_eid, li) in line_map.values() {
494 if let Some(line_comp) = world.line_mut(*line_eid) {
495 line_comp.group = group_id;
496 }
497 group_lines.push(li.clone());
498 }
499
500 let group = ElevatorGroup::new(group_id, "Default".into(), group_lines);
501 groups.push(group);
502
503 let dispatch: Box<dyn DispatchStrategy> =
504 Box::new(crate::dispatch::scan::ScanDispatch::new());
505 dispatchers.insert(group_id, dispatch);
506 strategy_ids.insert(group_id, BuiltinStrategy::Scan);
507 }
508
509 for (gid, d) in builder_dispatchers {
516 dispatchers.insert(gid, d);
517 strategy_ids
524 .entry(gid)
525 .or_insert_with(|| BuiltinStrategy::Custom("user-supplied".into()));
526 }
527
528 (groups, dispatchers, strategy_ids)
529 }
530
531 #[allow(clippy::too_many_arguments)]
533 pub(crate) fn from_parts(
534 world: World,
535 tick: u64,
536 dt: f64,
537 groups: Vec<ElevatorGroup>,
538 stop_lookup: HashMap<StopId, EntityId>,
539 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
540 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
541 metrics: Metrics,
542 ticks_per_second: f64,
543 ) -> Self {
544 let mut rider_index = RiderIndex::default();
545 rider_index.rebuild(&world);
546 Self {
547 world,
548 events: EventBus::default(),
549 pending_output: Vec::new(),
550 tick,
551 dt,
552 groups,
553 stop_lookup,
554 dispatchers,
555 strategy_ids,
556 repositioners: BTreeMap::new(),
557 reposition_ids: BTreeMap::new(),
558 metrics,
559 time: TimeAdapter::new(ticks_per_second),
560 hooks: PhaseHooks::default(),
561 elevator_ids_buf: Vec::new(),
562 reposition_buf: Vec::new(),
563 topo_graph: Mutex::new(TopologyGraph::new()),
564 rider_index,
565 tick_in_progress: false,
566 }
567 }
568
569 pub(crate) fn validate_config(config: &SimConfig) -> Result<(), SimError> {
571 if config.building.stops.is_empty() {
572 return Err(SimError::InvalidConfig {
573 field: "building.stops",
574 reason: "at least one stop is required".into(),
575 });
576 }
577
578 let mut seen_ids = HashSet::new();
580 for stop in &config.building.stops {
581 if !seen_ids.insert(stop.id) {
582 return Err(SimError::InvalidConfig {
583 field: "building.stops",
584 reason: format!("duplicate {}", stop.id),
585 });
586 }
587 if !stop.position.is_finite() {
588 return Err(SimError::InvalidConfig {
589 field: "building.stops.position",
590 reason: format!("{} has non-finite position {}", stop.id, stop.position),
591 });
592 }
593 }
594
595 let stop_ids: HashSet<StopId> = config.building.stops.iter().map(|s| s.id).collect();
596
597 if let Some(line_configs) = &config.building.lines {
598 Self::validate_explicit_topology(line_configs, &stop_ids, &config.building)?;
600 } else {
601 Self::validate_legacy_elevators(&config.elevators, &config.building)?;
603 }
604
605 if !config.simulation.ticks_per_second.is_finite()
606 || config.simulation.ticks_per_second <= 0.0
607 {
608 return Err(SimError::InvalidConfig {
609 field: "simulation.ticks_per_second",
610 reason: format!(
611 "must be finite and positive, got {}",
612 config.simulation.ticks_per_second
613 ),
614 });
615 }
616
617 Self::validate_passenger_spawning(&config.passenger_spawning)?;
618
619 Ok(())
620 }
621
622 fn validate_passenger_spawning(
627 spawn: &crate::config::PassengerSpawnConfig,
628 ) -> Result<(), SimError> {
629 let (lo, hi) = spawn.weight_range;
630 if !lo.is_finite() || !hi.is_finite() {
631 return Err(SimError::InvalidConfig {
632 field: "passenger_spawning.weight_range",
633 reason: format!("both endpoints must be finite, got ({lo}, {hi})"),
634 });
635 }
636 if lo < 0.0 || hi < 0.0 {
637 return Err(SimError::InvalidConfig {
638 field: "passenger_spawning.weight_range",
639 reason: format!("both endpoints must be non-negative, got ({lo}, {hi})"),
640 });
641 }
642 if lo > hi {
643 return Err(SimError::InvalidConfig {
644 field: "passenger_spawning.weight_range",
645 reason: format!("min must be <= max, got ({lo}, {hi})"),
646 });
647 }
648 if spawn.mean_interval_ticks == 0 {
649 return Err(SimError::InvalidConfig {
650 field: "passenger_spawning.mean_interval_ticks",
651 reason: "must be > 0; mean_interval_ticks=0 burst-fires \
652 every catch-up tick"
653 .into(),
654 });
655 }
656 Ok(())
657 }
658
659 fn validate_legacy_elevators(
661 elevators: &[crate::config::ElevatorConfig],
662 building: &crate::config::BuildingConfig,
663 ) -> Result<(), SimError> {
664 if elevators.is_empty() {
665 return Err(SimError::InvalidConfig {
666 field: "elevators",
667 reason: "at least one elevator is required".into(),
668 });
669 }
670
671 for elev in elevators {
672 Self::validate_elevator_config(elev, building)?;
673 }
674
675 Ok(())
676 }
677
678 fn validate_elevator_config(
680 elev: &crate::config::ElevatorConfig,
681 building: &crate::config::BuildingConfig,
682 ) -> Result<(), SimError> {
683 validate_elevator_physics(
684 elev.max_speed.value(),
685 elev.acceleration.value(),
686 elev.deceleration.value(),
687 elev.weight_capacity.value(),
688 elev.inspection_speed_factor,
689 elev.door_transition_ticks,
690 elev.door_open_ticks,
691 )?;
692 if !building.stops.iter().any(|s| s.id == elev.starting_stop) {
693 return Err(SimError::InvalidConfig {
694 field: "elevators.starting_stop",
695 reason: format!("references non-existent {}", elev.starting_stop),
696 });
697 }
698 Ok(())
699 }
700
701 fn validate_explicit_topology(
703 line_configs: &[crate::config::LineConfig],
704 stop_ids: &HashSet<StopId>,
705 building: &crate::config::BuildingConfig,
706 ) -> Result<(), SimError> {
707 let mut seen_line_ids = HashSet::new();
709 for lc in line_configs {
710 if !seen_line_ids.insert(lc.id) {
711 return Err(SimError::InvalidConfig {
712 field: "building.lines",
713 reason: format!("duplicate line id {}", lc.id),
714 });
715 }
716 }
717
718 for lc in line_configs {
720 if lc.serves.is_empty() {
721 return Err(SimError::InvalidConfig {
722 field: "building.lines.serves",
723 reason: format!("line {} has no stops", lc.id),
724 });
725 }
726 for sid in &lc.serves {
727 if !stop_ids.contains(sid) {
728 return Err(SimError::InvalidConfig {
729 field: "building.lines.serves",
730 reason: format!("line {} references non-existent {}", lc.id, sid),
731 });
732 }
733 }
734 for ec in &lc.elevators {
736 Self::validate_elevator_config(ec, building)?;
737 }
738
739 if let Some(max) = lc.max_cars
741 && lc.elevators.len() > max
742 {
743 return Err(SimError::InvalidConfig {
744 field: "building.lines.max_cars",
745 reason: format!(
746 "line {} has {} elevators but max_cars is {max}",
747 lc.id,
748 lc.elevators.len()
749 ),
750 });
751 }
752 }
753
754 let has_elevator = line_configs.iter().any(|lc| !lc.elevators.is_empty());
756 if !has_elevator {
757 return Err(SimError::InvalidConfig {
758 field: "building.lines",
759 reason: "at least one line must have at least one elevator".into(),
760 });
761 }
762
763 let served: HashSet<StopId> = line_configs
765 .iter()
766 .flat_map(|lc| lc.serves.iter().copied())
767 .collect();
768 for sid in stop_ids {
769 if !served.contains(sid) {
770 return Err(SimError::InvalidConfig {
771 field: "building.lines",
772 reason: format!("orphaned stop {sid} not served by any line"),
773 });
774 }
775 }
776
777 if let Some(group_configs) = &building.groups {
779 let line_id_set: HashSet<u32> = line_configs.iter().map(|lc| lc.id).collect();
780
781 let mut seen_group_ids = HashSet::new();
782 for gc in group_configs {
783 if !seen_group_ids.insert(gc.id) {
784 return Err(SimError::InvalidConfig {
785 field: "building.groups",
786 reason: format!("duplicate group id {}", gc.id),
787 });
788 }
789 for &lid in &gc.lines {
790 if !line_id_set.contains(&lid) {
791 return Err(SimError::InvalidConfig {
792 field: "building.groups.lines",
793 reason: format!(
794 "group {} references non-existent line id {}",
795 gc.id, lid
796 ),
797 });
798 }
799 }
800 }
801
802 let referenced_line_ids: HashSet<u32> = group_configs
804 .iter()
805 .flat_map(|g| g.lines.iter().copied())
806 .collect();
807 for lc in line_configs {
808 if !referenced_line_ids.contains(&lc.id) {
809 return Err(SimError::InvalidConfig {
810 field: "building.lines",
811 reason: format!("line {} is not assigned to any group", lc.id),
812 });
813 }
814 }
815 }
816
817 Ok(())
818 }
819
820 pub fn set_dispatch(
827 &mut self,
828 group: GroupId,
829 strategy: Box<dyn DispatchStrategy>,
830 id: crate::dispatch::BuiltinStrategy,
831 ) {
832 self.dispatchers.insert(group, strategy);
833 self.strategy_ids.insert(group, id);
834 }
835
836 pub fn set_reposition(
843 &mut self,
844 group: GroupId,
845 strategy: Box<dyn RepositionStrategy>,
846 id: BuiltinReposition,
847 ) {
848 self.repositioners.insert(group, strategy);
849 self.reposition_ids.insert(group, id);
850 }
851
852 pub fn remove_reposition(&mut self, group: GroupId) {
854 self.repositioners.remove(&group);
855 self.reposition_ids.remove(&group);
856 }
857
858 #[must_use]
860 pub fn reposition_id(&self, group: GroupId) -> Option<&BuiltinReposition> {
861 self.reposition_ids.get(&group)
862 }
863
864 pub fn add_before_hook(
871 &mut self,
872 phase: Phase,
873 hook: impl Fn(&mut World) + Send + Sync + 'static,
874 ) {
875 self.hooks.add_before(phase, Box::new(hook));
876 }
877
878 pub fn add_after_hook(
883 &mut self,
884 phase: Phase,
885 hook: impl Fn(&mut World) + Send + Sync + 'static,
886 ) {
887 self.hooks.add_after(phase, Box::new(hook));
888 }
889
890 pub fn add_before_group_hook(
892 &mut self,
893 phase: Phase,
894 group: GroupId,
895 hook: impl Fn(&mut World) + Send + Sync + 'static,
896 ) {
897 self.hooks.add_before_group(phase, group, Box::new(hook));
898 }
899
900 pub fn add_after_group_hook(
902 &mut self,
903 phase: Phase,
904 group: GroupId,
905 hook: impl Fn(&mut World) + Send + Sync + 'static,
906 ) {
907 self.hooks.add_after_group(phase, group, Box::new(hook));
908 }
909}