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