Skip to main content

elevator_core/sim/
accessors.rs

1//! Core accessors: world, tick, metrics, groups, stop lookup.
2//!
3//! Part of the [`super::Simulation`] API surface; extracted from the
4//! monolithic `sim.rs` for readability. See the parent module for the
5//! overarching essential-API summary.
6
7use crate::components::Velocity;
8use crate::dispatch::ElevatorGroup;
9use crate::entity::EntityId;
10use crate::error::SimError;
11use crate::events::Event;
12use crate::ids::GroupId;
13use crate::metrics::Metrics;
14use crate::stop::{StopId, StopRef};
15use crate::time::TimeAdapter;
16use crate::world::World;
17
18impl super::Simulation {
19    // ── Accessors ────────────────────────────────────────────────────
20
21    /// Get a shared reference to the world.
22    //
23    // Intentionally non-`const`: a `const` qualifier on a runtime accessor
24    // signals "usable in const context", which these methods are not in
25    // practice (the `World` is heap-allocated and mutated). Marking them
26    // `const` misleads readers without unlocking any call sites.
27    #[must_use]
28    #[allow(clippy::missing_const_for_fn)]
29    pub fn world(&self) -> &World {
30        &self.world
31    }
32
33    /// Get a mutable reference to the world.
34    ///
35    /// Exposed for advanced use cases (manual rider management, custom
36    /// component attachment). Prefer `spawn_rider` / `build_rider`
37    /// for standard operations.
38    #[allow(clippy::missing_const_for_fn)]
39    pub fn world_mut(&mut self) -> &mut World {
40        &mut self.world
41    }
42
43    /// Current simulation tick.
44    #[must_use]
45    pub const fn current_tick(&self) -> u64 {
46        self.tick
47    }
48
49    /// Time delta per tick (seconds).
50    #[must_use]
51    pub const fn dt(&self) -> f64 {
52        self.dt
53    }
54
55    /// Interpolated position between the previous and current tick.
56    ///
57    /// `alpha` is clamped to `[0.0, 1.0]`, where `0.0` returns the entity's
58    /// position at the start of the last completed tick and `1.0` returns
59    /// the current position. Intended for smooth rendering when a render
60    /// frame falls between simulation ticks.
61    ///
62    /// Returns `None` if the entity has no position component. Returns the
63    /// current position unchanged if no previous snapshot exists (i.e. before
64    /// the first [`step`](Self::step)).
65    ///
66    /// [`step`]: Self::step
67    #[must_use]
68    pub fn position_at(&self, id: EntityId, alpha: f64) -> Option<f64> {
69        let current = self.world.position(id)?.value;
70        let alpha = if alpha.is_nan() {
71            0.0
72        } else {
73            alpha.clamp(0.0, 1.0)
74        };
75        let prev = self.world.prev_position(id).map_or(current, |p| p.value);
76        Some((current - prev).mul_add(alpha, prev))
77    }
78
79    /// Current velocity of an entity along the shaft axis (signed: +up, -down).
80    ///
81    /// Convenience wrapper over [`World::velocity`] that returns the raw
82    /// `f64` value. Returns `None` if the entity has no velocity component.
83    #[must_use]
84    pub fn velocity(&self, id: EntityId) -> Option<f64> {
85        self.world.velocity(id).map(Velocity::value)
86    }
87
88    /// Get current simulation metrics.
89    #[must_use]
90    pub const fn metrics(&self) -> &Metrics {
91        &self.metrics
92    }
93
94    /// The time adapter for tick↔wall-clock conversion.
95    #[must_use]
96    pub const fn time(&self) -> &TimeAdapter {
97        &self.time
98    }
99
100    /// Get the elevator groups.
101    #[must_use]
102    pub fn groups(&self) -> &[ElevatorGroup] {
103        &self.groups
104    }
105
106    /// Build the [`DispatchManifest`](crate::dispatch::DispatchManifest)
107    /// for `group` as it would appear at the start of the next dispatch
108    /// pass. Intended for tests and tools that need to inspect the
109    /// demand/arrival-rate picture without stepping the sim.
110    #[must_use]
111    pub fn build_dispatch_manifest(
112        &self,
113        group: &ElevatorGroup,
114    ) -> crate::dispatch::DispatchManifest {
115        crate::systems::dispatch::build_manifest(&self.world, group, self.tick, &self.rider_index)
116    }
117
118    /// Convenience wrapper returning the manifest for the first group —
119    /// what a single-group default-topology sim would dispatch against.
120    #[must_use]
121    pub fn peek_dispatch_manifest(&self) -> crate::dispatch::DispatchManifest {
122        self.groups
123            .first()
124            .map(|g| self.build_dispatch_manifest(g))
125            .unwrap_or_default()
126    }
127
128    /// Set how far back the arrival log retains entries before
129    /// `advance_tick` prunes them. Strategies querying a window longer
130    /// than the default (`5` minutes at 60 Hz, see
131    /// [`DEFAULT_ARRIVAL_WINDOW_TICKS`](crate::arrival_log::DEFAULT_ARRIVAL_WINDOW_TICKS))
132    /// must call this with a matching retention or they will silently
133    /// see only the last 5 minutes of arrivals.
134    pub fn set_arrival_log_retention_ticks(&mut self, retention_ticks: u64) {
135        if let Some(r) = self
136            .world
137            .resource_mut::<crate::arrival_log::ArrivalLogRetention>()
138        {
139            r.0 = retention_ticks;
140        }
141    }
142
143    /// Mutable access to the group collection. Use this to flip a group
144    /// into [`HallCallMode::Destination`](crate::dispatch::HallCallMode)
145    /// or tune its `ack_latency_ticks` after construction. Changing the
146    /// line/elevator structure here is not supported — use the dedicated
147    /// topology mutators for that.
148    pub fn groups_mut(&mut self) -> &mut [ElevatorGroup] {
149        &mut self.groups
150    }
151
152    /// Resolve a config `StopId` to its runtime `EntityId`.
153    #[must_use]
154    pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
155        self.stop_lookup.get(&id).copied()
156    }
157
158    /// Resolve a [`StopRef`] to its runtime [`EntityId`].
159    pub(super) fn resolve_stop(&self, stop: StopRef) -> Result<EntityId, SimError> {
160        match stop {
161            StopRef::ByEntity(id) => Ok(id),
162            StopRef::ById(sid) => self.stop_entity(sid).ok_or(SimError::StopNotFound(sid)),
163        }
164    }
165
166    /// Get the strategy identifier for a group.
167    #[must_use]
168    pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
169        self.strategy_ids.get(&group)
170    }
171
172    /// Iterate over the stop ID → entity ID mapping.
173    pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
174        self.stop_lookup.iter()
175    }
176
177    /// Peek at events pending for consumer retrieval.
178    ///
179    /// Flushes the active event bus into the output buffer first so the
180    /// returned slice reflects every event emitted up to this call —
181    /// matching what [`drain_events`](Self::drain_events) would return.
182    /// Without the flush, events emitted outside the tick loop
183    /// (`spawn_rider`, `disable`, …) would be invisible to peek but
184    /// visible to drain — observed during round-2 audit (#264).
185    ///
186    /// Takes `&mut self` because the flush mutates internal state. If
187    /// you only need a count or a quick check after `step()`, prefer
188    /// `pending_events().len()` or pattern-matching the slice directly.
189    pub fn pending_events(&mut self) -> &[Event] {
190        self.pending_output.extend(self.events.drain());
191        &self.pending_output
192    }
193}