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 // ── Rider accessors ──────────────────────────────────────────────
340
341 /// Get an entity's rider component.
342 #[must_use]
343 pub fn rider(&self, id: EntityId) -> Option<&Rider> {
344 self.riders.get(id)
345 }
346
347 /// Get an entity's rider component mutably.
348 pub fn rider_mut(&mut self, id: EntityId) -> Option<&mut Rider> {
349 self.riders.get_mut(id)
350 }
351
352 /// Set an entity's rider component.
353 ///
354 /// # Warning
355 ///
356 /// This does **not** update the [`RiderIndex`](crate::rider_index::RiderIndex).
357 /// Call [`RiderIndex::rebuild`](crate::rider_index::RiderIndex::rebuild) afterward
358 /// if the phase or `current_stop` changed.
359 pub(crate) fn set_rider(&mut self, id: EntityId, rider: Rider) {
360 self.riders.insert(id, rider);
361 }
362
363 // ── Stop accessors ───────────────────────────────────────────────
364
365 /// Get an entity's stop component.
366 #[must_use]
367 pub fn stop(&self, id: EntityId) -> Option<&Stop> {
368 self.stops.get(id)
369 }
370
371 /// Get an entity's stop component mutably.
372 pub fn stop_mut(&mut self, id: EntityId) -> Option<&mut Stop> {
373 self.stops.get_mut(id)
374 }
375
376 /// Set an entity's stop component.
377 pub(crate) fn set_stop(&mut self, id: EntityId, stop: Stop) {
378 self.stops.insert(id, stop);
379 }
380
381 // ── Route accessors ──────────────────────────────────────────────
382
383 /// Get an entity's route.
384 #[must_use]
385 pub fn route(&self, id: EntityId) -> Option<&Route> {
386 self.routes.get(id)
387 }
388
389 /// Get an entity's route mutably.
390 pub fn route_mut(&mut self, id: EntityId) -> Option<&mut Route> {
391 self.routes.get_mut(id)
392 }
393
394 /// Set an entity's route.
395 pub(crate) fn set_route(&mut self, id: EntityId, route: Route) {
396 self.routes.insert(id, route);
397 }
398
399 // ── Line accessors ─────────────────────────────────────────────��──
400
401 /// Get an entity's line component.
402 #[must_use]
403 pub fn line(&self, id: EntityId) -> Option<&Line> {
404 self.lines.get(id)
405 }
406
407 /// Get an entity's line component mutably.
408 pub fn line_mut(&mut self, id: EntityId) -> Option<&mut Line> {
409 self.lines.get_mut(id)
410 }
411
412 /// Set an entity's line component.
413 pub(crate) fn set_line(&mut self, id: EntityId, line: Line) {
414 self.lines.insert(id, line);
415 }
416
417 /// Remove an entity's line component.
418 pub fn remove_line(&mut self, id: EntityId) -> Option<Line> {
419 self.lines.remove(id)
420 }
421
422 /// Iterate all line entities.
423 pub fn iter_lines(&self) -> impl Iterator<Item = (EntityId, &Line)> {
424 self.lines.iter()
425 }
426
427 // ── Patience accessors ───────────────────────────────────────────
428
429 /// Get an entity's patience.
430 #[must_use]
431 pub fn patience(&self, id: EntityId) -> Option<&Patience> {
432 self.patience.get(id)
433 }
434
435 /// Get an entity's patience mutably.
436 pub fn patience_mut(&mut self, id: EntityId) -> Option<&mut Patience> {
437 self.patience.get_mut(id)
438 }
439
440 /// Set an entity's patience.
441 pub(crate) fn set_patience(&mut self, id: EntityId, patience: Patience) {
442 self.patience.insert(id, patience);
443 }
444
445 // ── Preferences accessors ────────────────────────────────────────
446
447 /// Get an entity's preferences.
448 #[must_use]
449 pub fn preferences(&self, id: EntityId) -> Option<&Preferences> {
450 self.preferences.get(id)
451 }
452
453 /// Set an entity's preferences.
454 pub(crate) fn set_preferences(&mut self, id: EntityId, prefs: Preferences) {
455 self.preferences.insert(id, prefs);
456 }
457
458 // ── Access control accessors ────────────────────────────────────
459
460 /// Get an entity's access control.
461 #[must_use]
462 pub fn access_control(&self, id: EntityId) -> Option<&AccessControl> {
463 self.access_controls.get(id)
464 }
465
466 /// Get an entity's access control mutably.
467 pub fn access_control_mut(&mut self, id: EntityId) -> Option<&mut AccessControl> {
468 self.access_controls.get_mut(id)
469 }
470
471 /// Set an entity's access control.
472 pub(crate) fn set_access_control(&mut self, id: EntityId, ac: AccessControl) {
473 self.access_controls.insert(id, ac);
474 }
475
476 // ── Energy accessors (feature-gated) ────────────────────────────
477
478 #[cfg(feature = "energy")]
479 /// Get an entity's energy profile.
480 #[must_use]
481 pub fn energy_profile(&self, id: EntityId) -> Option<&EnergyProfile> {
482 self.energy_profiles.get(id)
483 }
484
485 #[cfg(feature = "energy")]
486 /// Get an entity's energy metrics.
487 #[must_use]
488 pub fn energy_metrics(&self, id: EntityId) -> Option<&EnergyMetrics> {
489 self.energy_metrics.get(id)
490 }
491
492 #[cfg(feature = "energy")]
493 /// Get an entity's energy metrics mutably.
494 pub fn energy_metrics_mut(&mut self, id: EntityId) -> Option<&mut EnergyMetrics> {
495 self.energy_metrics.get_mut(id)
496 }
497
498 #[cfg(feature = "energy")]
499 /// Set an entity's energy profile.
500 pub(crate) fn set_energy_profile(&mut self, id: EntityId, profile: EnergyProfile) {
501 self.energy_profiles.insert(id, profile);
502 }
503
504 #[cfg(feature = "energy")]
505 /// Set an entity's energy metrics.
506 pub(crate) fn set_energy_metrics(&mut self, id: EntityId, metrics: EnergyMetrics) {
507 self.energy_metrics.insert(id, metrics);
508 }
509
510 // ── Service mode accessors ──────────────────────────────────────
511
512 /// Get an entity's service mode.
513 #[must_use]
514 pub fn service_mode(&self, id: EntityId) -> Option<&ServiceMode> {
515 self.service_modes.get(id)
516 }
517
518 /// Set an entity's service mode.
519 pub(crate) fn set_service_mode(&mut self, id: EntityId, mode: ServiceMode) {
520 self.service_modes.insert(id, mode);
521 }
522
523 // ── Destination queue accessors ─────────────────────────────────
524
525 /// Get an entity's destination queue.
526 #[must_use]
527 pub fn destination_queue(&self, id: EntityId) -> Option<&DestinationQueue> {
528 self.destination_queues.get(id)
529 }
530
531 /// Get an entity's destination queue mutably (crate-internal — games
532 /// mutate via the [`Simulation`](crate::sim::Simulation) helpers).
533 pub(crate) fn destination_queue_mut(&mut self, id: EntityId) -> Option<&mut DestinationQueue> {
534 self.destination_queues.get_mut(id)
535 }
536
537 /// Set an entity's destination queue.
538 pub(crate) fn set_destination_queue(&mut self, id: EntityId, queue: DestinationQueue) {
539 self.destination_queues.insert(id, queue);
540 }
541
542 // ── Hall call / car call accessors ──────────────────────────────
543
544 /// Get the `(up, down)` hall call pair at a stop, if any exist.
545 #[must_use]
546 pub fn stop_calls(&self, stop: EntityId) -> Option<&StopCalls> {
547 self.hall_calls.get(stop)
548 }
549
550 /// Get a specific directional hall call at a stop.
551 #[must_use]
552 pub fn hall_call(&self, stop: EntityId, direction: CallDirection) -> Option<&HallCall> {
553 self.hall_calls.get(stop).and_then(|c| c.get(direction))
554 }
555
556 /// Mutable access to a directional hall call (crate-internal).
557 pub(crate) fn hall_call_mut(
558 &mut self,
559 stop: EntityId,
560 direction: CallDirection,
561 ) -> Option<&mut HallCall> {
562 self.hall_calls
563 .get_mut(stop)
564 .and_then(|c| c.get_mut(direction))
565 }
566
567 /// Insert (or replace) a hall call at `stop` in `direction`.
568 /// Returns `false` if the stop entity no longer exists in the world.
569 pub(crate) fn set_hall_call(&mut self, call: HallCall) -> bool {
570 let Some(entry) = self.hall_calls.entry(call.stop) else {
571 return false;
572 };
573 let slot = entry.or_default();
574 match call.direction {
575 CallDirection::Up => slot.up = Some(call),
576 CallDirection::Down => slot.down = Some(call),
577 }
578 true
579 }
580
581 /// Remove and return the hall call at `(stop, direction)`, if any.
582 pub(crate) fn remove_hall_call(
583 &mut self,
584 stop: EntityId,
585 direction: CallDirection,
586 ) -> Option<HallCall> {
587 let entry = self.hall_calls.get_mut(stop)?;
588 match direction {
589 CallDirection::Up => entry.up.take(),
590 CallDirection::Down => entry.down.take(),
591 }
592 }
593
594 /// Iterate every active hall call across the world.
595 pub fn iter_hall_calls(&self) -> impl Iterator<Item = &HallCall> {
596 self.hall_calls.values().flat_map(StopCalls::iter)
597 }
598
599 /// Mutable iteration over every active hall call (crate-internal).
600 pub(crate) fn iter_hall_calls_mut(&mut self) -> impl Iterator<Item = &mut HallCall> {
601 self.hall_calls.values_mut().flat_map(StopCalls::iter_mut)
602 }
603
604 /// Car calls currently registered inside `car`.
605 #[must_use]
606 pub fn car_calls(&self, car: EntityId) -> &[CarCall] {
607 self.car_calls.get(car).map_or(&[], Vec::as_slice)
608 }
609
610 /// Mutable access to the car-call list (crate-internal). Returns
611 /// `None` if the car entity no longer exists.
612 pub(crate) fn car_calls_mut(&mut self, car: EntityId) -> Option<&mut Vec<CarCall>> {
613 Some(self.car_calls.entry(car)?.or_default())
614 }
615
616 /// Remove `rider` from every hall- and car-call's `pending_riders`
617 /// list, and drop car calls whose list becomes empty as a result.
618 ///
619 /// Call on despawn or abandonment so stale rider IDs don't hold
620 /// calls open past the rider's life. Mirrors the per-exit cleanup
621 /// in `systems::loading` (for `Exit`-time car-call pruning). Hall
622 /// calls stay alive after the list empties: they may still represent
623 /// a script-driven press with no associated rider, and are cleared
624 /// the usual way when an eligible car opens doors.
625 pub(crate) fn scrub_rider_from_pending_calls(&mut self, rider: EntityId) {
626 for call in self.iter_hall_calls_mut() {
627 call.pending_riders.retain(|r| *r != rider);
628 }
629 for calls in self.car_calls.values_mut() {
630 for c in calls.iter_mut() {
631 c.pending_riders.retain(|r| *r != rider);
632 }
633 calls.retain(|c| !c.pending_riders.is_empty());
634 }
635 }
636
637 // ── Typed query helpers ──────────────────────────────────────────
638
639 /// Iterate all elevator entities (have `Elevator` + `Position`).
640 pub fn iter_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
641 self.elevators
642 .iter()
643 .filter_map(|(id, car)| self.positions.get(id).map(|pos| (id, pos, car)))
644 }
645
646 /// Iterate all elevator entity IDs without allocating.
647 ///
648 /// Prefer this over [`elevator_ids`](Self::elevator_ids) in tight
649 /// per-tick paths where the result is consumed immediately. The
650 /// allocating form remains for callers that genuinely need indexed
651 /// access (e.g. gdext mapping integer indices from `GDScript`).
652 ///
653 /// Yields `EntityId` rather than the typed [`ElevatorId`] newtype to
654 /// match the rest of the `World`-keyed accessor surface (`elevator`,
655 /// `iter_elevators`, `elevator_ids`). Callers crossing the
656 /// `Simulation` boundary should wrap with `ElevatorId::wrap_unchecked`
657 /// per the project ID-type convention.
658 ///
659 /// [`ElevatorId`]: crate::entity::ElevatorId
660 pub fn iter_elevator_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
661 self.elevators.keys()
662 }
663
664 /// Iterate all elevator entity IDs (allocates).
665 #[must_use]
666 pub fn elevator_ids(&self) -> Vec<EntityId> {
667 self.elevators.keys().collect()
668 }
669
670 /// Fill the buffer with all elevator entity IDs, clearing it first.
671 pub fn elevator_ids_into(&self, buf: &mut Vec<EntityId>) {
672 buf.clear();
673 buf.extend(self.elevators.keys());
674 }
675
676 /// Iterate all rider entities.
677 pub fn iter_riders(&self) -> impl Iterator<Item = (EntityId, &Rider)> {
678 self.riders.iter()
679 }
680
681 /// Iterate all rider entities mutably.
682 pub fn iter_riders_mut(&mut self) -> impl Iterator<Item = (EntityId, &mut Rider)> {
683 self.riders.iter_mut()
684 }
685
686 /// Iterate all rider entity IDs without allocating.
687 ///
688 /// Companion to [`iter_riders`](Self::iter_riders) when only the IDs
689 /// are needed.
690 pub fn iter_rider_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
691 self.riders.keys()
692 }
693
694 /// Iterate all rider entity IDs (allocates).
695 #[must_use]
696 pub fn rider_ids(&self) -> Vec<EntityId> {
697 self.riders.keys().collect()
698 }
699
700 /// Iterate all stop entities.
701 pub fn iter_stops(&self) -> impl Iterator<Item = (EntityId, &Stop)> {
702 self.stops.iter()
703 }
704
705 /// Iterate all stop entity IDs without allocating.
706 ///
707 /// Companion to [`iter_stops`](Self::iter_stops) when only the IDs
708 /// are needed.
709 pub fn iter_stop_ids(&self) -> impl Iterator<Item = EntityId> + '_ {
710 self.stops.keys()
711 }
712
713 /// Iterate all stop entity IDs (allocates).
714 #[must_use]
715 pub fn stop_ids(&self) -> Vec<EntityId> {
716 self.stops.keys().collect()
717 }
718
719 /// Iterate elevators in `Idle` phase (not disabled).
720 pub fn iter_idle_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
721 use crate::components::ElevatorPhase;
722 self.iter_elevators()
723 .filter(|(id, _, car)| car.phase == ElevatorPhase::Idle && !self.is_disabled(*id))
724 }
725
726 /// Iterate elevators that are currently moving — either on a dispatched
727 /// trip (`MovingToStop`) or a repositioning trip (`Repositioning`).
728 /// Excludes disabled elevators.
729 pub fn iter_moving_elevators(&self) -> impl Iterator<Item = (EntityId, &Position, &Elevator)> {
730 self.iter_elevators()
731 .filter(|(id, _, car)| car.phase.is_moving() && !self.is_disabled(*id))
732 }
733
734 /// Iterate riders in `Waiting` phase (not disabled).
735 pub fn iter_waiting_riders(&self) -> impl Iterator<Item = (EntityId, &Rider)> {
736 use crate::components::RiderPhase;
737 self.iter_riders()
738 .filter(|(id, r)| r.phase == RiderPhase::Waiting && !self.is_disabled(*id))
739 }
740
741 /// Find the stop entity at a given position (within
742 /// [`STOP_POSITION_EPSILON`](Self::STOP_POSITION_EPSILON)).
743 ///
744 /// Global lookup — does not filter by line. When two stops on
745 /// different lines share the same physical position the result is
746 /// whichever wins the linear scan, which is rarely what the
747 /// caller actually wants. Prefer
748 /// [`find_stop_at_position_in`](Self::find_stop_at_position_in)
749 /// when the caller knows which line's stops to consider.
750 #[must_use]
751 pub fn find_stop_at_position(&self, position: f64) -> Option<EntityId> {
752 self.stops.iter().find_map(|(id, stop)| {
753 if (stop.position - position).abs() < Self::STOP_POSITION_EPSILON {
754 Some(id)
755 } else {
756 None
757 }
758 })
759 }
760
761 /// Find the stop at a given position from within `candidates`.
762 ///
763 /// `candidates` is typically the `serves` list of a particular
764 /// [`LineInfo`](crate::dispatch::LineInfo) — i.e. the stops a
765 /// specific line can reach. Use this when a car arrives at a
766 /// position and you need *its* line's stop entity, not whichever
767 /// stop on any line happens to share the position. (Two parallel
768 /// shafts at the same physical floor, or a sky-lobby served by
769 /// both a low and high bank, both produce position collisions
770 /// the global lookup can't disambiguate.)
771 ///
772 /// O(n) over `candidates`, which is typically small.
773 #[must_use]
774 pub fn find_stop_at_position_in(
775 &self,
776 position: f64,
777 candidates: &[EntityId],
778 ) -> Option<EntityId> {
779 candidates.iter().copied().find(|&id| {
780 self.stops
781 .get(id)
782 .is_some_and(|stop| (stop.position - position).abs() < Self::STOP_POSITION_EPSILON)
783 })
784 }
785
786 /// Tolerance for [`find_stop_at_position`](Self::find_stop_at_position)
787 /// and [`find_stop_at_position_in`](Self::find_stop_at_position_in).
788 /// Sub-micrometre — small enough that no two distinct floors should
789 /// land within it, large enough to absorb floating-point noise from
790 /// trapezoidal-velocity arrival math.
791 pub const STOP_POSITION_EPSILON: f64 = 1e-6;
792
793 /// Find the stop entity nearest to a given position.
794 ///
795 /// Unlike [`find_stop_at_position`](Self::find_stop_at_position), this finds
796 /// the closest stop by minimum distance rather than requiring an exact match.
797 /// Used when ejecting riders from a disabled/despawned elevator mid-transit.
798 #[must_use]
799 pub fn find_nearest_stop(&self, position: f64) -> Option<EntityId> {
800 self.stops
801 .iter()
802 .min_by(|(_, a), (_, b)| {
803 (a.position - position)
804 .abs()
805 .total_cmp(&(b.position - position).abs())
806 })
807 .map(|(id, _)| id)
808 }
809
810 /// Get a stop's position by entity id.
811 #[must_use]
812 pub fn stop_position(&self, id: EntityId) -> Option<f64> {
813 self.stops.get(id).map(|s| s.position)
814 }
815
816 // ── Extension (custom component) storage ─────────────────────────
817
818 /// Insert a custom component for an entity.
819 ///
820 /// Games use this to attach their own typed data to simulation entities.
821 /// Extension components must be `Serialize + DeserializeOwned` to support
822 /// snapshot save/load. An [`ExtKey`] is required for serialization roundtrips.
823 /// Extension components are automatically cleaned up on `despawn()`.
824 ///
825 /// ```
826 /// use elevator_core::world::{ExtKey, World};
827 /// use serde::{Serialize, Deserialize};
828 ///
829 /// #[derive(Debug, Clone, Serialize, Deserialize)]
830 /// struct VipTag { level: u32 }
831 ///
832 /// let mut world = World::new();
833 /// let entity = world.spawn();
834 /// world.insert_ext(entity, VipTag { level: 3 }, ExtKey::from_type_name());
835 /// ```
836 pub fn insert_ext<T: 'static + Send + Sync + serde::Serialize + serde::de::DeserializeOwned>(
837 &mut self,
838 id: EntityId,
839 value: T,
840 key: ExtKey<T>,
841 ) {
842 let type_id = TypeId::of::<T>();
843 Self::assert_ext_name_unique(&self.ext_names, type_id, key.name());
844 let map = self
845 .extensions
846 .entry(type_id)
847 .or_insert_with(|| Box::new(SecondaryMap::<EntityId, T>::new()));
848 if let Some(m) = map.as_any_mut().downcast_mut::<SecondaryMap<EntityId, T>>() {
849 m.insert(id, value);
850 }
851 self.ext_names.insert(type_id, key.name().to_owned());
852 }
853
854 /// Get a clone of a custom component for an entity.
855 #[must_use]
856 pub fn ext<T: 'static + Send + Sync + Clone>(&self, id: EntityId) -> Option<T> {
857 self.ext_map::<T>()?.get(id).cloned()
858 }
859
860 /// Shared reference to a custom component for an entity.
861 ///
862 /// Zero-copy alternative to [`ext`](Self::ext): prefer this when
863 /// `T` is large or expensive to clone, or when the caller only needs a
864 /// borrow. Unlike `ext`, `T` does not need to implement `Clone`.
865 #[must_use]
866 pub fn ext_ref<T: 'static + Send + Sync>(&self, id: EntityId) -> Option<&T> {
867 self.ext_map::<T>()?.get(id)
868 }
869
870 /// Mutable reference to a custom component for an entity.
871 pub fn ext_mut<T: 'static + Send + Sync>(&mut self, id: EntityId) -> Option<&mut T> {
872 self.ext_map_mut::<T>()?.get_mut(id)
873 }
874
875 /// Remove a custom component for an entity.
876 pub fn remove_ext<T: 'static + Send + Sync>(&mut self, id: EntityId) -> Option<T> {
877 self.ext_map_mut::<T>()?.remove(id)
878 }
879
880 /// Downcast extension storage to a typed `SecondaryMap` (shared).
881 pub(crate) fn ext_map<T: 'static + Send + Sync>(&self) -> Option<&SecondaryMap<EntityId, T>> {
882 self.extensions
883 .get(&TypeId::of::<T>())?
884 .as_any()
885 .downcast_ref::<SecondaryMap<EntityId, T>>()
886 }
887
888 /// Downcast extension storage to a typed `SecondaryMap` (mutable).
889 fn ext_map_mut<T: 'static + Send + Sync>(&mut self) -> Option<&mut SecondaryMap<EntityId, T>> {
890 self.extensions
891 .get_mut(&TypeId::of::<T>())?
892 .as_any_mut()
893 .downcast_mut::<SecondaryMap<EntityId, T>>()
894 }
895
896 /// Serialize all extension component data for snapshot.
897 /// Returns name → (`EntityId` → RON string) mapping. `BTreeMap` for
898 /// deterministic snapshot bytes across processes (#254).
899 pub(crate) fn serialize_extensions(&self) -> BTreeMap<String, BTreeMap<EntityId, String>> {
900 let mut result = BTreeMap::new();
901 for (type_id, map) in &self.extensions {
902 if let Some(name) = self.ext_names.get(type_id) {
903 result.insert(name.clone(), map.serialize_entries());
904 }
905 }
906 result
907 }
908
909 /// Deserialize extension data from snapshot. Requires that extension types
910 /// have been registered (via `register_ext_deserializer`) before calling.
911 pub(crate) fn deserialize_extensions(
912 &mut self,
913 data: &BTreeMap<String, BTreeMap<EntityId, String>>,
914 ) {
915 for (name, entries) in data {
916 // Find the TypeId by name.
917 if let Some((&type_id, _)) = self.ext_names.iter().find(|(_, n)| *n == name)
918 && let Some(map) = self.extensions.get_mut(&type_id)
919 {
920 map.deserialize_entries(entries);
921 }
922 }
923 }
924
925 /// Return names from `snapshot_names` that have no registered extension type.
926 pub(crate) fn unregistered_ext_names<'a>(
927 &self,
928 snapshot_names: impl Iterator<Item = &'a String>,
929 ) -> Vec<String> {
930 let registered: std::collections::HashSet<&str> =
931 self.ext_names.values().map(String::as_str).collect();
932 snapshot_names
933 .filter(|name| !registered.contains(name.as_str()))
934 .cloned()
935 .collect()
936 }
937
938 /// Register an extension type for deserialization (creates empty storage).
939 ///
940 /// Must be called before `restore()` for each extension type that was
941 /// present in the original simulation. Returns the key for convenience.
942 pub fn register_ext<
943 T: 'static + Send + Sync + serde::Serialize + serde::de::DeserializeOwned,
944 >(
945 &mut self,
946 key: ExtKey<T>,
947 ) -> ExtKey<T> {
948 let type_id = TypeId::of::<T>();
949 Self::assert_ext_name_unique(&self.ext_names, type_id, key.name());
950 self.extensions
951 .entry(type_id)
952 .or_insert_with(|| Box::new(SecondaryMap::<EntityId, T>::new()));
953 self.ext_names.insert(type_id, key.name().to_owned());
954 key
955 }
956
957 /// Panic if `name` is already registered to a different `TypeId`.
958 ///
959 /// Two extension types sharing one `ExtKey` name silently corrupts
960 /// snapshot serde: `serialize_extensions` collapses both types' data
961 /// into one slot in the result map, and `deserialize_extensions`
962 /// routes the data to whichever `TypeId` `HashMap::iter().find` returns
963 /// first — a non-deterministic choice. Failing fast here prevents
964 /// the corruption from ever being written.
965 ///
966 /// Panic chosen over `Result` because [`register_ext`](Self::register_ext)
967 /// and [`insert_ext`](Self::insert_ext) are non-fallible by design and
968 /// changing their signatures would break every consumer. Name collisions
969 /// are programmer errors discoverable at startup, not runtime conditions
970 /// to recover from.
971 #[allow(clippy::panic)]
972 fn assert_ext_name_unique(ext_names: &HashMap<TypeId, String>, type_id: TypeId, name: &str) {
973 if let Some((existing_tid, _)) = ext_names
974 .iter()
975 .find(|(tid, n)| **tid != type_id && n.as_str() == name)
976 {
977 panic!(
978 "ExtKey name {name:?} is already registered to a different type \
979 ({existing_tid:?}); each extension type needs a unique key name. \
980 If renaming is impractical, use ExtKey::from_type_name() so the \
981 name embeds the fully-qualified Rust type name."
982 );
983 }
984 }
985
986 // ── Disabled entity management ──────────────────────────────────
987
988 /// Mark an entity as disabled. Disabled entities are skipped by all systems.
989 pub fn disable(&mut self, id: EntityId) {
990 self.disabled.insert(id, ());
991 }
992
993 /// Re-enable a disabled entity.
994 pub fn enable(&mut self, id: EntityId) {
995 self.disabled.remove(id);
996 }
997
998 /// Check if an entity is disabled.
999 #[must_use]
1000 pub fn is_disabled(&self, id: EntityId) -> bool {
1001 self.disabled.contains_key(id)
1002 }
1003
1004 // ── Global resources (singletons) ───────────────────────────────
1005
1006 /// Insert a global resource. Replaces any existing resource of the same type.
1007 ///
1008 /// Resources are singletons not attached to any entity. Games use them
1009 /// for event channels, score trackers, or any global state.
1010 ///
1011 /// ```
1012 /// use elevator_core::world::World;
1013 /// use elevator_core::events::EventChannel;
1014 ///
1015 /// #[derive(Debug)]
1016 /// enum MyEvent { Score(u32) }
1017 ///
1018 /// let mut world = World::new();
1019 /// world.insert_resource(EventChannel::<MyEvent>::new());
1020 /// ```
1021 pub fn insert_resource<T: 'static + Send + Sync>(&mut self, value: T) {
1022 self.resources.insert(TypeId::of::<T>(), Box::new(value));
1023 }
1024
1025 /// Get a shared reference to a global resource.
1026 #[must_use]
1027 pub fn resource<T: 'static + Send + Sync>(&self) -> Option<&T> {
1028 self.resources.get(&TypeId::of::<T>())?.downcast_ref()
1029 }
1030
1031 /// Get a mutable reference to a global resource.
1032 pub fn resource_mut<T: 'static + Send + Sync>(&mut self) -> Option<&mut T> {
1033 self.resources.get_mut(&TypeId::of::<T>())?.downcast_mut()
1034 }
1035
1036 /// Remove a global resource, returning it if it existed.
1037 pub fn remove_resource<T: 'static + Send + Sync>(&mut self) -> Option<T> {
1038 self.resources
1039 .remove(&TypeId::of::<T>())
1040 .and_then(|b| b.downcast().ok())
1041 .map(|b| *b)
1042 }
1043
1044 // ── Query builder ───────────────────────────────────────────────
1045
1046 /// Create a query builder for iterating entities by component composition.
1047 ///
1048 /// ```
1049 /// use elevator_core::components::{Position, Rider};
1050 /// use elevator_core::prelude::*;
1051 ///
1052 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1053 /// sim.spawn_rider(StopId(0), StopId(1), 75.0).unwrap();
1054 ///
1055 /// let world = sim.world();
1056 /// for (id, rider, pos) in world.query::<(EntityId, &Rider, &Position)>().iter() {
1057 /// println!("{id:?}: {:?} at {}", rider.phase(), pos.value());
1058 /// }
1059 /// ```
1060 #[must_use]
1061 pub const fn query<Q: crate::query::WorldQuery>(&self) -> crate::query::QueryBuilder<'_, Q> {
1062 crate::query::QueryBuilder::new(self)
1063 }
1064
1065 /// Create a mutable extension query builder.
1066 ///
1067 /// Uses the keys-snapshot pattern: collects matching entity IDs upfront
1068 /// into an owned `Vec`, then iterates with mutable access via
1069 /// [`for_each_mut`](crate::query::ExtQueryMut::for_each_mut).
1070 ///
1071 /// # Example
1072 ///
1073 /// ```no_run
1074 /// # use elevator_core::prelude::*;
1075 /// # use serde::{Serialize, Deserialize};
1076 /// # #[derive(Clone, Serialize, Deserialize)] struct VipTag { level: u32 }
1077 /// # fn run(world: &mut World) {
1078 /// world.query_ext_mut::<VipTag>().for_each_mut(|id, tag| {
1079 /// tag.level += 1;
1080 /// });
1081 /// # }
1082 /// ```
1083 pub fn query_ext_mut<T: 'static + Send + Sync>(&mut self) -> crate::query::ExtQueryMut<'_, T> {
1084 crate::query::ExtQueryMut::new(self)
1085 }
1086}
1087
1088impl Default for World {
1089 fn default() -> Self {
1090 Self::new()
1091 }
1092}
1093
1094/// Stops sorted by position for efficient range queries (binary search).
1095///
1096/// Used by the movement system to detect `PassingFloor` events in O(log n)
1097/// instead of O(n) per moving elevator per tick.
1098pub(crate) struct SortedStops(pub(crate) Vec<(f64, EntityId)>);
1099
1100/// The up/down hall call pair at a single stop.
1101///
1102/// At most two calls coexist at a stop (one per [`CallDirection`]);
1103/// this struct owns the slots. Stored in `World::hall_calls` keyed by
1104/// the stop's entity id.
1105#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
1106pub struct StopCalls {
1107 /// Pending upward call, if the up button is pressed.
1108 pub up: Option<HallCall>,
1109 /// Pending downward call, if the down button is pressed.
1110 pub down: Option<HallCall>,
1111}
1112
1113impl StopCalls {
1114 /// Borrow the call for a specific direction.
1115 #[must_use]
1116 pub const fn get(&self, direction: CallDirection) -> Option<&HallCall> {
1117 match direction {
1118 CallDirection::Up => self.up.as_ref(),
1119 CallDirection::Down => self.down.as_ref(),
1120 }
1121 }
1122
1123 /// Mutable borrow of the call for a direction.
1124 pub const fn get_mut(&mut self, direction: CallDirection) -> Option<&mut HallCall> {
1125 match direction {
1126 CallDirection::Up => self.up.as_mut(),
1127 CallDirection::Down => self.down.as_mut(),
1128 }
1129 }
1130
1131 /// Iterate both calls in (Up, Down) order, skipping empty slots.
1132 pub fn iter(&self) -> impl Iterator<Item = &HallCall> {
1133 self.up.iter().chain(self.down.iter())
1134 }
1135
1136 /// Mutable iteration over both calls.
1137 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut HallCall> {
1138 self.up.iter_mut().chain(self.down.iter_mut())
1139 }
1140}