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 let weight: Weight = weight.into();
80 let group = self.auto_detect_group(origin, destination)?;
81
82 let route = Route::direct(origin, destination, group);
83 Ok(RiderId::from(self.spawn_rider_inner(
84 origin,
85 destination,
86 weight,
87 route,
88 )))
89 }
90
91 /// Find the single group that serves both `origin` and `destination`.
92 ///
93 /// Returns `Ok(group)` when exactly one group serves both stops.
94 /// Returns [`SimError::NoRoute`] when no group does.
95 /// Returns [`SimError::AmbiguousRoute`] when more than one does.
96 pub(super) fn auto_detect_group(
97 &self,
98 origin: EntityId,
99 destination: EntityId,
100 ) -> Result<GroupId, SimError> {
101 let matching: Vec<GroupId> = self
102 .groups
103 .iter()
104 .filter(|g| {
105 g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
106 })
107 .map(ElevatorGroup::id)
108 .collect();
109
110 match matching.len() {
111 0 => {
112 let origin_groups: Vec<GroupId> = self
113 .groups
114 .iter()
115 .filter(|g| g.stop_entities().contains(&origin))
116 .map(ElevatorGroup::id)
117 .collect();
118 let destination_groups: Vec<GroupId> = self
119 .groups
120 .iter()
121 .filter(|g| g.stop_entities().contains(&destination))
122 .map(ElevatorGroup::id)
123 .collect();
124 Err(SimError::NoRoute {
125 origin,
126 destination,
127 origin_groups,
128 destination_groups,
129 })
130 }
131 1 => Ok(matching[0]),
132 _ => Err(SimError::AmbiguousRoute {
133 origin,
134 destination,
135 groups: matching,
136 }),
137 }
138 }
139
140 /// Internal helper: spawn a rider entity with the given route.
141 pub(super) fn spawn_rider_inner(
142 &mut self,
143 origin: EntityId,
144 destination: EntityId,
145 weight: Weight,
146 route: Route,
147 ) -> EntityId {
148 let eid = self.world.spawn();
149 self.world.set_rider(
150 eid,
151 Rider {
152 weight,
153 phase: RiderPhase::Waiting,
154 current_stop: Some(origin),
155 spawn_tick: self.tick,
156 board_tick: None,
157 },
158 );
159 self.world.set_route(eid, route);
160 self.rider_index.insert_waiting(origin, eid);
161 self.events.emit(Event::RiderSpawned {
162 rider: eid,
163 origin,
164 destination,
165 tick: self.tick,
166 });
167
168 // Auto-press the hall button for this rider. Direction is the
169 // sign of `dest_pos - origin_pos`; if the two coincide (walk
170 // leg, identity trip) no call is registered.
171 if let (Some(op), Some(dp)) = (
172 self.world.stop_position(origin),
173 self.world.stop_position(destination),
174 ) && let Some(direction) = CallDirection::between(op, dp)
175 {
176 self.register_hall_call_for_rider(origin, direction, eid, destination);
177 }
178
179 // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
180 let stop_tag = self
181 .world
182 .stop(origin)
183 .map(|s| format!("stop:{}", s.name()));
184
185 // Inherit metric tags from the origin stop.
186 if let Some(tags_res) = self
187 .world
188 .resource_mut::<crate::tagged_metrics::MetricTags>()
189 {
190 let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
191 for tag in origin_tags {
192 tags_res.tag(eid, tag);
193 }
194 // Apply the origin stop tag.
195 if let Some(tag) = stop_tag {
196 tags_res.tag(eid, tag);
197 }
198 }
199
200 eid
201 }
202
203 /// Drain all pending events from completed ticks.
204 ///
205 /// Events emitted during `step()` (or per-phase methods) are buffered
206 /// and made available here after `advance_tick()` is called.
207 /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
208 /// are also included.
209 ///
210 /// ```
211 /// use elevator_core::prelude::*;
212 ///
213 /// let mut sim = SimulationBuilder::demo().build().unwrap();
214 ///
215 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
216 /// sim.step();
217 ///
218 /// let events = sim.drain_events();
219 /// assert!(!events.is_empty());
220 /// ```
221 pub fn drain_events(&mut self) -> Vec<Event> {
222 // Flush any events still in the bus (from spawn_rider, disable, etc.)
223 self.pending_output.extend(self.events.drain());
224 std::mem::take(&mut self.pending_output)
225 }
226
227 /// Push an event into the pending output buffer (crate-internal).
228 pub(crate) fn push_event(&mut self, event: Event) {
229 self.pending_output.push(event);
230 }
231
232 /// Drain only events matching a predicate.
233 ///
234 /// Events that don't match the predicate remain in the buffer
235 /// and will be returned by future `drain_events` or
236 /// `drain_events_where` calls.
237 ///
238 /// ```
239 /// use elevator_core::prelude::*;
240 ///
241 /// let mut sim = SimulationBuilder::demo().build().unwrap();
242 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
243 /// sim.step();
244 ///
245 /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
246 /// matches!(e, Event::RiderSpawned { .. })
247 /// });
248 /// ```
249 pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
250 // Flush bus into pending_output first.
251 self.pending_output.extend(self.events.drain());
252
253 let mut matched = Vec::new();
254 let mut remaining = Vec::new();
255 for event in std::mem::take(&mut self.pending_output) {
256 if predicate(&event) {
257 matched.push(event);
258 } else {
259 remaining.push(event);
260 }
261 }
262 self.pending_output = remaining;
263 matched
264 }
265
266 /// Register (or aggregate) a hall call on behalf of a specific
267 /// rider, including their destination in DCS mode.
268 fn register_hall_call_for_rider(
269 &mut self,
270 stop: EntityId,
271 direction: CallDirection,
272 rider: EntityId,
273 destination: EntityId,
274 ) {
275 let mode = self
276 .groups
277 .iter()
278 .find(|g| g.stop_entities().contains(&stop))
279 .map(ElevatorGroup::hall_call_mode);
280 let dest = match mode {
281 Some(HallCallMode::Destination) => Some(destination),
282 _ => None,
283 };
284 self.ensure_hall_call(stop, direction, Some(rider), dest);
285 }
286}