Skip to main content

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