elevator_core/world.rs
1//! Central entity/component storage (struct-of-arrays ECS).
2
3use std::any::{Any, TypeId};
4use std::collections::{BTreeMap, HashMap};
5use std::marker::PhantomData;
6
7use slotmap::{SecondaryMap, SlotMap};
8
9use crate::components::{
10 AccessControl, CallDirection, CarCall, DestinationQueue, Elevator, HallCall, Line, Patience,
11 Position, Preferences, Rider, Route, ServiceMode, Stop, Velocity,
12};
13#[cfg(feature = "energy")]
14use crate::energy::{EnergyMetrics, EnergyProfile};
15use crate::entity::EntityId;
16use crate::query::storage::AnyExtMap;
17
18/// Typed handle for extension component storage.
19///
20/// Constructed via [`ExtKey::new`] with an explicit name, or
21/// [`ExtKey::from_type_name`] which uses `std::any::type_name::<T>()`.
22///
23/// # Snapshot stability
24///
25/// The `name` is the wire-format key for snapshot extension storage.
26/// **Prefer [`ExtKey::new`] with an explicit string for any production
27/// or persisted scenario** — `std::any::type_name::<T>()` is documented
28/// as not guaranteed stable across compilations and can drift in
29/// practice (renamed crates, generic monomorphization choices, future
30/// compiler releases). A snapshot saved on one build profile and
31/// restored on another whose `type_name<T>()` differs will silently
32/// drop the extension data.
33///
34/// `assert_ext_name_unique` (called by `register_ext` and
35/// `insert_ext`) panics on in-process collisions, so in-memory drift
36/// is loud; the silent failure mode is purely a snapshot-restore-on-a
37/// different-build issue.
38#[derive(Debug)]
39pub struct ExtKey<T> {
40 /// Human-readable storage name, used for serialization roundtrips.
41 name: &'static str,
42 /// Binds this key to the extension component type `T`.
43 _marker: PhantomData<T>,
44}
45
46impl<T> Clone for ExtKey<T> {
47 fn clone(&self) -> Self {
48 *self
49 }
50}
51impl<T> Copy for ExtKey<T> {}
52
53impl<T> ExtKey<T> {
54 /// Create a key with an explicit storage name.
55 #[must_use]
56 pub const fn new(name: &'static str) -> Self {
57 Self {
58 name,
59 _marker: PhantomData,
60 }
61 }
62
63 /// Create a key using `std::any::type_name::<T>()` as the storage name.
64 ///
65 /// Convenient for prototyping and tests, but the resulting `name`
66 /// is **not snapshot-stable across builds** — see the type-level
67 /// "Snapshot stability" note. Production code that persists
68 /// snapshots should use [`ExtKey::new`] with an explicit name.
69 #[must_use]
70 pub fn from_type_name() -> Self {
71 Self {
72 name: std::any::type_name::<T>(),
73 _marker: PhantomData,
74 }
75 }
76
77 /// The storage name for this key.
78 #[must_use]
79 pub const fn name(&self) -> &'static str {
80 self.name
81 }
82}
83
84impl<T> Default for ExtKey<T> {
85 fn default() -> Self {
86 Self::from_type_name()
87 }
88}
89
90/// Central storage for all simulation entities and their components.
91///
92/// Uses separate `SecondaryMap` per component type (struct-of-arrays pattern)
93/// to enable independent mutable borrows of different component storages
94/// within the same system function.
95///
96/// Built-in components are accessed via typed methods. Games can attach
97/// custom data via the extension storage (`insert_ext` / `ext`).
98/// The query builder (`world.query::<...>()`) provides ECS-style iteration.
99///
100/// # Component-accessor semantics
101///
102/// All `<component>(&self, id)` accessors return `Option`. They return `None`
103/// when the entity is dead **or** when it is alive but lacks that component —
104/// slotmap semantics make these two cases indistinguishable through this
105/// surface. Use [`World::is_alive`] (or check the typed component you expect
106/// to be present, e.g. `elevator()` for an elevator entity) when the
107/// distinction matters.
108pub struct World {
109 /// Primary key storage. An entity exists iff its key is here.
110 pub(crate) alive: SlotMap<EntityId, ()>,
111
112 // -- Built-in component storages (crate-internal) --
113 /// Shaft-axis positions.
114 pub(crate) positions: SecondaryMap<EntityId, Position>,
115 /// Snapshot of `positions` taken at the start of the current tick.
116 /// Enables sub-tick interpolation for smooth rendering between steps.
117 pub(crate) prev_positions: SecondaryMap<EntityId, Position>,
118 /// Shaft-axis velocities.
119 pub(crate) velocities: SecondaryMap<EntityId, Velocity>,
120 /// Elevator components.
121 pub(crate) elevators: SecondaryMap<EntityId, Elevator>,
122 /// Stop (floor/station) data.
123 pub(crate) stops: SecondaryMap<EntityId, Stop>,
124 /// Rider core data.
125 pub(crate) riders: SecondaryMap<EntityId, Rider>,
126 /// Multi-leg routes.
127 pub(crate) routes: SecondaryMap<EntityId, Route>,
128 /// Line (physical path) components.
129 pub(crate) lines: SecondaryMap<EntityId, Line>,
130 /// Patience tracking.
131 pub(crate) patience: SecondaryMap<EntityId, Patience>,
132 /// Boarding preferences.
133 pub(crate) preferences: SecondaryMap<EntityId, Preferences>,
134 /// Per-rider access control (allowed stops).
135 pub(crate) access_controls: SecondaryMap<EntityId, AccessControl>,
136
137 /// Per-elevator energy cost profiles.
138 #[cfg(feature = "energy")]
139 pub(crate) energy_profiles: SecondaryMap<EntityId, EnergyProfile>,
140 /// Per-elevator accumulated energy metrics.
141 #[cfg(feature = "energy")]
142 pub(crate) energy_metrics: SecondaryMap<EntityId, EnergyMetrics>,
143 /// Elevator service modes.
144 pub(crate) service_modes: SecondaryMap<EntityId, ServiceMode>,
145 /// Per-elevator destination queues.
146 pub(crate) destination_queues: SecondaryMap<EntityId, DestinationQueue>,
147 /// Up/down hall call buttons per stop. At most two per stop.
148 pub(crate) hall_calls: SecondaryMap<EntityId, StopCalls>,
149 /// Floor buttons pressed inside each car (Classic mode).
150 pub(crate) car_calls: SecondaryMap<EntityId, Vec<CarCall>>,
151
152 /// Disabled marker (entities skipped by all systems).
153 pub(crate) disabled: SecondaryMap<EntityId, ()>,
154
155 // -- Extension storage for game-specific components --
156 /// Type-erased per-entity maps for custom components.
157 extensions: HashMap<TypeId, Box<dyn AnyExtMap>>,
158 /// `TypeId` → name mapping for extension serialization.
159 ext_names: HashMap<TypeId, String>,
160
161 // -- Global resources (singletons not attached to any entity) --
162 /// Type-erased global resources for game-specific state.
163 resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
164}
165
166impl World {
167 /// Create an empty world with no entities.
168 #[must_use]
169 pub fn new() -> Self {
170 Self {
171 alive: SlotMap::with_key(),
172 positions: SecondaryMap::new(),
173 prev_positions: SecondaryMap::new(),
174 velocities: SecondaryMap::new(),
175 elevators: SecondaryMap::new(),
176 stops: SecondaryMap::new(),
177 riders: SecondaryMap::new(),
178 routes: SecondaryMap::new(),
179 lines: SecondaryMap::new(),
180 patience: SecondaryMap::new(),
181 preferences: SecondaryMap::new(),
182 access_controls: SecondaryMap::new(),
183 #[cfg(feature = "energy")]
184 energy_profiles: SecondaryMap::new(),
185 #[cfg(feature = "energy")]
186 energy_metrics: SecondaryMap::new(),
187 service_modes: SecondaryMap::new(),
188 destination_queues: SecondaryMap::new(),
189 hall_calls: SecondaryMap::new(),
190 car_calls: SecondaryMap::new(),
191 disabled: SecondaryMap::new(),
192 extensions: HashMap::new(),
193 ext_names: HashMap::new(),
194 resources: HashMap::new(),
195 }
196 }
197
198 /// Allocate a new entity. Returns its id. No components attached yet.
199 pub fn spawn(&mut self) -> EntityId {
200 self.alive.insert(())
201 }
202
203 /// Remove an entity and all its components (built-in and extensions).
204 ///
205 /// `World::despawn` is a low-level operation: it removes the entity's
206 /// arena entries and performs the cross-references that `World` can
207 /// safely maintain on its own. It does **not** perform rider lifecycle
208 /// transitions (which require `RiderIndex`, owned by `Simulation`).
209 ///
210 /// Cross-references handled here:
211 /// - If the entity is a rider aboard an elevator, it is removed from
212 /// the elevator's rider list and `current_load` is adjusted.
213 ///
214 /// **Despawning an elevator with aboard riders is the caller's
215 /// responsibility to clean up.** Use
216 /// [`Simulation::remove_elevator`](crate::sim::Simulation::remove_elevator)
217 /// (which calls [`Simulation::disable`](crate::sim::Simulation::disable)
218 /// first to transition aboard riders to `Waiting` via the transition
219 /// gateway). Calling this method directly on a populated elevator leaves
220 /// aboard riders pointing at a now-dead `EntityId` in their `phase` —
221 /// a footgun this method no longer attempts to paper over, since any
222 /// reset it produced would silently desync `RiderIndex`.
223 pub fn despawn(&mut self, id: EntityId) {
224 // Clean up rider → elevator cross-references.
225 if let Some(rider) = self.riders.get(id) {
226 let weight = rider.weight;
227 // If this rider is aboard an elevator, remove from its riders list.
228 match rider.phase {
229 crate::components::RiderPhase::Boarding(elev)
230 | crate::components::RiderPhase::Riding(elev)
231 | crate::components::RiderPhase::Exiting(elev) => {
232 if let Some(car) = self.elevators.get_mut(elev) {
233 car.riders.retain(|r| *r != id);
234 car.current_load -= weight;
235 }
236 }
237 _ => {}
238 }
239 }
240
241 self.alive.remove(id);
242 self.positions.remove(id);
243 self.prev_positions.remove(id);
244 self.velocities.remove(id);
245 self.elevators.remove(id);
246 self.stops.remove(id);
247 self.riders.remove(id);
248 self.routes.remove(id);
249 self.lines.remove(id);
250 self.patience.remove(id);
251 self.preferences.remove(id);
252 self.access_controls.remove(id);
253 #[cfg(feature = "energy")]
254 self.energy_profiles.remove(id);
255 #[cfg(feature = "energy")]
256 self.energy_metrics.remove(id);
257 self.service_modes.remove(id);
258 self.destination_queues.remove(id);
259 self.disabled.remove(id);
260 self.hall_calls.remove(id);
261 self.car_calls.remove(id);
262
263 for ext in self.extensions.values_mut() {
264 ext.remove(id);
265 }
266 }
267
268 /// Check if an entity is alive.
269 #[must_use]
270 pub fn is_alive(&self, id: EntityId) -> bool {
271 self.alive.contains_key(id)
272 }
273
274 /// Number of live entities.
275 #[must_use]
276 pub fn entity_count(&self) -> usize {
277 self.alive.len()
278 }
279
280 /// Iterate all alive entity keys (used by the query builder).
281 pub(crate) fn alive_keys(&self) -> slotmap::basic::Keys<'_, EntityId, ()> {
282 self.alive.keys()
283 }
284
285 // ── Position accessors ───────────────────────────────────────────
286
287 /// Get an entity's position.
288 #[must_use]
289 pub fn position(&self, id: EntityId) -> Option<&Position> {
290 self.positions.get(id)
291 }
292
293 /// Get an entity's position mutably.
294 pub fn position_mut(&mut self, id: EntityId) -> Option<&mut Position> {
295 self.positions.get_mut(id)
296 }
297
298 /// Set an entity's position.
299 pub(crate) fn set_position(&mut self, id: EntityId, pos: Position) {
300 self.positions.insert(id, pos);
301 }
302
303 /// Snapshot of an entity's position at the start of the current tick.
304 ///
305 /// Pairs with [`position`](Self::position) to support sub-tick interpolation
306 /// (see [`Simulation::position_at`](crate::sim::Simulation::position_at)).
307 #[must_use]
308 pub fn prev_position(&self, id: EntityId) -> Option<&Position> {
309 self.prev_positions.get(id)
310 }
311
312 /// Snapshot all current positions into `prev_positions`.
313 ///
314 /// Called at the start of each tick by
315 /// [`Simulation::step`](crate::sim::Simulation::step) before any phase
316 /// mutates positions.
317 pub(crate) fn snapshot_prev_positions(&mut self) {
318 self.prev_positions.clear();
319 for (id, pos) in &self.positions {
320 self.prev_positions.insert(id, *pos);
321 }
322 }
323
324 // ── Velocity accessors ───────────────────────────────────────────
325
326 /// Get an entity's velocity.
327 #[must_use]
328 pub fn velocity(&self, id: EntityId) -> Option<&Velocity> {
329 self.velocities.get(id)
330 }
331
332 /// Get an entity's velocity mutably.
333 pub fn velocity_mut(&mut self, id: EntityId) -> Option<&mut Velocity> {
334 self.velocities.get_mut(id)
335 }
336
337 /// Set an entity's velocity.
338 pub(crate) fn set_velocity(&mut self, id: EntityId, vel: Velocity) {
339 self.velocities.insert(id, vel);
340 }
341
342 // ── Elevator accessors ───────────────────────────────────────────
343
344 /// Get an entity's elevator component.
345 #[must_use]
346 pub fn elevator(&self, id: EntityId) -> Option<&Elevator> {
347 self.elevators.get(id)
348 }
349
350 /// Get an entity's elevator component mutably.
351 pub fn elevator_mut(&mut self, id: EntityId) -> Option<&mut Elevator> {
352 self.elevators.get_mut(id)
353 }
354
355 /// Set an entity's elevator component.
356 pub(crate) fn set_elevator(&mut self, id: EntityId, elev: Elevator) {
357 self.elevators.insert(id, elev);
358 }
359
360 /// Current load divided by weight capacity, in `[0.0, 1.0]`.
361 ///
362 /// Returns `None` if `id` is not an elevator. Several systems and
363 /// hosts compute this ratio inline against [`Elevator::current_load`]
364 /// and [`Elevator::weight_capacity`]; centralising it removes the
365 /// duplicate division and preserves the construction-time invariant
366 /// that capacity is always strictly positive.
367 #[must_use]
368 pub fn elevator_load_ratio(&self, id: EntityId) -> Option<f64> {
369 self.elevators.get(id).map(|e| {
370 let cap = e.weight_capacity().value();
371 if cap > 0.0 {
372 (e.current_load().value() / cap).clamp(0.0, 1.0)
373 } else {
374 0.0
375 }
376 })
377 }
378
379 /// Riders currently aboard `id`.
380 ///
381 /// Slimmer than `world.elevator(id).map(|e| e.riders())` and matches
382 /// the `World`-keyed accessor pattern. Returns `None` if `id` is not
383 /// an elevator.
384 #[must_use]
385 pub fn elevator_occupants(&self, id: EntityId) -> Option<&[EntityId]> {
386 self.elevators.get(id).map(Elevator::riders)
387 }
388
389 /// Find any elevator whose physical position matches `position`
390 /// within [`STOP_POSITION_EPSILON`](Self::STOP_POSITION_EPSILON).
391 ///
392 /// Mirrors [`find_stop_at_position`](Self::find_stop_at_position) for
393 /// stops. Useful for "is there a car parked here right now?" queries
394 /// where the caller already has a position from a hall call or a
395 /// stop. O(n) over elevators; n is small in practice.
396 ///
397 /// When two cars momentarily share the same physical position — e.g.
398 /// one arriving as another departs a common lobby floor — the result
399 /// is whichever wins the linear scan, which is rarely what the caller
400 /// actually wants. Callers that need a specific car should filter the
401 /// candidates by line or group themselves.
402 #[must_use]
403 pub fn find_elevator_at_position(&self, position: f64) -> Option<EntityId> {
404 self.elevators.iter().find_map(|(id, _)| {
405 let pos = self.positions.get(id)?;
406 ((pos.value() - position).abs() < Self::STOP_POSITION_EPSILON).then_some(id)
407 })
408 }
409
410 // ── Rider accessors ──────────────────────────────────────────────
411
412 /// Get an entity's rider component.
413 #[must_use]
414 pub fn rider(&self, id: EntityId) -> Option<&Rider> {
415 self.riders.get(id)
416 }
417
418 /// Get an entity's rider component mutably.
419 pub fn rider_mut(&mut self, id: EntityId) -> Option<&mut Rider> {
420 self.riders.get_mut(id)
421 }
422
423 /// Set an entity's rider component.
424 ///
425 /// # Warning
426 ///
427 /// This does **not** update the [`RiderIndex`](crate::rider_index::RiderIndex).
428 /// Call [`RiderIndex::rebuild`](crate::rider_index::RiderIndex::rebuild) afterward
429 /// if the phase or `current_stop` changed.
430 pub(crate) fn set_rider(&mut self, id: EntityId, rider: Rider) {
431 self.riders.insert(id, rider);
432 }
433
434 // ── Stop accessors ───────────────────────────────────────────────
435
436 /// Get an entity's stop component.
437 #[must_use]
438 pub fn stop(&self, id: EntityId) -> Option<&Stop> {
439 self.stops.get(id)
440 }
441
442 /// Get an entity's stop component mutably.
443 pub fn stop_mut(&mut self, id: EntityId) -> Option<&mut Stop> {
444 self.stops.get_mut(id)
445 }
446
447 /// Set an entity's stop component.
448 pub(crate) fn set_stop(&mut self, id: EntityId, stop: Stop) {
449 self.stops.insert(id, stop);
450 }
451
452 // ── Route accessors ──────────────────────────────────────────────
453
454 /// Get an entity's route.
455 #[must_use]
456 pub fn route(&self, id: EntityId) -> Option<&Route> {
457 self.routes.get(id)
458 }
459
460 /// Get an entity's route mutably.
461 pub fn route_mut(&mut self, id: EntityId) -> Option<&mut Route> {
462 self.routes.get_mut(id)
463 }
464
465 /// Set an entity's route.
466 pub(crate) fn set_route(&mut self, id: EntityId, route: Route) {
467 self.routes.insert(id, route);
468 }
469
470 // ── Line accessors ─────────────────────────────────────────────��──
471
472 /// Get an entity's line component.
473 #[must_use]
474 pub fn line(&self, id: EntityId) -> Option<&Line> {
475 self.lines.get(id)
476 }
477
478 /// Get an entity's line component mutably.
479 pub fn line_mut(&mut self, id: EntityId) -> Option<&mut Line> {
480 self.lines.get_mut(id)
481 }
482
483 /// Set an entity's line component.
484 pub(crate) fn set_line(&mut self, id: EntityId, line: Line) {
485 self.lines.insert(id, line);
486 }
487
488 /// Remove an entity's line component.
489 pub fn remove_line(&mut self, id: EntityId) -> Option<Line> {
490 self.lines.remove(id)
491 }
492
493 /// Iterate all line entities.
494 pub fn iter_lines(&self) -> impl Iterator<Item = (EntityId, &Line)> {
495 self.lines.iter()
496 }
497
498 // ── Patience accessors ───────────────────────────────────────────
499
500 /// Get an entity's patience.
501 #[must_use]
502 pub fn patience(&self, id: EntityId) -> Option<&Patience> {
503 self.patience.get(id)
504 }
505
506 /// Get an entity's patience mutably.
507 pub fn patience_mut(&mut self, id: EntityId) -> Option<&mut Patience> {
508 self.patience.get_mut(id)
509 }
510
511 /// Set an entity's patience.
512 pub(crate) fn set_patience(&mut self, id: EntityId, patience: Patience) {
513 self.patience.insert(id, patience);
514 }
515
516 // ── Preferences accessors ────────────────────────────────────────
517
518 /// Get an entity's preferences.
519 #[must_use]
520 pub fn preferences(&self, id: EntityId) -> Option<&Preferences> {
521 self.preferences.get(id)
522 }
523
524 /// Set an entity's preferences.
525 pub(crate) fn set_preferences(&mut self, id: EntityId, prefs: Preferences) {
526 self.preferences.insert(id, prefs);
527 }
528
529 // ── Access control accessors ────────────────────────────────────
530
531 /// Get an entity's access control.
532 #[must_use]
533 pub fn access_control(&self, id: EntityId) -> Option<&AccessControl> {
534 self.access_controls.get(id)
535 }
536
537 /// Get an entity's access control mutably.
538 pub fn access_control_mut(&mut self, id: EntityId) -> Option<&mut AccessControl> {
539 self.access_controls.get_mut(id)
540 }
541
542 /// Set an entity's access control.
543 pub(crate) fn set_access_control(&mut self, id: EntityId, ac: AccessControl) {
544 self.access_controls.insert(id, ac);
545 }
546
547 // ── Energy accessors (feature-gated) ────────────────────────────
548
549 #[cfg(feature = "energy")]
550 /// Get an entity's energy profile.
551 #[must_use]
552 pub fn energy_profile(&self, id: EntityId) -> Option<&EnergyProfile> {
553 self.energy_profiles.get(id)
554 }
555
556 #[cfg(feature = "energy")]
557 /// Get an entity's energy metrics.
558 #[must_use]
559 pub fn energy_metrics(&self, id: EntityId) -> Option<&EnergyMetrics> {
560 self.energy_metrics.get(id)
561 }
562
563 #[cfg(feature = "energy")]
564 /// Get an entity's energy metrics mutably.
565 pub fn energy_metrics_mut(&mut self, id: EntityId) -> Option<&mut EnergyMetrics> {
566 self.energy_metrics.get_mut(id)
567 }
568
569 #[cfg(feature = "energy")]
570 /// Set an entity's energy profile.
571 pub(crate) fn set_energy_profile(&mut self, id: EntityId, profile: EnergyProfile) {
572 self.energy_profiles.insert(id, profile);
573 }
574
575 #[cfg(feature = "energy")]
576 /// Set an entity's energy metrics.
577 pub(crate) fn set_energy_metrics(&mut self, id: EntityId, metrics: EnergyMetrics) {
578 self.energy_metrics.insert(id, metrics);
579 }
580
581 // ── Service mode accessors ──────────────────────────────────────
582
583 /// Get an entity's service mode.
584 #[must_use]
585 pub fn service_mode(&self, id: EntityId) -> Option<&ServiceMode> {
586 self.service_modes.get(id)
587 }
588
589 /// Set an entity's service mode.
590 pub(crate) fn set_service_mode(&mut self, id: EntityId, mode: ServiceMode) {
591 self.service_modes.insert(id, mode);
592 }
593
594 // ── Destination queue accessors ─────────────────────────────────
595
596 /// Get an entity's destination queue.
597 #[must_use]
598 pub fn destination_queue(&self, id: EntityId) -> Option<&DestinationQueue> {
599 self.destination_queues.get(id)
600 }
601
602 /// Get an entity's destination queue mutably (crate-internal — games
603 /// mutate via the [`Simulation`](crate::sim::Simulation) helpers).
604 pub(crate) fn destination_queue_mut(&mut self, id: EntityId) -> Option<&mut DestinationQueue> {
605 self.destination_queues.get_mut(id)
606 }
607
608 /// Set an entity's destination queue.
609 pub(crate) fn set_destination_queue(&mut self, id: EntityId, queue: DestinationQueue) {
610 self.destination_queues.insert(id, queue);
611 }
612
613 // ── Hall call / car call accessors ──────────────────────────────
614
615 /// Get the `(up, down)` hall call pair at a stop, if any exist.
616 #[must_use]
617 pub fn stop_calls(&self, stop: EntityId) -> Option<&StopCalls> {
618 self.hall_calls.get(stop)
619 }
620
621 /// Get a specific directional hall call at a stop.
622 #[must_use]
623 pub fn hall_call(&self, stop: EntityId, direction: CallDirection) -> Option<&HallCall> {
624 self.hall_calls.get(stop).and_then(|c| c.get(direction))
625 }
626
627 /// Mutable access to a directional hall call (crate-internal).
628 pub(crate) fn hall_call_mut(
629 &mut self,
630 stop: EntityId,
631 direction: CallDirection,
632 ) -> Option<&mut HallCall> {
633 self.hall_calls
634 .get_mut(stop)
635 .and_then(|c| c.get_mut(direction))
636 }
637
638 /// Insert (or replace) a hall call at `stop` in `direction`.
639 /// Returns `false` if the stop entity no longer exists in the world.
640 pub(crate) fn set_hall_call(&mut self, call: HallCall) -> bool {
641 let Some(entry) = self.hall_calls.entry(call.stop) else {
642 return false;
643 };
644 let slot = entry.or_default();
645 match call.direction {
646 CallDirection::Up => slot.up = Some(call),
647 CallDirection::Down => slot.down = Some(call),
648 }
649 true
650 }
651
652 /// Remove and return the hall call at `(stop, direction)`, if any.
653 pub(crate) fn remove_hall_call(
654 &mut self,
655 stop: EntityId,
656 direction: CallDirection,
657 ) -> Option<HallCall> {
658 let entry = self.hall_calls.get_mut(stop)?;
659 match direction {
660 CallDirection::Up => entry.up.take(),
661 CallDirection::Down => entry.down.take(),
662 }
663 }
664
665 /// Iterate every active hall call across the world.
666 pub fn iter_hall_calls(&self) -> impl Iterator<Item = &HallCall> {
667 self.hall_calls.values().flat_map(StopCalls::iter)
668 }
669
670 /// Iterate every active hall call paired with the stop entity it
671 /// originated at.
672 ///
673 /// Shape-parity companion to [`iter_riders`](Self::iter_riders) /
674 /// [`iter_stops`](Self::iter_stops) / [`iter_lines`](Self::iter_lines)
675 /// for callers that prefer a `(EntityId, &T)` cursor instead of
676 /// reading [`HallCall::stop`] off the value. The stop returned here
677 /// is always equal to `call.stop` for the yielded `&HallCall`.
678 pub fn iter_hall_calls_with_id(&self) -> impl Iterator<Item = (EntityId, &HallCall)> {
679 self.hall_calls
680 .iter()
681 .flat_map(|(stop, calls)| calls.iter().map(move |c| (stop, c)))
682 }
683
684 /// Mutable iteration over every active hall call (crate-internal).
685 pub(crate) fn iter_hall_calls_mut(&mut self) -> impl Iterator<Item = &mut HallCall> {
686 self.hall_calls.values_mut().flat_map(StopCalls::iter_mut)
687 }
688
689 /// Car calls currently registered inside `car`.
690 #[must_use]
691 pub fn car_calls(&self, car: EntityId) -> &[CarCall] {
692 self.car_calls.get(car).map_or(&[], Vec::as_slice)
693 }
694
695 /// Mutable access to the car-call list (crate-internal). Returns
696 /// `None` if the car entity no longer exists.
697 pub(crate) fn car_calls_mut(&mut self, car: EntityId) -> Option<&mut Vec<CarCall>> {
698 Some(self.car_calls.entry(car)?.or_default())
699 }
700
701 /// Remove `rider` from every hall- and car-call's `pending_riders`
702 /// list, and drop car calls whose list becomes empty as a result.
703 ///
704 /// Call on despawn or abandonment so stale rider IDs don't hold
705 /// calls open past the rider's life. Mirrors the per-exit cleanup
706 /// in `systems::loading` (for `Exit`-time car-call pruning). Hall
707 /// calls stay alive after the list empties: they may still represent
708 /// a script-driven press with no associated rider, and are cleared
709 /// the usual way when an eligible car opens doors.
710 pub(crate) fn scrub_rider_from_pending_calls(&mut self, rider: EntityId) {
711 for call in self.iter_hall_calls_mut() {
712 call.pending_riders.retain(|r| *r != rider);
713 }
714 for calls in self.car_calls.values_mut() {
715 for c in calls.iter_mut() {
716 c.pending_riders.retain(|r| *r != rider);
717 }
718 calls.retain(|c| !c.pending_riders.is_empty());
719 }
720 }
721
722 // ── Typed query helpers ──────────────────────────────────────────
723
724 /// Iterate all elevator entities (have `Elevator` + `Position`).
725 ///
726 /// Yields `(EntityId, &Position, &Elevator)` rather than the
727 /// `(EntityId, &T)` shape of [`iter_riders`](Self::iter_riders) /
728 /// [`iter_stops`](Self::iter_stops) / [`iter_hall_calls_with_id`](Self::iter_hall_calls_with_id)
729 /// because every dispatch / movement / reposition hot path that
730 /// touches an elevator also touches its position, and bundling
731 /// avoids the second `SoA` lookup per car per tick. Callers that
732 /// only need IDs use [`iter_elevator_ids`](Self::iter_elevator_ids).
733 pub fn iter_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
734 self.elevators
735 .iter()
736 .filter_map(|(id, car)| self.positions.get(id).map(|pos| (id, pos, car)))
737 }
738
739 /// Iterate all elevator entity IDs without allocating.
740 ///
741 /// Prefer this over [`elevator_ids`](Self::elevator_ids) in tight
742 /// per-tick paths where the result is consumed immediately. The
743 /// allocating form remains for callers that genuinely need indexed
744 /// access (e.g. gdext mapping integer indices from `GDScript`).
745 ///
746 /// Yields `EntityId` rather than the typed [`ElevatorId`] newtype to
747 /// match the rest of the `World`-keyed accessor surface (`elevator`,
748 /// `iter_elevators`, `elevator_ids`). Callers crossing the
749 /// `Simulation` boundary should wrap with `ElevatorId::wrap_unchecked`
750 /// per the project ID-type convention.
751 ///
752 /// [`ElevatorId`]: crate::entity::ElevatorId
753 pub fn iter_elevator_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
754 self.elevators.keys()
755 }
756
757 /// Iterate all elevator entity IDs (allocates).
758 #[must_use]
759 pub fn elevator_ids(&self) -> Vec<EntityId> {
760 self.elevators.keys().collect()
761 }
762
763 /// Fill the buffer with all elevator entity IDs, clearing it first.
764 pub fn elevator_ids_into(&self, buf: &mut Vec<EntityId>) {
765 buf.clear();
766 buf.extend(self.elevators.keys());
767 }
768
769 /// Iterate all rider entities.
770 pub fn iter_riders(&self) -> impl Iterator<Item = (EntityId, &Rider)> {
771 self.riders.iter()
772 }
773
774 /// Iterate all rider entities mutably.
775 pub fn iter_riders_mut(&mut self) -> impl Iterator<Item = (EntityId, &mut Rider)> {
776 self.riders.iter_mut()
777 }
778
779 /// Iterate all rider entity IDs without allocating.
780 ///
781 /// Companion to [`iter_riders`](Self::iter_riders) when only the IDs
782 /// are needed.
783 pub fn iter_rider_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
784 self.riders.keys()
785 }
786
787 /// Iterate all rider entity IDs (allocates).
788 #[must_use]
789 pub fn rider_ids(&self) -> Vec<EntityId> {
790 self.riders.keys().collect()
791 }
792
793 /// Iterate all stop entities.
794 pub fn iter_stops(&self) -> impl Iterator<Item = (EntityId, &Stop)> {
795 self.stops.iter()
796 }
797
798 /// Iterate all stop entity IDs without allocating.
799 ///
800 /// Companion to [`iter_stops`](Self::iter_stops) when only the IDs
801 /// are needed.
802 pub fn iter_stop_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
803 self.stops.keys()
804 }
805
806 /// Iterate all stop entity IDs (allocates).
807 #[must_use]
808 pub fn stop_ids(&self) -> Vec<EntityId> {
809 self.stops.keys().collect()
810 }
811
812 /// Iterate elevators in `Idle` phase (not disabled).
813 pub fn iter_idle_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
814 use crate::components::ElevatorPhase;
815 self.iter_elevators()
816 .filter(|(id, _, car)| car.phase == ElevatorPhase::Idle && !self.is_disabled(*id))
817 }
818
819 /// Iterate elevators that are currently moving — either on a dispatched
820 /// trip (`MovingToStop`) or a repositioning trip (`Repositioning`).
821 /// Excludes disabled elevators.
822 pub fn iter_moving_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
823 self.iter_elevators()
824 .filter(|(id, _, car)| car.phase.is_moving() && !self.is_disabled(*id))
825 }
826
827 /// Iterate riders in `Waiting` phase (not disabled).
828 pub fn iter_waiting_riders(&self) -> impl Iterator<Item = (EntityId, &Rider)> {
829 use crate::components::RiderPhase;
830 self.iter_riders()
831 .filter(|(id, r)| r.phase == RiderPhase::Waiting && !self.is_disabled(*id))
832 }
833
834 /// Find the stop entity at a given position (within
835 /// [`STOP_POSITION_EPSILON`](Self::STOP_POSITION_EPSILON)).
836 ///
837 /// Global lookup — does not filter by line. When two stops on
838 /// different lines share the same physical position the result is
839 /// whichever wins the linear scan, which is rarely what the
840 /// caller actually wants. Prefer
841 /// [`find_stop_at_position_in`](Self::find_stop_at_position_in)
842 /// when the caller knows which line's stops to consider.
843 #[must_use]
844 pub fn find_stop_at_position(&self, position: f64) -> Option<EntityId> {
845 self.stops.iter().find_map(|(id, stop)| {
846 if (stop.position - position).abs() < Self::STOP_POSITION_EPSILON {
847 Some(id)
848 } else {
849 None
850 }
851 })
852 }
853
854 /// Find the stop at a given position from within `candidates`.
855 ///
856 /// `candidates` is typically the `serves` list of a particular
857 /// [`LineInfo`](crate::dispatch::LineInfo) — i.e. the stops a
858 /// specific line can reach. Use this when a car arrives at a
859 /// position and you need *its* line's stop entity, not whichever
860 /// stop on any line happens to share the position. (Two parallel
861 /// shafts at the same physical floor, or a sky-lobby served by
862 /// both a low and high bank, both produce position collisions
863 /// the global lookup can't disambiguate.)
864 ///
865 /// O(n) over `candidates`, which is typically small.
866 #[must_use]
867 pub fn find_stop_at_position_in(
868 &self,
869 position: f64,
870 candidates: &[EntityId],
871 ) -> Option<EntityId> {
872 candidates.iter().copied().find(|&id| {
873 self.stops
874 .get(id)
875 .is_some_and(|stop| (stop.position - position).abs() < Self::STOP_POSITION_EPSILON)
876 })
877 }
878
879 /// Tolerance for [`find_stop_at_position`](Self::find_stop_at_position)
880 /// and [`find_stop_at_position_in`](Self::find_stop_at_position_in).
881 /// Sub-micrometre — small enough that no two distinct floors should
882 /// land within it, large enough to absorb floating-point noise from
883 /// trapezoidal-velocity arrival math.
884 pub const STOP_POSITION_EPSILON: f64 = 1e-6;
885
886 /// Find the stop entity nearest to a given position.
887 ///
888 /// Unlike [`find_stop_at_position`](Self::find_stop_at_position), this finds
889 /// the closest stop by minimum distance rather than requiring an exact match.
890 /// Used when ejecting riders from a disabled/despawned elevator mid-transit.
891 #[must_use]
892 pub fn find_nearest_stop(&self, position: f64) -> Option<EntityId> {
893 self.stops
894 .iter()
895 .min_by(|(_, a), (_, b)| {
896 (a.position - position)
897 .abs()
898 .total_cmp(&(b.position - position).abs())
899 })
900 .map(|(id, _)| id)
901 }
902
903 /// Get a stop's position by entity id.
904 #[must_use]
905 pub fn stop_position(&self, id: EntityId) -> Option<f64> {
906 self.stops.get(id).map(|s| s.position)
907 }
908
909 // ── Extension (custom component) storage ─────────────────────────
910
911 /// Insert a custom component for an entity.
912 ///
913 /// Games use this to attach their own typed data to simulation entities.
914 /// Extension components must be `Serialize + DeserializeOwned` to support
915 /// snapshot save/load. An [`ExtKey`] is required for serialization roundtrips.
916 /// Extension components are automatically cleaned up on `despawn()`.
917 ///
918 /// ```
919 /// use elevator_core::world::{ExtKey, World};
920 /// use serde::{Serialize, Deserialize};
921 ///
922 /// #[derive(Debug, Clone, Serialize, Deserialize)]
923 /// struct VipTag { level: u32 }
924 ///
925 /// let mut world = World::new();
926 /// let entity = world.spawn();
927 /// world.insert_ext(entity, VipTag { level: 3 }, ExtKey::from_type_name());
928 /// ```
929 pub fn insert_ext<T: 'static + Send + Sync + serde::Serialize + serde::de::DeserializeOwned>(
930 &mut self,
931 id: EntityId,
932 value: T,
933 key: ExtKey<T>,
934 ) {
935 let type_id = TypeId::of::<T>();
936 Self::assert_ext_name_unique(&self.ext_names, type_id, key.name());
937 let map = self
938 .extensions
939 .entry(type_id)
940 .or_insert_with(|| Box::new(SecondaryMap::<EntityId, T>::new()));
941 if let Some(m) = map.as_any_mut().downcast_mut::<SecondaryMap<EntityId, T>>() {
942 m.insert(id, value);
943 }
944 self.ext_names.insert(type_id, key.name().to_owned());
945 }
946
947 /// Get a clone of a custom component for an entity.
948 #[must_use]
949 pub fn ext<T: 'static + Send + Sync + Clone>(&self, id: EntityId) -> Option<T> {
950 self.ext_map::<T>()?.get(id).cloned()
951 }
952
953 /// Shared reference to a custom component for an entity.
954 ///
955 /// Zero-copy alternative to [`ext`](Self::ext): prefer this when
956 /// `T` is large or expensive to clone, or when the caller only needs a
957 /// borrow. Unlike `ext`, `T` does not need to implement `Clone`.
958 #[must_use]
959 pub fn ext_ref<T: 'static + Send + Sync>(&self, id: EntityId) -> Option<&T> {
960 self.ext_map::<T>()?.get(id)
961 }
962
963 /// Mutable reference to a custom component for an entity.
964 pub fn ext_mut<T: 'static + Send + Sync>(&mut self, id: EntityId) -> Option<&mut T> {
965 self.ext_map_mut::<T>()?.get_mut(id)
966 }
967
968 /// Remove a custom component for an entity.
969 pub fn remove_ext<T: 'static + Send + Sync>(&mut self, id: EntityId) -> Option<T> {
970 self.ext_map_mut::<T>()?.remove(id)
971 }
972
973 /// Downcast extension storage to a typed `SecondaryMap` (shared).
974 pub(crate) fn ext_map<T: 'static + Send + Sync>(&self) -> Option<&SecondaryMap<EntityId, T>> {
975 self.extensions
976 .get(&TypeId::of::<T>())?
977 .as_any()
978 .downcast_ref::<SecondaryMap<EntityId, T>>()
979 }
980
981 /// Downcast extension storage to a typed `SecondaryMap` (mutable).
982 fn ext_map_mut<T: 'static + Send + Sync>(&mut self) -> Option<&mut SecondaryMap<EntityId, T>> {
983 self.extensions
984 .get_mut(&TypeId::of::<T>())?
985 .as_any_mut()
986 .downcast_mut::<SecondaryMap<EntityId, T>>()
987 }
988
989 /// Serialize all extension component data for snapshot.
990 /// Returns name → (`EntityId` → RON string) mapping. `BTreeMap` for
991 /// deterministic snapshot bytes across processes (#254).
992 pub(crate) fn serialize_extensions(&self) -> BTreeMap<String, BTreeMap<EntityId, String>> {
993 let mut result = BTreeMap::new();
994 for (type_id, map) in &self.extensions {
995 if let Some(name) = self.ext_names.get(type_id) {
996 result.insert(name.clone(), map.serialize_entries());
997 }
998 }
999 result
1000 }
1001
1002 /// Deserialize extension data from snapshot. Requires that extension types
1003 /// have been registered (via `register_ext_deserializer`) before calling.
1004 pub(crate) fn deserialize_extensions(
1005 &mut self,
1006 data: &BTreeMap<String, BTreeMap<EntityId, String>>,
1007 ) {
1008 for (name, entries) in data {
1009 // Find the TypeId by name.
1010 if let Some((&type_id, _)) = self.ext_names.iter().find(|(_, n)| *n == name)
1011 && let Some(map) = self.extensions.get_mut(&type_id)
1012 {
1013 map.deserialize_entries(entries);
1014 }
1015 }
1016 }
1017
1018 /// Return names from `snapshot_names` that have no registered extension type.
1019 pub(crate) fn unregistered_ext_names<'a>(
1020 &self,
1021 snapshot_names: impl Iterator<Item = &'a String>,
1022 ) -> Vec<String> {
1023 let registered: std::collections::HashSet<&str> =
1024 self.ext_names.values().map(String::as_str).collect();
1025 snapshot_names
1026 .filter(|name| !registered.contains(name.as_str()))
1027 .cloned()
1028 .collect()
1029 }
1030
1031 /// Register an extension type for deserialization (creates empty storage).
1032 ///
1033 /// Must be called before `restore()` for each extension type that was
1034 /// present in the original simulation. Returns the key for convenience.
1035 pub fn register_ext<
1036 T: 'static + Send + Sync + serde::Serialize + serde::de::DeserializeOwned,
1037 >(
1038 &mut self,
1039 key: ExtKey<T>,
1040 ) -> ExtKey<T> {
1041 let type_id = TypeId::of::<T>();
1042 Self::assert_ext_name_unique(&self.ext_names, type_id, key.name());
1043 self.extensions
1044 .entry(type_id)
1045 .or_insert_with(|| Box::new(SecondaryMap::<EntityId, T>::new()));
1046 self.ext_names.insert(type_id, key.name().to_owned());
1047 key
1048 }
1049
1050 /// Panic if `name` is already registered to a different `TypeId`.
1051 ///
1052 /// Two extension types sharing one `ExtKey` name silently corrupts
1053 /// snapshot serde: `serialize_extensions` collapses both types' data
1054 /// into one slot in the result map, and `deserialize_extensions`
1055 /// routes the data to whichever `TypeId` `HashMap::iter().find` returns
1056 /// first — a non-deterministic choice. Failing fast here prevents
1057 /// the corruption from ever being written.
1058 ///
1059 /// Panic chosen over `Result` because [`register_ext`](Self::register_ext)
1060 /// and [`insert_ext`](Self::insert_ext) are non-fallible by design and
1061 /// changing their signatures would break every consumer. Name collisions
1062 /// are programmer errors discoverable at startup, not runtime conditions
1063 /// to recover from.
1064 #[allow(clippy::panic)]
1065 fn assert_ext_name_unique(ext_names: &HashMap<TypeId, String>, type_id: TypeId, name: &str) {
1066 if let Some((existing_tid, _)) = ext_names
1067 .iter()
1068 .find(|(tid, n)| **tid != type_id && n.as_str() == name)
1069 {
1070 panic!(
1071 "ExtKey name {name:?} is already registered to a different type \
1072 ({existing_tid:?}); each extension type needs a unique key name. \
1073 If renaming is impractical, use ExtKey::from_type_name() so the \
1074 name embeds the fully-qualified Rust type name."
1075 );
1076 }
1077 }
1078
1079 // ── Disabled entity management ──────────────────────────────────
1080
1081 /// Mark an entity as disabled. Disabled entities are skipped by all systems.
1082 pub fn disable(&mut self, id: EntityId) {
1083 self.disabled.insert(id, ());
1084 }
1085
1086 /// Re-enable a disabled entity.
1087 pub fn enable(&mut self, id: EntityId) {
1088 self.disabled.remove(id);
1089 }
1090
1091 /// Check if an entity is disabled.
1092 #[must_use]
1093 pub fn is_disabled(&self, id: EntityId) -> bool {
1094 self.disabled.contains_key(id)
1095 }
1096
1097 // ── Global resources (singletons) ───────────────────────────────
1098
1099 /// Insert a global resource. Replaces any existing resource of the same type.
1100 ///
1101 /// Resources are singletons not attached to any entity. Games use them
1102 /// for event channels, score trackers, or any global state.
1103 ///
1104 /// ```
1105 /// use elevator_core::world::World;
1106 /// use elevator_core::events::EventChannel;
1107 ///
1108 /// #[derive(Debug)]
1109 /// enum MyEvent { Score(u32) }
1110 ///
1111 /// let mut world = World::new();
1112 /// world.insert_resource(EventChannel::<MyEvent>::new());
1113 /// ```
1114 pub fn insert_resource<T: 'static + Send + Sync>(&mut self, value: T) {
1115 self.resources.insert(TypeId::of::<T>(), Box::new(value));
1116 }
1117
1118 /// Get a shared reference to a global resource.
1119 #[must_use]
1120 pub fn resource<T: 'static + Send + Sync>(&self) -> Option<&T> {
1121 self.resources.get(&TypeId::of::<T>())?.downcast_ref()
1122 }
1123
1124 /// Get a mutable reference to a global resource.
1125 pub fn resource_mut<T: 'static + Send + Sync>(&mut self) -> Option<&mut T> {
1126 self.resources.get_mut(&TypeId::of::<T>())?.downcast_mut()
1127 }
1128
1129 /// Remove a global resource, returning it if it existed.
1130 pub fn remove_resource<T: 'static + Send + Sync>(&mut self) -> Option<T> {
1131 self.resources
1132 .remove(&TypeId::of::<T>())
1133 .and_then(|b| b.downcast().ok())
1134 .map(|b| *b)
1135 }
1136
1137 // ── Query builder ───────────────────────────────────────────────
1138
1139 /// Create a query builder for iterating entities by component composition.
1140 ///
1141 /// ```
1142 /// use elevator_core::components::{Position, Rider};
1143 /// use elevator_core::prelude::*;
1144 ///
1145 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1146 /// sim.spawn_rider(StopId(0), StopId(1), 75.0).unwrap();
1147 ///
1148 /// let world = sim.world();
1149 /// for (id, rider, pos) in world.query::<(EntityId, &Rider, &Position)>().iter() {
1150 /// println!("{id:?}: {:?} at {}", rider.phase(), pos.value());
1151 /// }
1152 /// ```
1153 #[must_use]
1154 pub const fn query<Q: crate::query::WorldQuery>(&self) -> crate::query::QueryBuilder<'_, Q> {
1155 crate::query::QueryBuilder::new(self)
1156 }
1157
1158 /// Create a mutable extension query builder.
1159 ///
1160 /// Uses the keys-snapshot pattern: collects matching entity IDs upfront
1161 /// into an owned `Vec`, then iterates with mutable access via
1162 /// [`for_each_mut`](crate::query::ExtQueryMut::for_each_mut).
1163 ///
1164 /// # Example
1165 ///
1166 /// ```no_run
1167 /// # use elevator_core::prelude::*;
1168 /// # use serde::{Serialize, Deserialize};
1169 /// # #[derive(Clone, Serialize, Deserialize)] struct VipTag { level: u32 }
1170 /// # fn run(world: &mut World) {
1171 /// world.query_ext_mut::<VipTag>().for_each_mut(|id, tag| {
1172 /// tag.level += 1;
1173 /// });
1174 /// # }
1175 /// ```
1176 pub fn query_ext_mut<T: 'static + Send + Sync>(&mut self) -> crate::query::ExtQueryMut<'_, T> {
1177 crate::query::ExtQueryMut::new(self)
1178 }
1179}
1180
1181impl Default for World {
1182 fn default() -> Self {
1183 Self::new()
1184 }
1185}
1186
1187/// Stops sorted by position for efficient range queries (binary search).
1188///
1189/// Used by the movement system to detect `PassingFloor` events in O(log n)
1190/// instead of O(n) per moving elevator per tick.
1191pub(crate) struct SortedStops(pub(crate) Vec<(f64, EntityId)>);
1192
1193/// The up/down hall call pair at a single stop.
1194///
1195/// At most two calls coexist at a stop (one per [`CallDirection`]);
1196/// this struct owns the slots. Stored in `World::hall_calls` keyed by
1197/// the stop's entity id.
1198#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1199pub struct StopCalls {
1200 /// Pending upward call, if the up button is pressed.
1201 pub up: Option<HallCall>,
1202 /// Pending downward call, if the down button is pressed.
1203 pub down: Option<HallCall>,
1204}
1205
1206impl StopCalls {
1207 /// Borrow the call for a specific direction.
1208 #[must_use]
1209 pub const fn get(&self, direction: CallDirection) -> Option<&HallCall> {
1210 match direction {
1211 CallDirection::Up => self.up.as_ref(),
1212 CallDirection::Down => self.down.as_ref(),
1213 }
1214 }
1215
1216 /// Mutable borrow of the call for a direction.
1217 pub const fn get_mut(&mut self, direction: CallDirection) -> Option<&mut HallCall> {
1218 match direction {
1219 CallDirection::Up => self.up.as_mut(),
1220 CallDirection::Down => self.down.as_mut(),
1221 }
1222 }
1223
1224 /// Iterate both calls in (Up, Down) order, skipping empty slots.
1225 pub fn iter(&self) -> impl Iterator<Item = &HallCall> {
1226 self.up.iter().chain(self.down.iter())
1227 }
1228
1229 /// Mutable iteration over both calls.
1230 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut HallCall> {
1231 self.up.iter_mut().chain(self.down.iter_mut())
1232 }
1233}