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(crate::fp::fma(current - prev, 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.
130 ///
131 /// [`set_reposition`](super::Simulation::set_reposition) auto-widens
132 /// retention to the installed strategy's
133 /// [`min_arrival_log_window`](crate::dispatch::RepositionStrategy::min_arrival_log_window),
134 /// so most callers don't need this. Reach for it only when retention
135 /// must differ from any strategy's window — tests, custom consumers
136 /// reading [`ArrivalLog`](crate::arrival_log::ArrivalLog) directly,
137 /// or to pre-stage retention before installing a strategy.
138 ///
139 /// ## Footgun
140 /// Calling this *after* `set_reposition` with a value smaller than
141 /// the installed strategy's window silently re-introduces the
142 /// truncation bug `set_reposition` was designed to avoid: the
143 /// strategy will see only the last `retention_ticks` of arrivals,
144 /// not its configured window. The setter trusts the caller; if you
145 /// only want to ensure retention is at least N ticks, do
146 /// `max(N, current_retention)` at the call site.
147 pub fn set_arrival_log_retention_ticks(&mut self, retention_ticks: u64) {
148 if let Some(r) = self
149 .world
150 .resource_mut::<crate::arrival_log::ArrivalLogRetention>()
151 {
152 r.0 = retention_ticks;
153 }
154 }
155
156 /// Mutable access to the group collection. Use this to flip a group
157 /// into [`HallCallMode::Destination`](crate::dispatch::HallCallMode)
158 /// or tune its `ack_latency_ticks` after construction. Changing the
159 /// line/elevator structure here is not supported — use the dedicated
160 /// topology mutators for that.
161 pub fn groups_mut(&mut self) -> &mut [ElevatorGroup] {
162 &mut self.groups
163 }
164
165 /// Resolve a config `StopId` to its runtime `EntityId`.
166 #[must_use]
167 pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
168 self.stop_lookup.get(&id).copied()
169 }
170
171 /// Resolve a [`StopRef`] to its runtime [`EntityId`].
172 pub(super) fn resolve_stop(&self, stop: StopRef) -> Result<EntityId, SimError> {
173 match stop {
174 StopRef::ByEntity(id) => Ok(id),
175 StopRef::ById(sid) => self.stop_entity(sid).ok_or(SimError::StopNotFound(sid)),
176 }
177 }
178
179 /// Get the strategy identifier for a group.
180 #[must_use]
181 pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
182 self.strategy_ids.get(&group)
183 }
184
185 /// Iterate over the stop ID → entity ID mapping.
186 pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
187 self.stop_lookup.iter()
188 }
189
190 /// Peek at events pending for consumer retrieval.
191 ///
192 /// Flushes the active event bus into the output buffer first so the
193 /// returned slice reflects every event emitted up to this call —
194 /// matching what [`drain_events`](Self::drain_events) would return.
195 /// Without the flush, events emitted outside the tick loop
196 /// (`spawn_rider`, `disable`, …) would be invisible to peek but
197 /// visible to drain — observed during round-2 audit (#264).
198 ///
199 /// Takes `&mut self` because the flush mutates internal state. If
200 /// you only need a count or a quick check after `step()`, prefer
201 /// `pending_events().len()` or pattern-matching the slice directly.
202 pub fn pending_events(&mut self) -> &[Event] {
203 self.pending_output.extend(self.events.drain());
204 &self.pending_output
205 }
206}