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 },
169 );
170 self.world.set_route(eid, route);
171 self.rider_index.insert_waiting(origin, eid);
172 if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
173 log.record(self.tick, origin);
174 }
175 self.events.emit(Event::RiderSpawned {
176 rider: eid,
177 origin,
178 destination,
179 tick: self.tick,
180 });
181
182 // Auto-press the hall button for this rider. Direction is the
183 // sign of `dest_pos - origin_pos`; if the two coincide (walk
184 // leg, identity trip) no call is registered.
185 if let (Some(op), Some(dp)) = (
186 self.world.stop_position(origin),
187 self.world.stop_position(destination),
188 ) && let Some(direction) = CallDirection::between(op, dp)
189 {
190 self.register_hall_call_for_rider(origin, direction, eid, destination);
191 }
192
193 // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
194 let stop_tag = self
195 .world
196 .stop(origin)
197 .map(|s| format!("stop:{}", s.name()));
198
199 // Inherit metric tags from the origin stop.
200 if let Some(tags_res) = self
201 .world
202 .resource_mut::<crate::tagged_metrics::MetricTags>()
203 {
204 let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
205 for tag in origin_tags {
206 tags_res.tag(eid, tag);
207 }
208 // Apply the origin stop tag.
209 if let Some(tag) = stop_tag {
210 tags_res.tag(eid, tag);
211 }
212 }
213
214 eid
215 }
216
217 /// Drain all pending events from completed ticks.
218 ///
219 /// Events emitted during `step()` (or per-phase methods) are buffered
220 /// and made available here after `advance_tick()` is called.
221 /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
222 /// are also included.
223 ///
224 /// ```
225 /// use elevator_core::prelude::*;
226 ///
227 /// let mut sim = SimulationBuilder::demo().build().unwrap();
228 ///
229 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
230 /// sim.step();
231 ///
232 /// let events = sim.drain_events();
233 /// assert!(!events.is_empty());
234 /// ```
235 pub fn drain_events(&mut self) -> Vec<Event> {
236 // Flush any events still in the bus (from spawn_rider, disable, etc.)
237 self.pending_output.extend(self.events.drain());
238 std::mem::take(&mut self.pending_output)
239 }
240
241 /// Push an event into the pending output buffer (crate-internal).
242 pub(crate) fn push_event(&mut self, event: Event) {
243 self.pending_output.push(event);
244 }
245
246 /// Drain only events matching a predicate.
247 ///
248 /// Events that don't match the predicate remain in the buffer
249 /// and will be returned by future `drain_events` or
250 /// `drain_events_where` calls.
251 ///
252 /// ```
253 /// use elevator_core::prelude::*;
254 ///
255 /// let mut sim = SimulationBuilder::demo().build().unwrap();
256 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
257 /// sim.step();
258 ///
259 /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
260 /// matches!(e, Event::RiderSpawned { .. })
261 /// });
262 /// ```
263 pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
264 // Flush bus into pending_output first.
265 self.pending_output.extend(self.events.drain());
266
267 let mut matched = Vec::new();
268 let mut remaining = Vec::new();
269 for event in std::mem::take(&mut self.pending_output) {
270 if predicate(&event) {
271 matched.push(event);
272 } else {
273 remaining.push(event);
274 }
275 }
276 self.pending_output = remaining;
277 matched
278 }
279
280 /// Register (or aggregate) a hall call on behalf of a specific
281 /// rider, including their destination in DCS mode.
282 fn register_hall_call_for_rider(
283 &mut self,
284 stop: EntityId,
285 direction: CallDirection,
286 rider: EntityId,
287 destination: EntityId,
288 ) {
289 let mode = self
290 .groups
291 .iter()
292 .find(|g| g.stop_entities().contains(&stop))
293 .map(ElevatorGroup::hall_call_mode);
294 let dest = match mode {
295 Some(HallCallMode::Destination) => Some(destination),
296 _ => None,
297 };
298 self.ensure_hall_call(stop, direction, Some(rider), dest);
299 }
300}