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 })
235 }
236
237 fn spawn_elevator_entity(
243 world: &mut World,
244 ec: &crate::config::ElevatorConfig,
245 line: EntityId,
246 stop_lookup: &HashMap<StopId, EntityId>,
247 start_pos_lookup: &[crate::stop::StopConfig],
248 ) -> EntityId {
249 let eid = world.spawn();
250 let start_pos = start_pos_lookup
251 .iter()
252 .find(|s| s.id == ec.starting_stop)
253 .map_or(0.0, |s| s.position);
254 world.set_position(eid, Position { value: start_pos });
255 world.set_velocity(eid, Velocity { value: 0.0 });
256 let restricted: HashSet<EntityId> = ec
257 .restricted_stops
258 .iter()
259 .filter_map(|sid| stop_lookup.get(sid).copied())
260 .collect();
261 world.set_elevator(
262 eid,
263 Elevator {
264 phase: ElevatorPhase::Idle,
265 door: DoorState::Closed,
266 max_speed: ec.max_speed,
267 acceleration: ec.acceleration,
268 deceleration: ec.deceleration,
269 weight_capacity: ec.weight_capacity,
270 current_load: crate::components::Weight::ZERO,
271 riders: Vec::new(),
272 target_stop: None,
273 door_transition_ticks: ec.door_transition_ticks,
274 door_open_ticks: ec.door_open_ticks,
275 line,
276 repositioning: false,
277 restricted_stops: restricted,
278 inspection_speed_factor: ec.inspection_speed_factor,
279 going_up: true,
280 going_down: true,
281 move_count: 0,
282 door_command_queue: Vec::new(),
283 manual_target_velocity: None,
284 },
285 );
286 #[cfg(feature = "energy")]
287 if let Some(ref profile) = ec.energy_profile {
288 world.set_energy_profile(eid, profile.clone());
289 world.set_energy_metrics(eid, crate::energy::EnergyMetrics::default());
290 }
291 if let Some(mode) = ec.service_mode {
292 world.set_service_mode(eid, mode);
293 }
294 world.set_destination_queue(eid, crate::components::DestinationQueue::new());
295 eid
296 }
297
298 fn build_legacy_topology(
300 world: &mut World,
301 config: &SimConfig,
302 stop_lookup: &HashMap<StopId, EntityId>,
303 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
304 ) -> TopologyResult {
305 let all_stop_entities: Vec<EntityId> = stop_lookup.values().copied().collect();
306 let stop_positions: Vec<f64> = config.building.stops.iter().map(|s| s.position).collect();
307 let min_pos = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
308 let max_pos = stop_positions
309 .iter()
310 .copied()
311 .fold(f64::NEG_INFINITY, f64::max);
312
313 let default_line_eid = world.spawn();
314 world.set_line(
315 default_line_eid,
316 Line {
317 name: "Default".into(),
318 group: GroupId(0),
319 orientation: Orientation::Vertical,
320 position: None,
321 min_position: min_pos,
322 max_position: max_pos,
323 max_cars: None,
324 },
325 );
326
327 let mut elevator_entities = Vec::new();
328 for ec in &config.elevators {
329 let eid = Self::spawn_elevator_entity(
330 world,
331 ec,
332 default_line_eid,
333 stop_lookup,
334 &config.building.stops,
335 );
336 elevator_entities.push(eid);
337 }
338
339 let default_line_info =
340 LineInfo::new(default_line_eid, elevator_entities, all_stop_entities);
341
342 let group = ElevatorGroup::new(GroupId(0), "Default".into(), vec![default_line_info]);
343
344 let mut dispatchers = BTreeMap::new();
351 let mut strategy_ids = BTreeMap::new();
352 let user_dispatcher = builder_dispatchers
353 .into_iter()
354 .find_map(|(gid, d)| if gid == GroupId(0) { Some(d) } else { None });
355 if let Some(d) = user_dispatcher {
356 dispatchers.insert(GroupId(0), d);
357 } else {
358 dispatchers.insert(
359 GroupId(0),
360 Box::new(crate::dispatch::scan::ScanDispatch::new()) as Box<dyn DispatchStrategy>,
361 );
362 }
363 strategy_ids.insert(GroupId(0), BuiltinStrategy::Scan);
369
370 (vec![group], dispatchers, strategy_ids)
371 }
372
373 #[allow(clippy::too_many_lines)]
375 fn build_explicit_topology(
376 world: &mut World,
377 config: &SimConfig,
378 line_configs: &[crate::config::LineConfig],
379 stop_lookup: &HashMap<StopId, EntityId>,
380 builder_dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
381 ) -> TopologyResult {
382 let mut line_map: HashMap<u32, (EntityId, LineInfo)> = HashMap::new();
384
385 for lc in line_configs {
386 let served_entities: Vec<EntityId> = lc
388 .serves
389 .iter()
390 .filter_map(|sid| stop_lookup.get(sid).copied())
391 .collect();
392
393 let stop_positions: Vec<f64> = lc
395 .serves
396 .iter()
397 .filter_map(|sid| {
398 config
399 .building
400 .stops
401 .iter()
402 .find(|s| s.id == *sid)
403 .map(|s| s.position)
404 })
405 .collect();
406 let auto_min = stop_positions.iter().copied().fold(f64::INFINITY, f64::min);
407 let auto_max = stop_positions
408 .iter()
409 .copied()
410 .fold(f64::NEG_INFINITY, f64::max);
411
412 let min_pos = lc.min_position.unwrap_or(auto_min);
413 let max_pos = lc.max_position.unwrap_or(auto_max);
414
415 let line_eid = world.spawn();
416 world.set_line(
419 line_eid,
420 Line {
421 name: lc.name.clone(),
422 group: GroupId(0),
423 orientation: lc.orientation,
424 position: lc.position,
425 min_position: min_pos,
426 max_position: max_pos,
427 max_cars: lc.max_cars,
428 },
429 );
430
431 let mut elevator_entities = Vec::new();
433 for ec in &lc.elevators {
434 let eid = Self::spawn_elevator_entity(
435 world,
436 ec,
437 line_eid,
438 stop_lookup,
439 &config.building.stops,
440 );
441 elevator_entities.push(eid);
442 }
443
444 let line_info = LineInfo::new(line_eid, elevator_entities, served_entities);
445 line_map.insert(lc.id, (line_eid, line_info));
446 }
447
448 let group_configs = config.building.groups.as_deref();
450 let mut groups = Vec::new();
451 let mut dispatchers = BTreeMap::new();
452 let mut strategy_ids = BTreeMap::new();
453
454 if let Some(gcs) = group_configs {
455 for gc in gcs {
456 let group_id = GroupId(gc.id);
457
458 let mut group_lines = Vec::new();
459
460 for &lid in &gc.lines {
461 if let Some((line_eid, li)) = line_map.get(&lid) {
462 if let Some(line_comp) = world.line_mut(*line_eid) {
464 line_comp.group = group_id;
465 }
466 group_lines.push(li.clone());
467 }
468 }
469
470 let mut group = ElevatorGroup::new(group_id, gc.name.clone(), group_lines);
471 if let Some(mode) = gc.hall_call_mode {
472 group.set_hall_call_mode(mode);
473 }
474 if let Some(ticks) = gc.ack_latency_ticks {
475 group.set_ack_latency_ticks(ticks);
476 }
477 groups.push(group);
478
479 let dispatch: Box<dyn DispatchStrategy> = gc
481 .dispatch
482 .instantiate()
483 .unwrap_or_else(|| Box::new(crate::dispatch::scan::ScanDispatch::new()));
484 dispatchers.insert(group_id, dispatch);
485 strategy_ids.insert(group_id, gc.dispatch.clone());
486 }
487 } else {
488 let group_id = GroupId(0);
490 let mut group_lines = Vec::new();
491
492 for (line_eid, li) in line_map.values() {
493 if let Some(line_comp) = world.line_mut(*line_eid) {
494 line_comp.group = group_id;
495 }
496 group_lines.push(li.clone());
497 }
498
499 let group = ElevatorGroup::new(group_id, "Default".into(), group_lines);
500 groups.push(group);
501
502 let dispatch: Box<dyn DispatchStrategy> =
503 Box::new(crate::dispatch::scan::ScanDispatch::new());
504 dispatchers.insert(group_id, dispatch);
505 strategy_ids.insert(group_id, BuiltinStrategy::Scan);
506 }
507
508 for (gid, d) in builder_dispatchers {
515 dispatchers.insert(gid, d);
516 strategy_ids
523 .entry(gid)
524 .or_insert_with(|| BuiltinStrategy::Custom("user-supplied".into()));
525 }
526
527 (groups, dispatchers, strategy_ids)
528 }
529
530 #[allow(clippy::too_many_arguments)]
532 pub(crate) fn from_parts(
533 world: World,
534 tick: u64,
535 dt: f64,
536 groups: Vec<ElevatorGroup>,
537 stop_lookup: HashMap<StopId, EntityId>,
538 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
539 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
540 metrics: Metrics,
541 ticks_per_second: f64,
542 ) -> Self {
543 let mut rider_index = RiderIndex::default();
544 rider_index.rebuild(&world);
545 Self {
546 world,
547 events: EventBus::default(),
548 pending_output: Vec::new(),
549 tick,
550 dt,
551 groups,
552 stop_lookup,
553 dispatchers,
554 strategy_ids,
555 repositioners: BTreeMap::new(),
556 reposition_ids: BTreeMap::new(),
557 metrics,
558 time: TimeAdapter::new(ticks_per_second),
559 hooks: PhaseHooks::default(),
560 elevator_ids_buf: Vec::new(),
561 reposition_buf: Vec::new(),
562 topo_graph: Mutex::new(TopologyGraph::new()),
563 rider_index,
564 }
565 }
566
567 pub(crate) fn validate_config(config: &SimConfig) -> Result<(), SimError> {
569 if config.building.stops.is_empty() {
570 return Err(SimError::InvalidConfig {
571 field: "building.stops",
572 reason: "at least one stop is required".into(),
573 });
574 }
575
576 let mut seen_ids = HashSet::new();
578 for stop in &config.building.stops {
579 if !seen_ids.insert(stop.id) {
580 return Err(SimError::InvalidConfig {
581 field: "building.stops",
582 reason: format!("duplicate {}", stop.id),
583 });
584 }
585 if !stop.position.is_finite() {
586 return Err(SimError::InvalidConfig {
587 field: "building.stops.position",
588 reason: format!("{} has non-finite position {}", stop.id, stop.position),
589 });
590 }
591 }
592
593 let stop_ids: HashSet<StopId> = config.building.stops.iter().map(|s| s.id).collect();
594
595 if let Some(line_configs) = &config.building.lines {
596 Self::validate_explicit_topology(line_configs, &stop_ids, &config.building)?;
598 } else {
599 Self::validate_legacy_elevators(&config.elevators, &config.building)?;
601 }
602
603 if !config.simulation.ticks_per_second.is_finite()
604 || config.simulation.ticks_per_second <= 0.0
605 {
606 return Err(SimError::InvalidConfig {
607 field: "simulation.ticks_per_second",
608 reason: format!(
609 "must be finite and positive, got {}",
610 config.simulation.ticks_per_second
611 ),
612 });
613 }
614
615 Self::validate_passenger_spawning(&config.passenger_spawning)?;
616
617 Ok(())
618 }
619
620 fn validate_passenger_spawning(
625 spawn: &crate::config::PassengerSpawnConfig,
626 ) -> Result<(), SimError> {
627 let (lo, hi) = spawn.weight_range;
628 if !lo.is_finite() || !hi.is_finite() {
629 return Err(SimError::InvalidConfig {
630 field: "passenger_spawning.weight_range",
631 reason: format!("both endpoints must be finite, got ({lo}, {hi})"),
632 });
633 }
634 if lo < 0.0 || hi < 0.0 {
635 return Err(SimError::InvalidConfig {
636 field: "passenger_spawning.weight_range",
637 reason: format!("both endpoints must be non-negative, got ({lo}, {hi})"),
638 });
639 }
640 if lo > hi {
641 return Err(SimError::InvalidConfig {
642 field: "passenger_spawning.weight_range",
643 reason: format!("min must be <= max, got ({lo}, {hi})"),
644 });
645 }
646 if spawn.mean_interval_ticks == 0 {
647 return Err(SimError::InvalidConfig {
648 field: "passenger_spawning.mean_interval_ticks",
649 reason: "must be > 0; mean_interval_ticks=0 burst-fires \
650 every catch-up tick"
651 .into(),
652 });
653 }
654 Ok(())
655 }
656
657 fn validate_legacy_elevators(
659 elevators: &[crate::config::ElevatorConfig],
660 building: &crate::config::BuildingConfig,
661 ) -> Result<(), SimError> {
662 if elevators.is_empty() {
663 return Err(SimError::InvalidConfig {
664 field: "elevators",
665 reason: "at least one elevator is required".into(),
666 });
667 }
668
669 for elev in elevators {
670 Self::validate_elevator_config(elev, building)?;
671 }
672
673 Ok(())
674 }
675
676 fn validate_elevator_config(
678 elev: &crate::config::ElevatorConfig,
679 building: &crate::config::BuildingConfig,
680 ) -> Result<(), SimError> {
681 validate_elevator_physics(
682 elev.max_speed.value(),
683 elev.acceleration.value(),
684 elev.deceleration.value(),
685 elev.weight_capacity.value(),
686 elev.inspection_speed_factor,
687 elev.door_transition_ticks,
688 elev.door_open_ticks,
689 )?;
690 if !building.stops.iter().any(|s| s.id == elev.starting_stop) {
691 return Err(SimError::InvalidConfig {
692 field: "elevators.starting_stop",
693 reason: format!("references non-existent {}", elev.starting_stop),
694 });
695 }
696 Ok(())
697 }
698
699 fn validate_explicit_topology(
701 line_configs: &[crate::config::LineConfig],
702 stop_ids: &HashSet<StopId>,
703 building: &crate::config::BuildingConfig,
704 ) -> Result<(), SimError> {
705 let mut seen_line_ids = HashSet::new();
707 for lc in line_configs {
708 if !seen_line_ids.insert(lc.id) {
709 return Err(SimError::InvalidConfig {
710 field: "building.lines",
711 reason: format!("duplicate line id {}", lc.id),
712 });
713 }
714 }
715
716 for lc in line_configs {
718 if lc.serves.is_empty() {
719 return Err(SimError::InvalidConfig {
720 field: "building.lines.serves",
721 reason: format!("line {} has no stops", lc.id),
722 });
723 }
724 for sid in &lc.serves {
725 if !stop_ids.contains(sid) {
726 return Err(SimError::InvalidConfig {
727 field: "building.lines.serves",
728 reason: format!("line {} references non-existent {}", lc.id, sid),
729 });
730 }
731 }
732 for ec in &lc.elevators {
734 Self::validate_elevator_config(ec, building)?;
735 }
736
737 if let Some(max) = lc.max_cars
739 && lc.elevators.len() > max
740 {
741 return Err(SimError::InvalidConfig {
742 field: "building.lines.max_cars",
743 reason: format!(
744 "line {} has {} elevators but max_cars is {max}",
745 lc.id,
746 lc.elevators.len()
747 ),
748 });
749 }
750 }
751
752 let has_elevator = line_configs.iter().any(|lc| !lc.elevators.is_empty());
754 if !has_elevator {
755 return Err(SimError::InvalidConfig {
756 field: "building.lines",
757 reason: "at least one line must have at least one elevator".into(),
758 });
759 }
760
761 let served: HashSet<StopId> = line_configs
763 .iter()
764 .flat_map(|lc| lc.serves.iter().copied())
765 .collect();
766 for sid in stop_ids {
767 if !served.contains(sid) {
768 return Err(SimError::InvalidConfig {
769 field: "building.lines",
770 reason: format!("orphaned stop {sid} not served by any line"),
771 });
772 }
773 }
774
775 if let Some(group_configs) = &building.groups {
777 let line_id_set: HashSet<u32> = line_configs.iter().map(|lc| lc.id).collect();
778
779 let mut seen_group_ids = HashSet::new();
780 for gc in group_configs {
781 if !seen_group_ids.insert(gc.id) {
782 return Err(SimError::InvalidConfig {
783 field: "building.groups",
784 reason: format!("duplicate group id {}", gc.id),
785 });
786 }
787 for &lid in &gc.lines {
788 if !line_id_set.contains(&lid) {
789 return Err(SimError::InvalidConfig {
790 field: "building.groups.lines",
791 reason: format!(
792 "group {} references non-existent line id {}",
793 gc.id, lid
794 ),
795 });
796 }
797 }
798 }
799
800 let referenced_line_ids: HashSet<u32> = group_configs
802 .iter()
803 .flat_map(|g| g.lines.iter().copied())
804 .collect();
805 for lc in line_configs {
806 if !referenced_line_ids.contains(&lc.id) {
807 return Err(SimError::InvalidConfig {
808 field: "building.lines",
809 reason: format!("line {} is not assigned to any group", lc.id),
810 });
811 }
812 }
813 }
814
815 Ok(())
816 }
817
818 pub fn set_dispatch(
825 &mut self,
826 group: GroupId,
827 strategy: Box<dyn DispatchStrategy>,
828 id: crate::dispatch::BuiltinStrategy,
829 ) {
830 self.dispatchers.insert(group, strategy);
831 self.strategy_ids.insert(group, id);
832 }
833
834 pub fn set_reposition(
841 &mut self,
842 group: GroupId,
843 strategy: Box<dyn RepositionStrategy>,
844 id: BuiltinReposition,
845 ) {
846 self.repositioners.insert(group, strategy);
847 self.reposition_ids.insert(group, id);
848 }
849
850 pub fn remove_reposition(&mut self, group: GroupId) {
852 self.repositioners.remove(&group);
853 self.reposition_ids.remove(&group);
854 }
855
856 #[must_use]
858 pub fn reposition_id(&self, group: GroupId) -> Option<&BuiltinReposition> {
859 self.reposition_ids.get(&group)
860 }
861
862 pub fn add_before_hook(
869 &mut self,
870 phase: Phase,
871 hook: impl Fn(&mut World) + Send + Sync + 'static,
872 ) {
873 self.hooks.add_before(phase, Box::new(hook));
874 }
875
876 pub fn add_after_hook(
881 &mut self,
882 phase: Phase,
883 hook: impl Fn(&mut World) + Send + Sync + 'static,
884 ) {
885 self.hooks.add_after(phase, Box::new(hook));
886 }
887
888 pub fn add_before_group_hook(
890 &mut self,
891 phase: Phase,
892 group: GroupId,
893 hook: impl Fn(&mut World) + Send + Sync + 'static,
894 ) {
895 self.hooks.add_before_group(phase, group, Box::new(hook));
896 }
897
898 pub fn add_after_group_hook(
900 &mut self,
901 phase: Phase,
902 group: GroupId,
903 hook: impl Fn(&mut World) + Send + Sync + 'static,
904 ) {
905 self.hooks.add_after_group(phase, group, Box::new(hook));
906 }
907}