elevator_core/sim/rider.rs
1//! Rider spawning, routing, and lifecycle management.
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::{CallDirection, Rider, RiderPhase, Route, Weight};
8use crate::dispatch::{ElevatorGroup, HallCallMode};
9use crate::entity::{EntityId, RiderId};
10use crate::error::SimError;
11use crate::events::Event;
12use crate::ids::GroupId;
13use crate::stop::StopRef;
14
15impl super::Simulation {
16 // ── Rider spawning ───────────────────────────────────────────────
17
18 /// Create a rider builder for fluent rider spawning.
19 ///
20 /// Accepts [`EntityId`] or [`StopId`](crate::stop::StopId) for origin and destination
21 /// (anything that implements `Into<StopRef>`).
22 ///
23 /// # Errors
24 ///
25 /// Returns [`SimError::StopNotFound`] if a [`StopId`](crate::stop::StopId) does not exist
26 /// in the building configuration.
27 ///
28 /// ```
29 /// use elevator_core::prelude::*;
30 ///
31 /// let mut sim = SimulationBuilder::demo().build().unwrap();
32 /// let rider = sim.build_rider(StopId(0), StopId(1))
33 /// .unwrap()
34 /// .weight(80.0)
35 /// .spawn()
36 /// .unwrap();
37 /// ```
38 pub fn build_rider(
39 &mut self,
40 origin: impl Into<StopRef>,
41 destination: impl Into<StopRef>,
42 ) -> Result<super::RiderBuilder<'_>, SimError> {
43 let origin = self.resolve_stop(origin.into())?;
44 let destination = self.resolve_stop(destination.into())?;
45 Ok(super::RiderBuilder {
46 sim: self,
47 origin,
48 destination,
49 weight: Weight::from(75.0),
50 group: None,
51 route: None,
52 patience: None,
53 preferences: None,
54 access_control: None,
55 })
56 }
57
58 /// Spawn a rider with default preferences (convenience shorthand).
59 ///
60 /// Equivalent to `build_rider(origin, destination)?.weight(weight).spawn()`.
61 /// Use [`build_rider`](Self::build_rider) instead when you need to set
62 /// patience, preferences, access control, or an explicit route.
63 ///
64 /// Auto-detects the elevator group by finding groups that serve both origin
65 /// and destination stops.
66 ///
67 /// # Errors
68 ///
69 /// Returns [`SimError::NoRoute`] if no group serves both stops.
70 /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops.
71 pub fn spawn_rider(
72 &mut self,
73 origin: impl Into<StopRef>,
74 destination: impl Into<StopRef>,
75 weight: impl Into<Weight>,
76 ) -> Result<RiderId, SimError> {
77 let origin = self.resolve_stop(origin.into())?;
78 let destination = self.resolve_stop(destination.into())?;
79 // Same origin & destination = no hall call gets registered (the
80 // direction is undefined), so the rider would sit Waiting forever
81 // while inflating `total_spawned`. Reject up front. (#273)
82 if origin == destination {
83 return Err(SimError::InvalidConfig {
84 field: "destination",
85 reason: "origin and destination must differ; same-stop \
86 spawns deadlock with no hall call to summon a car"
87 .into(),
88 });
89 }
90 let weight: Weight = weight.into();
91 let group = self.auto_detect_group(origin, destination)?;
92
93 let route = Route::direct(origin, destination, group);
94 Ok(RiderId::from(self.spawn_rider_inner(
95 origin,
96 destination,
97 weight,
98 route,
99 )))
100 }
101
102 /// Find the single group that serves both `origin` and `destination`.
103 ///
104 /// Returns `Ok(group)` when exactly one group serves both stops.
105 /// Returns [`SimError::NoRoute`] when no group does.
106 /// Returns [`SimError::AmbiguousRoute`] when more than one does.
107 pub(super) fn auto_detect_group(
108 &self,
109 origin: EntityId,
110 destination: EntityId,
111 ) -> Result<GroupId, SimError> {
112 let matching: Vec<GroupId> = self
113 .groups
114 .iter()
115 .filter(|g| {
116 g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
117 })
118 .map(ElevatorGroup::id)
119 .collect();
120
121 match matching.len() {
122 0 => {
123 let origin_groups: Vec<GroupId> = self
124 .groups
125 .iter()
126 .filter(|g| g.stop_entities().contains(&origin))
127 .map(ElevatorGroup::id)
128 .collect();
129 let destination_groups: Vec<GroupId> = self
130 .groups
131 .iter()
132 .filter(|g| g.stop_entities().contains(&destination))
133 .map(ElevatorGroup::id)
134 .collect();
135 Err(SimError::NoRoute {
136 origin,
137 destination,
138 origin_groups,
139 destination_groups,
140 })
141 }
142 1 => Ok(matching[0]),
143 _ => Err(SimError::AmbiguousRoute {
144 origin,
145 destination,
146 groups: matching,
147 }),
148 }
149 }
150
151 /// Internal helper: spawn a rider entity with the given route.
152 pub(super) fn spawn_rider_inner(
153 &mut self,
154 origin: EntityId,
155 destination: EntityId,
156 weight: Weight,
157 route: Route,
158 ) -> EntityId {
159 let eid = self.world.spawn();
160 self.world.set_rider(
161 eid,
162 Rider {
163 weight,
164 phase: RiderPhase::Waiting,
165 current_stop: Some(origin),
166 spawn_tick: self.tick,
167 board_tick: None,
168 tag: 0,
169 },
170 );
171 self.world.set_route(eid, route);
172 self.rider_index.insert_waiting(origin, eid);
173 if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
174 log.record(self.tick, origin);
175 }
176 if let Some(log) = self
177 .world
178 .resource_mut::<crate::arrival_log::DestinationLog>()
179 {
180 log.record(self.tick, destination);
181 }
182 self.events.emit(Event::RiderSpawned {
183 rider: eid,
184 origin,
185 destination,
186 tick: self.tick,
187 });
188
189 // Auto-press the hall button for this rider. Direction is the
190 // sign of `dest_pos - origin_pos`; if the two coincide (walk
191 // leg, identity trip) no call is registered.
192 if let (Some(op), Some(dp)) = (
193 self.world.stop_position(origin),
194 self.world.stop_position(destination),
195 ) && let Some(direction) = CallDirection::between(op, dp)
196 {
197 self.register_hall_call_for_rider(origin, direction, eid, destination);
198 }
199
200 // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
201 let stop_tag = self
202 .world
203 .stop(origin)
204 .map(|s| format!("stop:{}", s.name()));
205
206 // Inherit metric tags from the origin stop.
207 if let Some(tags_res) = self
208 .world
209 .resource_mut::<crate::tagged_metrics::MetricTags>()
210 {
211 let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
212 for tag in origin_tags {
213 tags_res.tag(eid, tag);
214 }
215 // Apply the origin stop tag.
216 if let Some(tag) = stop_tag {
217 tags_res.tag(eid, tag);
218 }
219 }
220
221 eid
222 }
223
224 /// Drain all pending events from completed ticks.
225 ///
226 /// Events emitted during `step()` (or per-phase methods) are buffered
227 /// and made available here after `advance_tick()` is called.
228 /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
229 /// are also included.
230 ///
231 /// ```
232 /// use elevator_core::prelude::*;
233 ///
234 /// let mut sim = SimulationBuilder::demo().build().unwrap();
235 ///
236 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
237 /// sim.step();
238 ///
239 /// let events = sim.drain_events();
240 /// assert!(!events.is_empty());
241 /// ```
242 pub fn drain_events(&mut self) -> Vec<Event> {
243 // Flush any events still in the bus (from spawn_rider, disable, etc.)
244 self.pending_output.extend(self.events.drain());
245 std::mem::take(&mut self.pending_output)
246 }
247
248 /// Push an event into the pending output buffer (crate-internal).
249 pub(crate) fn push_event(&mut self, event: Event) {
250 self.pending_output.push(event);
251 }
252
253 /// Drain only events matching a predicate.
254 ///
255 /// Events that don't match the predicate remain in the buffer
256 /// and will be returned by future `drain_events` or
257 /// `drain_events_where` calls.
258 ///
259 /// ```
260 /// use elevator_core::prelude::*;
261 ///
262 /// let mut sim = SimulationBuilder::demo().build().unwrap();
263 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
264 /// sim.step();
265 ///
266 /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
267 /// matches!(e, Event::RiderSpawned { .. })
268 /// });
269 /// ```
270 pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
271 // Flush bus into pending_output first.
272 self.pending_output.extend(self.events.drain());
273
274 let mut matched = Vec::new();
275 let mut remaining = Vec::new();
276 for event in std::mem::take(&mut self.pending_output) {
277 if predicate(&event) {
278 matched.push(event);
279 } else {
280 remaining.push(event);
281 }
282 }
283 self.pending_output = remaining;
284 matched
285 }
286
287 // ── Rider tag (opaque consumer-attached id) ──────────────────────
288
289 /// Read the opaque tag attached to a rider.
290 ///
291 /// Consumers use [`set_rider_tag`](Self::set_rider_tag) to stash an
292 /// external identifier on the rider (a game-side sim id, a player
293 /// id, a freight shipment id) and read it back here without keeping
294 /// a parallel `RiderId → u64` map. The engine never interprets the
295 /// value; it survives snapshot round-trip.
296 ///
297 /// Returns `0` for the default "untagged" state.
298 ///
299 /// # Errors
300 ///
301 /// Returns [`SimError::EntityNotFound`] if `id` does not correspond
302 /// to a live rider.
303 pub fn rider_tag(&self, id: RiderId) -> Result<u64, SimError> {
304 let eid = id.entity();
305 self.world
306 .rider(eid)
307 .map(Rider::tag)
308 .ok_or(SimError::EntityNotFound(eid))
309 }
310
311 /// Attach an opaque tag to a rider. The engine doesn't interpret the
312 /// value — pick whatever encoding your consumer needs (e.g. a 32-bit
313 /// external id zero-extended to `u64`, or two 32-bit half-words).
314 /// Pass `0` to clear the tag (the reserved "untagged" sentinel).
315 ///
316 /// # Errors
317 ///
318 /// Returns [`SimError::EntityNotFound`] if `id` does not correspond
319 /// to a live rider.
320 pub fn set_rider_tag(&mut self, id: RiderId, tag: u64) -> Result<(), SimError> {
321 let eid = id.entity();
322 let rider = self
323 .world
324 .rider_mut(eid)
325 .ok_or(SimError::EntityNotFound(eid))?;
326 rider.tag = tag;
327 Ok(())
328 }
329
330 /// Register (or aggregate) a hall call on behalf of a specific
331 /// rider, including their destination in DCS mode.
332 fn register_hall_call_for_rider(
333 &mut self,
334 stop: EntityId,
335 direction: CallDirection,
336 rider: EntityId,
337 destination: EntityId,
338 ) {
339 let mode = self
340 .groups
341 .iter()
342 .find(|g| g.stop_entities().contains(&stop))
343 .map(ElevatorGroup::hall_call_mode);
344 let dest = match mode {
345 Some(HallCallMode::Destination) => Some(destination),
346 _ => None,
347 };
348 self.ensure_hall_call(stop, direction, Some(rider), dest);
349 }
350}