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