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///
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    /// Mutable iteration over every active hall call (crate-internal).
671    pub(crate) fn iter_hall_calls_mut(&mut self) -> impl Iterator<Item = &mut HallCall> {
672        self.hall_calls.values_mut().flat_map(StopCalls::iter_mut)
673    }
674
675    /// Car calls currently registered inside `car`.
676    #[must_use]
677    pub fn car_calls(&self, car: EntityId) -> &[CarCall] {
678        self.car_calls.get(car).map_or(&[], Vec::as_slice)
679    }
680
681    /// Mutable access to the car-call list (crate-internal). Returns
682    /// `None` if the car entity no longer exists.
683    pub(crate) fn car_calls_mut(&mut self, car: EntityId) -> Option<&mut Vec<CarCall>> {
684        Some(self.car_calls.entry(car)?.or_default())
685    }
686
687    /// Remove `rider` from every hall- and car-call's `pending_riders`
688    /// list, and drop car calls whose list becomes empty as a result.
689    ///
690    /// Call on despawn or abandonment so stale rider IDs don't hold
691    /// calls open past the rider's life. Mirrors the per-exit cleanup
692    /// in `systems::loading` (for `Exit`-time car-call pruning). Hall
693    /// calls stay alive after the list empties: they may still represent
694    /// a script-driven press with no associated rider, and are cleared
695    /// the usual way when an eligible car opens doors.
696    pub(crate) fn scrub_rider_from_pending_calls(&mut self, rider: EntityId) {
697        for call in self.iter_hall_calls_mut() {
698            call.pending_riders.retain(|r| *r != rider);
699        }
700        for calls in self.car_calls.values_mut() {
701            for c in calls.iter_mut() {
702                c.pending_riders.retain(|r| *r != rider);
703            }
704            calls.retain(|c| !c.pending_riders.is_empty());
705        }
706    }
707
708    // ── Typed query helpers ──────────────────────────────────────────
709
710    /// Iterate all elevator entities (have `Elevator` + `Position`).
711    pub fn iter_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
712        self.elevators
713            .iter()
714            .filter_map(|(id, car)| self.positions.get(id).map(|pos| (id, pos, car)))
715    }
716
717    /// Iterate all elevator entity IDs without allocating.
718    ///
719    /// Prefer this over [`elevator_ids`](Self::elevator_ids) in tight
720    /// per-tick paths where the result is consumed immediately. The
721    /// allocating form remains for callers that genuinely need indexed
722    /// access (e.g. gdext mapping integer indices from `GDScript`).
723    ///
724    /// Yields `EntityId` rather than the typed [`ElevatorId`] newtype to
725    /// match the rest of the `World`-keyed accessor surface (`elevator`,
726    /// `iter_elevators`, `elevator_ids`). Callers crossing the
727    /// `Simulation` boundary should wrap with `ElevatorId::wrap_unchecked`
728    /// per the project ID-type convention.
729    ///
730    /// [`ElevatorId`]: crate::entity::ElevatorId
731    pub fn iter_elevator_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
732        self.elevators.keys()
733    }
734
735    /// Iterate all elevator entity IDs (allocates).
736    #[must_use]
737    pub fn elevator_ids(&self) -> Vec<EntityId> {
738        self.elevators.keys().collect()
739    }
740
741    /// Fill the buffer with all elevator entity IDs, clearing it first.
742    pub fn elevator_ids_into(&self, buf: &mut Vec<EntityId>) {
743        buf.clear();
744        buf.extend(self.elevators.keys());
745    }
746
747    /// Iterate all rider entities.
748    pub fn iter_riders(&self) -> impl Iterator<Item = (EntityId, &Rider)> {
749        self.riders.iter()
750    }
751
752    /// Iterate all rider entities mutably.
753    pub fn iter_riders_mut(&mut self) -> impl Iterator<Item = (EntityId, &mut Rider)> {
754        self.riders.iter_mut()
755    }
756
757    /// Iterate all rider entity IDs without allocating.
758    ///
759    /// Companion to [`iter_riders`](Self::iter_riders) when only the IDs
760    /// are needed.
761    pub fn iter_rider_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
762        self.riders.keys()
763    }
764
765    /// Iterate all rider entity IDs (allocates).
766    #[must_use]
767    pub fn rider_ids(&self) -> Vec<EntityId> {
768        self.riders.keys().collect()
769    }
770
771    /// Iterate all stop entities.
772    pub fn iter_stops(&self) -> impl Iterator<Item = (EntityId, &Stop)> {
773        self.stops.iter()
774    }
775
776    /// Iterate all stop entity IDs without allocating.
777    ///
778    /// Companion to [`iter_stops`](Self::iter_stops) when only the IDs
779    /// are needed.
780    pub fn iter_stop_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
781        self.stops.keys()
782    }
783
784    /// Iterate all stop entity IDs (allocates).
785    #[must_use]
786    pub fn stop_ids(&self) -> Vec<EntityId> {
787        self.stops.keys().collect()
788    }
789
790    /// Iterate elevators in `Idle` phase (not disabled).
791    pub fn iter_idle_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
792        use crate::components::ElevatorPhase;
793        self.iter_elevators()
794            .filter(|(id, _, car)| car.phase == ElevatorPhase::Idle && !self.is_disabled(*id))
795    }
796
797    /// Iterate elevators that are currently moving — either on a dispatched
798    /// trip (`MovingToStop`) or a repositioning trip (`Repositioning`).
799    /// Excludes disabled elevators.
800    pub fn iter_moving_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
801        self.iter_elevators()
802            .filter(|(id, _, car)| car.phase.is_moving() && !self.is_disabled(*id))
803    }
804
805    /// Iterate riders in `Waiting` phase (not disabled).
806    pub fn iter_waiting_riders(&self) -> impl Iterator<Item = (EntityId, &Rider)> {
807        use crate::components::RiderPhase;
808        self.iter_riders()
809            .filter(|(id, r)| r.phase == RiderPhase::Waiting && !self.is_disabled(*id))
810    }
811
812    /// Find the stop entity at a given position (within
813    /// [`STOP_POSITION_EPSILON`](Self::STOP_POSITION_EPSILON)).
814    ///
815    /// Global lookup — does not filter by line. When two stops on
816    /// different lines share the same physical position the result is
817    /// whichever wins the linear scan, which is rarely what the
818    /// caller actually wants. Prefer
819    /// [`find_stop_at_position_in`](Self::find_stop_at_position_in)
820    /// when the caller knows which line's stops to consider.
821    #[must_use]
822    pub fn find_stop_at_position(&self, position: f64) -> Option<EntityId> {
823        self.stops.iter().find_map(|(id, stop)| {
824            if (stop.position - position).abs() < Self::STOP_POSITION_EPSILON {
825                Some(id)
826            } else {
827                None
828            }
829        })
830    }
831
832    /// Find the stop at a given position from within `candidates`.
833    ///
834    /// `candidates` is typically the `serves` list of a particular
835    /// [`LineInfo`](crate::dispatch::LineInfo) — i.e. the stops a
836    /// specific line can reach. Use this when a car arrives at a
837    /// position and you need *its* line's stop entity, not whichever
838    /// stop on any line happens to share the position. (Two parallel
839    /// shafts at the same physical floor, or a sky-lobby served by
840    /// both a low and high bank, both produce position collisions
841    /// the global lookup can't disambiguate.)
842    ///
843    /// O(n) over `candidates`, which is typically small.
844    #[must_use]
845    pub fn find_stop_at_position_in(
846        &self,
847        position: f64,
848        candidates: &[EntityId],
849    ) -> Option<EntityId> {
850        candidates.iter().copied().find(|&id| {
851            self.stops
852                .get(id)
853                .is_some_and(|stop| (stop.position - position).abs() < Self::STOP_POSITION_EPSILON)
854        })
855    }
856
857    /// Tolerance for [`find_stop_at_position`](Self::find_stop_at_position)
858    /// and [`find_stop_at_position_in`](Self::find_stop_at_position_in).
859    /// Sub-micrometre — small enough that no two distinct floors should
860    /// land within it, large enough to absorb floating-point noise from
861    /// trapezoidal-velocity arrival math.
862    pub const STOP_POSITION_EPSILON: f64 = 1e-6;
863
864    /// Find the stop entity nearest to a given position.
865    ///
866    /// Unlike [`find_stop_at_position`](Self::find_stop_at_position), this finds
867    /// the closest stop by minimum distance rather than requiring an exact match.
868    /// Used when ejecting riders from a disabled/despawned elevator mid-transit.
869    #[must_use]
870    pub fn find_nearest_stop(&self, position: f64) -> Option<EntityId> {
871        self.stops
872            .iter()
873            .min_by(|(_, a), (_, b)| {
874                (a.position - position)
875                    .abs()
876                    .total_cmp(&(b.position - position).abs())
877            })
878            .map(|(id, _)| id)
879    }
880
881    /// Get a stop's position by entity id.
882    #[must_use]
883    pub fn stop_position(&self, id: EntityId) -> Option<f64> {
884        self.stops.get(id).map(|s| s.position)
885    }
886
887    // ── Extension (custom component) storage ─────────────────────────
888
889    /// Insert a custom component for an entity.
890    ///
891    /// Games use this to attach their own typed data to simulation entities.
892    /// Extension components must be `Serialize + DeserializeOwned` to support
893    /// snapshot save/load. An [`ExtKey`] is required for serialization roundtrips.
894    /// Extension components are automatically cleaned up on `despawn()`.
895    ///
896    /// ```
897    /// use elevator_core::world::{ExtKey, World};
898    /// use serde::{Serialize, Deserialize};
899    ///
900    /// #[derive(Debug, Clone, Serialize, Deserialize)]
901    /// struct VipTag { level: u32 }
902    ///
903    /// let mut world = World::new();
904    /// let entity = world.spawn();
905    /// world.insert_ext(entity, VipTag { level: 3 }, ExtKey::from_type_name());
906    /// ```
907    pub fn insert_ext<T: 'static + Send + Sync + serde::Serialize + serde::de::DeserializeOwned>(
908        &mut self,
909        id: EntityId,
910        value: T,
911        key: ExtKey<T>,
912    ) {
913        let type_id = TypeId::of::<T>();
914        Self::assert_ext_name_unique(&self.ext_names, type_id, key.name());
915        let map = self
916            .extensions
917            .entry(type_id)
918            .or_insert_with(|| Box::new(SecondaryMap::<EntityId, T>::new()));
919        if let Some(m) = map.as_any_mut().downcast_mut::<SecondaryMap<EntityId, T>>() {
920            m.insert(id, value);
921        }
922        self.ext_names.insert(type_id, key.name().to_owned());
923    }
924
925    /// Get a clone of a custom component for an entity.
926    #[must_use]
927    pub fn ext<T: 'static + Send + Sync + Clone>(&self, id: EntityId) -> Option<T> {
928        self.ext_map::<T>()?.get(id).cloned()
929    }
930
931    /// Shared reference to a custom component for an entity.
932    ///
933    /// Zero-copy alternative to [`ext`](Self::ext): prefer this when
934    /// `T` is large or expensive to clone, or when the caller only needs a
935    /// borrow. Unlike `ext`, `T` does not need to implement `Clone`.
936    #[must_use]
937    pub fn ext_ref<T: 'static + Send + Sync>(&self, id: EntityId) -> Option<&T> {
938        self.ext_map::<T>()?.get(id)
939    }
940
941    /// Mutable reference to a custom component for an entity.
942    pub fn ext_mut<T: 'static + Send + Sync>(&mut self, id: EntityId) -> Option<&mut T> {
943        self.ext_map_mut::<T>()?.get_mut(id)
944    }
945
946    /// Remove a custom component for an entity.
947    pub fn remove_ext<T: 'static + Send + Sync>(&mut self, id: EntityId) -> Option<T> {
948        self.ext_map_mut::<T>()?.remove(id)
949    }
950
951    /// Downcast extension storage to a typed `SecondaryMap` (shared).
952    pub(crate) fn ext_map<T: 'static + Send + Sync>(&self) -> Option<&SecondaryMap<EntityId, T>> {
953        self.extensions
954            .get(&TypeId::of::<T>())?
955            .as_any()
956            .downcast_ref::<SecondaryMap<EntityId, T>>()
957    }
958
959    /// Downcast extension storage to a typed `SecondaryMap` (mutable).
960    fn ext_map_mut<T: 'static + Send + Sync>(&mut self) -> Option<&mut SecondaryMap<EntityId, T>> {
961        self.extensions
962            .get_mut(&TypeId::of::<T>())?
963            .as_any_mut()
964            .downcast_mut::<SecondaryMap<EntityId, T>>()
965    }
966
967    /// Serialize all extension component data for snapshot.
968    /// Returns name → (`EntityId` → RON string) mapping. `BTreeMap` for
969    /// deterministic snapshot bytes across processes (#254).
970    pub(crate) fn serialize_extensions(&self) -> BTreeMap<String, BTreeMap<EntityId, String>> {
971        let mut result = BTreeMap::new();
972        for (type_id, map) in &self.extensions {
973            if let Some(name) = self.ext_names.get(type_id) {
974                result.insert(name.clone(), map.serialize_entries());
975            }
976        }
977        result
978    }
979
980    /// Deserialize extension data from snapshot. Requires that extension types
981    /// have been registered (via `register_ext_deserializer`) before calling.
982    pub(crate) fn deserialize_extensions(
983        &mut self,
984        data: &BTreeMap<String, BTreeMap<EntityId, String>>,
985    ) {
986        for (name, entries) in data {
987            // Find the TypeId by name.
988            if let Some((&type_id, _)) = self.ext_names.iter().find(|(_, n)| *n == name)
989                && let Some(map) = self.extensions.get_mut(&type_id)
990            {
991                map.deserialize_entries(entries);
992            }
993        }
994    }
995
996    /// Return names from `snapshot_names` that have no registered extension type.
997    pub(crate) fn unregistered_ext_names<'a>(
998        &self,
999        snapshot_names: impl Iterator<Item = &'a String>,
1000    ) -> Vec<String> {
1001        let registered: std::collections::HashSet<&str> =
1002            self.ext_names.values().map(String::as_str).collect();
1003        snapshot_names
1004            .filter(|name| !registered.contains(name.as_str()))
1005            .cloned()
1006            .collect()
1007    }
1008
1009    /// Register an extension type for deserialization (creates empty storage).
1010    ///
1011    /// Must be called before `restore()` for each extension type that was
1012    /// present in the original simulation. Returns the key for convenience.
1013    pub fn register_ext<
1014        T: 'static + Send + Sync + serde::Serialize + serde::de::DeserializeOwned,
1015    >(
1016        &mut self,
1017        key: ExtKey<T>,
1018    ) -> ExtKey<T> {
1019        let type_id = TypeId::of::<T>();
1020        Self::assert_ext_name_unique(&self.ext_names, type_id, key.name());
1021        self.extensions
1022            .entry(type_id)
1023            .or_insert_with(|| Box::new(SecondaryMap::<EntityId, T>::new()));
1024        self.ext_names.insert(type_id, key.name().to_owned());
1025        key
1026    }
1027
1028    /// Panic if `name` is already registered to a different `TypeId`.
1029    ///
1030    /// Two extension types sharing one `ExtKey` name silently corrupts
1031    /// snapshot serde: `serialize_extensions` collapses both types' data
1032    /// into one slot in the result map, and `deserialize_extensions`
1033    /// routes the data to whichever `TypeId` `HashMap::iter().find` returns
1034    /// first — a non-deterministic choice. Failing fast here prevents
1035    /// the corruption from ever being written.
1036    ///
1037    /// Panic chosen over `Result` because [`register_ext`](Self::register_ext)
1038    /// and [`insert_ext`](Self::insert_ext) are non-fallible by design and
1039    /// changing their signatures would break every consumer. Name collisions
1040    /// are programmer errors discoverable at startup, not runtime conditions
1041    /// to recover from.
1042    #[allow(clippy::panic)]
1043    fn assert_ext_name_unique(ext_names: &HashMap<TypeId, String>, type_id: TypeId, name: &str) {
1044        if let Some((existing_tid, _)) = ext_names
1045            .iter()
1046            .find(|(tid, n)| **tid != type_id && n.as_str() == name)
1047        {
1048            panic!(
1049                "ExtKey name {name:?} is already registered to a different type \
1050                 ({existing_tid:?}); each extension type needs a unique key name. \
1051                 If renaming is impractical, use ExtKey::from_type_name() so the \
1052                 name embeds the fully-qualified Rust type name."
1053            );
1054        }
1055    }
1056
1057    // ── Disabled entity management ──────────────────────────────────
1058
1059    /// Mark an entity as disabled. Disabled entities are skipped by all systems.
1060    pub fn disable(&mut self, id: EntityId) {
1061        self.disabled.insert(id, ());
1062    }
1063
1064    /// Re-enable a disabled entity.
1065    pub fn enable(&mut self, id: EntityId) {
1066        self.disabled.remove(id);
1067    }
1068
1069    /// Check if an entity is disabled.
1070    #[must_use]
1071    pub fn is_disabled(&self, id: EntityId) -> bool {
1072        self.disabled.contains_key(id)
1073    }
1074
1075    // ── Global resources (singletons) ───────────────────────────────
1076
1077    /// Insert a global resource. Replaces any existing resource of the same type.
1078    ///
1079    /// Resources are singletons not attached to any entity. Games use them
1080    /// for event channels, score trackers, or any global state.
1081    ///
1082    /// ```
1083    /// use elevator_core::world::World;
1084    /// use elevator_core::events::EventChannel;
1085    ///
1086    /// #[derive(Debug)]
1087    /// enum MyEvent { Score(u32) }
1088    ///
1089    /// let mut world = World::new();
1090    /// world.insert_resource(EventChannel::<MyEvent>::new());
1091    /// ```
1092    pub fn insert_resource<T: 'static + Send + Sync>(&mut self, value: T) {
1093        self.resources.insert(TypeId::of::<T>(), Box::new(value));
1094    }
1095
1096    /// Get a shared reference to a global resource.
1097    #[must_use]
1098    pub fn resource<T: 'static + Send + Sync>(&self) -> Option<&T> {
1099        self.resources.get(&TypeId::of::<T>())?.downcast_ref()
1100    }
1101
1102    /// Get a mutable reference to a global resource.
1103    pub fn resource_mut<T: 'static + Send + Sync>(&mut self) -> Option<&mut T> {
1104        self.resources.get_mut(&TypeId::of::<T>())?.downcast_mut()
1105    }
1106
1107    /// Remove a global resource, returning it if it existed.
1108    pub fn remove_resource<T: 'static + Send + Sync>(&mut self) -> Option<T> {
1109        self.resources
1110            .remove(&TypeId::of::<T>())
1111            .and_then(|b| b.downcast().ok())
1112            .map(|b| *b)
1113    }
1114
1115    // ── Query builder ───────────────────────────────────────────────
1116
1117    /// Create a query builder for iterating entities by component composition.
1118    ///
1119    /// ```
1120    /// use elevator_core::components::{Position, Rider};
1121    /// use elevator_core::prelude::*;
1122    ///
1123    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1124    /// sim.spawn_rider(StopId(0), StopId(1), 75.0).unwrap();
1125    ///
1126    /// let world = sim.world();
1127    /// for (id, rider, pos) in world.query::<(EntityId, &Rider, &Position)>().iter() {
1128    ///     println!("{id:?}: {:?} at {}", rider.phase(), pos.value());
1129    /// }
1130    /// ```
1131    #[must_use]
1132    pub const fn query<Q: crate::query::WorldQuery>(&self) -> crate::query::QueryBuilder<'_, Q> {
1133        crate::query::QueryBuilder::new(self)
1134    }
1135
1136    /// Create a mutable extension query builder.
1137    ///
1138    /// Uses the keys-snapshot pattern: collects matching entity IDs upfront
1139    /// into an owned `Vec`, then iterates with mutable access via
1140    /// [`for_each_mut`](crate::query::ExtQueryMut::for_each_mut).
1141    ///
1142    /// # Example
1143    ///
1144    /// ```no_run
1145    /// # use elevator_core::prelude::*;
1146    /// # use serde::{Serialize, Deserialize};
1147    /// # #[derive(Clone, Serialize, Deserialize)] struct VipTag { level: u32 }
1148    /// # fn run(world: &mut World) {
1149    /// world.query_ext_mut::<VipTag>().for_each_mut(|id, tag| {
1150    ///     tag.level += 1;
1151    /// });
1152    /// # }
1153    /// ```
1154    pub fn query_ext_mut<T: 'static + Send + Sync>(&mut self) -> crate::query::ExtQueryMut<'_, T> {
1155        crate::query::ExtQueryMut::new(self)
1156    }
1157}
1158
1159impl Default for World {
1160    fn default() -> Self {
1161        Self::new()
1162    }
1163}
1164
1165/// Stops sorted by position for efficient range queries (binary search).
1166///
1167/// Used by the movement system to detect `PassingFloor` events in O(log n)
1168/// instead of O(n) per moving elevator per tick.
1169pub(crate) struct SortedStops(pub(crate) Vec<(f64, EntityId)>);
1170
1171/// The up/down hall call pair at a single stop.
1172///
1173/// At most two calls coexist at a stop (one per [`CallDirection`]);
1174/// this struct owns the slots. Stored in `World::hall_calls` keyed by
1175/// the stop's entity id.
1176#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1177pub struct StopCalls {
1178    /// Pending upward call, if the up button is pressed.
1179    pub up: Option<HallCall>,
1180    /// Pending downward call, if the down button is pressed.
1181    pub down: Option<HallCall>,
1182}
1183
1184impl StopCalls {
1185    /// Borrow the call for a specific direction.
1186    #[must_use]
1187    pub const fn get(&self, direction: CallDirection) -> Option<&HallCall> {
1188        match direction {
1189            CallDirection::Up => self.up.as_ref(),
1190            CallDirection::Down => self.down.as_ref(),
1191        }
1192    }
1193
1194    /// Mutable borrow of the call for a direction.
1195    pub const fn get_mut(&mut self, direction: CallDirection) -> Option<&mut HallCall> {
1196        match direction {
1197            CallDirection::Up => self.up.as_mut(),
1198            CallDirection::Down => self.down.as_mut(),
1199        }
1200    }
1201
1202    /// Iterate both calls in (Up, Down) order, skipping empty slots.
1203    pub fn iter(&self) -> impl Iterator<Item = &HallCall> {
1204        self.up.iter().chain(self.down.iter())
1205    }
1206
1207    /// Mutable iteration over both calls.
1208    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut HallCall> {
1209        self.up.iter_mut().chain(self.down.iter_mut())
1210    }
1211}