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::{
8 AccessControl, CallDirection, Patience, Preferences, Rider, RiderPhase, Route, Weight,
9};
10use crate::dispatch::{ElevatorGroup, HallCallMode};
11use crate::entity::{EntityId, RiderId};
12use crate::error::SimError;
13use crate::events::Event;
14use crate::ids::GroupId;
15use crate::stop::StopRef;
16
17impl super::Simulation {
18 // ── Rider spawning ───────────────────────────────────────────────
19
20 /// Create a rider builder for fluent rider spawning.
21 ///
22 /// Accepts [`EntityId`] or [`StopId`](crate::stop::StopId) for origin and destination
23 /// (anything that implements `Into<StopRef>`).
24 ///
25 /// # Errors
26 ///
27 /// Returns [`SimError::StopNotFound`] if a [`StopId`](crate::stop::StopId) does not exist
28 /// in the building configuration.
29 ///
30 /// ```
31 /// use elevator_core::prelude::*;
32 ///
33 /// let mut sim = SimulationBuilder::demo().build().unwrap();
34 /// let rider = sim.build_rider(StopId(0), StopId(1))
35 /// .unwrap()
36 /// .weight(80.0)
37 /// .spawn()
38 /// .unwrap();
39 /// ```
40 pub fn build_rider(
41 &mut self,
42 origin: impl Into<StopRef>,
43 destination: impl Into<StopRef>,
44 ) -> Result<super::RiderBuilder<'_>, SimError> {
45 let origin = self.resolve_stop(origin.into())?;
46 let destination = self.resolve_stop(destination.into())?;
47 Ok(super::RiderBuilder {
48 sim: self,
49 origin,
50 destination,
51 weight: Weight::from(75.0),
52 group: None,
53 route: None,
54 patience: None,
55 preferences: None,
56 access_control: None,
57 })
58 }
59
60 /// Spawn a rider with default preferences (convenience shorthand).
61 ///
62 /// Equivalent to `build_rider(origin, destination)?.weight(weight).spawn()`.
63 /// Use [`build_rider`](Self::build_rider) instead when you need to set
64 /// patience, preferences, access control, or an explicit route.
65 ///
66 /// Auto-detects the elevator group by finding groups that serve both origin
67 /// and destination stops.
68 ///
69 /// # Errors
70 ///
71 /// Returns [`SimError::NoRoute`] if no group serves both stops.
72 /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops.
73 pub fn spawn_rider(
74 &mut self,
75 origin: impl Into<StopRef>,
76 destination: impl Into<StopRef>,
77 weight: impl Into<Weight>,
78 ) -> Result<RiderId, SimError> {
79 let origin = self.resolve_stop(origin.into())?;
80 let destination = self.resolve_stop(destination.into())?;
81 // Same origin & destination = no hall call gets registered (the
82 // direction is undefined), so the rider would sit Waiting forever
83 // while inflating `total_spawned`. Reject up front. (#273)
84 if origin == destination {
85 return Err(SimError::InvalidConfig {
86 field: "destination",
87 reason: "origin and destination must differ; same-stop \
88 spawns deadlock with no hall call to summon a car"
89 .into(),
90 });
91 }
92 let weight: Weight = weight.into();
93 let group = self.auto_detect_group(origin, destination)?;
94
95 let route = Route::direct(origin, destination, group);
96 Ok(RiderId::wrap_unchecked(self.spawn_rider_inner(
97 origin,
98 destination,
99 weight,
100 route,
101 )))
102 }
103
104 /// Find the single group that serves both `origin` and `destination`.
105 ///
106 /// Returns `Ok(group)` when exactly one group serves both stops.
107 /// Returns [`SimError::NoRoute`] when no group does.
108 /// Returns [`SimError::AmbiguousRoute`] when more than one does.
109 pub(super) fn auto_detect_group(
110 &self,
111 origin: EntityId,
112 destination: EntityId,
113 ) -> Result<GroupId, SimError> {
114 let matching: Vec<GroupId> = self
115 .groups
116 .iter()
117 .filter(|g| {
118 g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
119 })
120 .map(ElevatorGroup::id)
121 .collect();
122
123 match matching.len() {
124 0 => {
125 let origin_groups: Vec<GroupId> = self
126 .groups
127 .iter()
128 .filter(|g| g.stop_entities().contains(&origin))
129 .map(ElevatorGroup::id)
130 .collect();
131 let destination_groups: Vec<GroupId> = self
132 .groups
133 .iter()
134 .filter(|g| g.stop_entities().contains(&destination))
135 .map(ElevatorGroup::id)
136 .collect();
137 Err(SimError::NoRoute {
138 origin,
139 destination,
140 origin_groups,
141 destination_groups,
142 })
143 }
144 1 => Ok(matching[0]),
145 _ => Err(SimError::AmbiguousRoute {
146 origin,
147 destination,
148 groups: matching,
149 }),
150 }
151 }
152
153 /// Internal helper: spawn a rider entity with the given route.
154 pub(super) fn spawn_rider_inner(
155 &mut self,
156 origin: EntityId,
157 destination: EntityId,
158 weight: Weight,
159 route: Route,
160 ) -> EntityId {
161 let eid = self.world.spawn();
162 self.world.set_rider(
163 eid,
164 Rider {
165 weight,
166 phase: RiderPhase::Waiting,
167 current_stop: Some(origin),
168 spawn_tick: self.tick,
169 board_tick: None,
170 tag: 0,
171 },
172 );
173 self.world.set_route(eid, route);
174 self.rider_index.insert_waiting(origin, eid);
175 if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
176 log.record(self.tick, origin);
177 }
178 if let Some(log) = self
179 .world
180 .resource_mut::<crate::arrival_log::DestinationLog>()
181 {
182 log.record(self.tick, destination);
183 }
184 self.events.emit(Event::RiderSpawned {
185 rider: eid,
186 origin,
187 destination,
188 tag: 0,
189 tick: self.tick,
190 });
191
192 // Auto-press the hall button for this rider. Direction is the
193 // sign of `dest_pos - origin_pos`; if the two coincide (walk
194 // leg, identity trip) no call is registered.
195 if let (Some(op), Some(dp)) = (
196 self.world.stop_position(origin),
197 self.world.stop_position(destination),
198 ) && let Some(direction) = CallDirection::between(op, dp)
199 {
200 self.register_hall_call_for_rider(origin, direction, eid, destination);
201 }
202
203 // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
204 let stop_tag = self
205 .world
206 .stop(origin)
207 .map(|s| format!("stop:{}", s.name()));
208
209 // Inherit metric tags from the origin stop.
210 if let Some(tags_res) = self
211 .world
212 .resource_mut::<crate::tagged_metrics::MetricTags>()
213 {
214 let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
215 for tag in origin_tags {
216 tags_res.tag(eid, tag);
217 }
218 // Apply the origin stop tag.
219 if let Some(tag) = stop_tag {
220 tags_res.tag(eid, tag);
221 }
222 }
223
224 eid
225 }
226
227 /// Drain all pending events from completed ticks.
228 ///
229 /// Events emitted during `step()` (or per-phase methods) are buffered
230 /// and made available here after `advance_tick()` is called.
231 /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
232 /// are also included.
233 ///
234 /// ```
235 /// use elevator_core::prelude::*;
236 ///
237 /// let mut sim = SimulationBuilder::demo().build().unwrap();
238 ///
239 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
240 /// sim.step();
241 ///
242 /// let events = sim.drain_events();
243 /// assert!(!events.is_empty());
244 /// ```
245 pub fn drain_events(&mut self) -> Vec<Event> {
246 // Flush any events still in the bus (from spawn_rider, disable, etc.)
247 self.pending_output.extend(self.events.drain());
248 std::mem::take(&mut self.pending_output)
249 }
250
251 /// Push an event into the pending output buffer (crate-internal).
252 pub(crate) fn push_event(&mut self, event: Event) {
253 self.pending_output.push(event);
254 }
255
256 /// Drain only events matching a predicate.
257 ///
258 /// Events that don't match the predicate remain in the buffer
259 /// and will be returned by future `drain_events` or
260 /// `drain_events_where` calls.
261 ///
262 /// ```
263 /// use elevator_core::prelude::*;
264 ///
265 /// let mut sim = SimulationBuilder::demo().build().unwrap();
266 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
267 /// sim.step();
268 ///
269 /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
270 /// matches!(e, Event::RiderSpawned { .. })
271 /// });
272 /// ```
273 pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
274 // Flush bus into pending_output first.
275 self.pending_output.extend(self.events.drain());
276
277 let mut matched = Vec::new();
278 let mut remaining = Vec::new();
279 for event in std::mem::take(&mut self.pending_output) {
280 if predicate(&event) {
281 matched.push(event);
282 } else {
283 remaining.push(event);
284 }
285 }
286 self.pending_output = remaining;
287 matched
288 }
289
290 /// Drain events whose [`kind`](Event::kind) is in `kinds`.
291 ///
292 /// Closure-free counterpart to
293 /// [`drain_events_where`](Self::drain_events_where) — useful from
294 /// FFI / wasm / gdext call sites that can't marshal a Rust closure
295 /// across the language boundary. `kinds` is treated as a small
296 /// set; for very large filter sets prefer the closure form.
297 ///
298 /// Events whose kind is not in the set remain in the buffer and
299 /// will be returned by future `drain_events*` calls.
300 ///
301 /// ```
302 /// use elevator_core::prelude::*;
303 ///
304 /// let mut sim = SimulationBuilder::demo().build().unwrap();
305 /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
306 /// sim.step();
307 ///
308 /// let spawns = sim.drain_events_by_kind(&[EventKind::RiderSpawned]);
309 /// assert!(spawns.iter().all(|e| matches!(e, Event::RiderSpawned { .. })));
310 /// ```
311 pub fn drain_events_by_kind(&mut self, kinds: &[crate::events::EventKind]) -> Vec<Event> {
312 self.drain_events_where(|e| kinds.contains(&e.kind()))
313 }
314
315 /// Drain events that reference `entity` in any of their fields.
316 ///
317 /// Closure-free counterpart to
318 /// [`drain_events_where`](Self::drain_events_where) for the common
319 /// "give me everything that happened to this rider / car / stop"
320 /// query — usable from FFI / wasm / gdext call sites that can't
321 /// marshal a Rust closure across the language boundary.
322 ///
323 /// Matching is delegated to [`Event::involves`]: an event matches
324 /// when any [`EntityId`] field on the
325 /// payload equals `entity`. Multi-entity events (e.g.
326 /// [`RiderBoarded`](Event::RiderBoarded), which references both
327 /// rider and elevator) match when *either* role does, so a query
328 /// for a car returns the same event as a separate query for the
329 /// rider.
330 ///
331 /// Events that don't reference `entity` remain in the buffer and
332 /// will be returned by future `drain_events*` calls.
333 ///
334 /// ```
335 /// use elevator_core::prelude::*;
336 ///
337 /// let mut sim = SimulationBuilder::demo().build().unwrap();
338 /// let rider = sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
339 /// sim.step();
340 ///
341 /// let rider_events = sim.drain_events_for_entity(rider.entity());
342 /// assert!(rider_events.iter().all(|e| e.involves(rider.entity())));
343 /// ```
344 pub fn drain_events_for_entity(&mut self, entity: crate::entity::EntityId) -> Vec<Event> {
345 self.drain_events_where(|e| e.involves(entity))
346 }
347
348 // ── Rider tag (opaque consumer-attached id) ──────────────────────
349
350 /// Read the opaque tag attached to a rider.
351 ///
352 /// Consumers use [`set_rider_tag`](Self::set_rider_tag) to stash an
353 /// external identifier on the rider (a game-side sim id, a player
354 /// id, a freight shipment id) and read it back here without keeping
355 /// a parallel `RiderId → u64` map. The engine never interprets the
356 /// value; it survives snapshot round-trip.
357 ///
358 /// Returns `0` for the default "untagged" state.
359 ///
360 /// # Errors
361 ///
362 /// Returns [`SimError::EntityNotFound`] if `id` does not correspond
363 /// to a live rider.
364 pub fn rider_tag(&self, id: RiderId) -> Result<u64, SimError> {
365 let eid = id.entity();
366 self.world
367 .rider(eid)
368 .map(Rider::tag)
369 .ok_or(SimError::EntityNotFound(eid))
370 }
371
372 /// Attach an opaque tag to a rider. The engine doesn't interpret the
373 /// value — pick whatever encoding your consumer needs (e.g. a 32-bit
374 /// external id zero-extended to `u64`, or two 32-bit half-words).
375 /// Pass `0` to clear the tag (the reserved "untagged" sentinel).
376 ///
377 /// # Errors
378 ///
379 /// Returns [`SimError::EntityNotFound`] if `id` does not correspond
380 /// to a live rider.
381 pub fn set_rider_tag(&mut self, id: RiderId, tag: u64) -> Result<(), SimError> {
382 let eid = id.entity();
383 let rider = self
384 .world
385 .rider_mut(eid)
386 .ok_or(SimError::EntityNotFound(eid))?;
387 rider.tag = tag;
388 Ok(())
389 }
390
391 /// Register (or aggregate) a hall call on behalf of a specific
392 /// rider, including their destination in DCS mode.
393 fn register_hall_call_for_rider(
394 &mut self,
395 stop: EntityId,
396 direction: CallDirection,
397 rider: EntityId,
398 destination: EntityId,
399 ) {
400 let mode = self
401 .groups
402 .iter()
403 .find(|g| g.stop_entities().contains(&stop))
404 .map(ElevatorGroup::hall_call_mode);
405 let dest = match mode {
406 Some(HallCallMode::Destination) => Some(destination),
407 _ => None,
408 };
409 self.ensure_hall_call(stop, direction, Some(rider), dest);
410 }
411}
412
413/// Fluent builder for spawning riders with optional configuration.
414///
415/// Created via [`super::Simulation::build_rider`].
416///
417/// ```
418/// use elevator_core::prelude::*;
419///
420/// let mut sim = SimulationBuilder::demo().build().unwrap();
421/// let rider = sim.build_rider(StopId(0), StopId(1))
422/// .unwrap()
423/// .weight(80.0)
424/// .spawn()
425/// .unwrap();
426/// ```
427pub struct RiderBuilder<'a> {
428 /// Mutable reference to the simulation (consumed on spawn).
429 pub(super) sim: &'a mut super::Simulation,
430 /// Origin stop entity.
431 pub(super) origin: EntityId,
432 /// Destination stop entity.
433 pub(super) destination: EntityId,
434 /// Rider weight (default: 75.0).
435 pub(super) weight: Weight,
436 /// Explicit dispatch group (skips auto-detection).
437 pub(super) group: Option<GroupId>,
438 /// Explicit multi-leg route.
439 pub(super) route: Option<Route>,
440 /// Maximum wait ticks before abandoning.
441 pub(super) patience: Option<u64>,
442 /// Boarding preferences.
443 pub(super) preferences: Option<Preferences>,
444 /// Per-rider access control.
445 pub(super) access_control: Option<AccessControl>,
446}
447
448impl RiderBuilder<'_> {
449 /// Set the rider's weight (default: 75.0).
450 #[must_use]
451 pub fn weight(mut self, weight: impl Into<Weight>) -> Self {
452 self.weight = weight.into();
453 self
454 }
455
456 /// Set the dispatch group explicitly, skipping auto-detection.
457 #[must_use]
458 pub const fn group(mut self, group: GroupId) -> Self {
459 self.group = Some(group);
460 self
461 }
462
463 /// Provide an explicit multi-leg route.
464 #[must_use]
465 pub fn route(mut self, route: Route) -> Self {
466 self.route = Some(route);
467 self
468 }
469
470 /// Set maximum wait ticks before the rider abandons.
471 #[must_use]
472 pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
473 self.patience = Some(max_wait_ticks);
474 self
475 }
476
477 /// Set boarding preferences.
478 #[must_use]
479 pub const fn preferences(mut self, prefs: Preferences) -> Self {
480 self.preferences = Some(prefs);
481 self
482 }
483
484 /// Set per-rider access control (allowed stops).
485 #[must_use]
486 pub fn access_control(mut self, ac: AccessControl) -> Self {
487 self.access_control = Some(ac);
488 self
489 }
490
491 /// Spawn the rider with the configured options.
492 ///
493 /// # Errors
494 ///
495 /// Returns [`SimError::NoRoute`] if no group serves both stops (when auto-detecting).
496 /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops (when auto-detecting).
497 /// Returns [`SimError::GroupNotFound`] if an explicit group does not exist.
498 /// Returns [`SimError::RouteOriginMismatch`] if an explicit route's first leg
499 /// does not start at `origin`.
500 pub fn spawn(self) -> Result<RiderId, SimError> {
501 let route = if let Some(route) = self.route {
502 // Validate route origin matches the spawn origin.
503 if let Some(leg) = route.current()
504 && leg.from != self.origin
505 {
506 return Err(SimError::RouteOriginMismatch {
507 expected_origin: self.origin,
508 route_origin: leg.from,
509 });
510 }
511 route
512 } else {
513 // No explicit route: must build one from origin → destination.
514 // Same origin/destination produces a Route::direct that no hall
515 // call can summon a car for — rider deadlocks Waiting (#273).
516 // The route-supplied path above is exempt from this check: a
517 // caller that constructs their own Route presumably also drives
518 // the corresponding hall-call / dispatch path, so the
519 // same-stop case there is their responsibility, not ours.
520 if self.origin == self.destination {
521 return Err(SimError::InvalidConfig {
522 field: "destination",
523 reason: "origin and destination must differ; same-stop \
524 spawns deadlock with no hall call to summon a car"
525 .into(),
526 });
527 }
528 if let Some(group) = self.group {
529 if !self.sim.groups.iter().any(|g| g.id() == group) {
530 return Err(SimError::GroupNotFound(group));
531 }
532 Route::direct(self.origin, self.destination, group)
533 } else {
534 // Auto-detect the single-group case first; on `NoRoute` or
535 // `AmbiguousRoute`, fall back to the multi-leg topology
536 // search so zoned buildings and specialty-overlap floors
537 // work through the plain `spawn_rider` API without callers
538 // having to thread a group pick through transfer points.
539 match self.sim.auto_detect_group(self.origin, self.destination) {
540 Ok(group) => Route::direct(self.origin, self.destination, group),
541 Err(
542 original @ (SimError::NoRoute { .. } | SimError::AmbiguousRoute { .. }),
543 ) => match self.sim.shortest_route(self.origin, self.destination) {
544 Some(route) => route,
545 // Preserve the original diagnostic context (which
546 // groups serve origin / destination) so callers
547 // still see the misconfiguration, not just a
548 // bare "no route" from the fallback.
549 None => return Err(original),
550 },
551 Err(other) => return Err(other),
552 }
553 }
554 };
555
556 let eid = self
557 .sim
558 .spawn_rider_inner(self.origin, self.destination, self.weight, route);
559
560 // Apply optional components.
561 if let Some(max_wait) = self.patience {
562 self.sim.world.set_patience(
563 eid,
564 Patience {
565 max_wait_ticks: max_wait,
566 waited_ticks: 0,
567 },
568 );
569 }
570 if let Some(prefs) = self.preferences {
571 self.sim.world.set_preferences(eid, prefs);
572 }
573 if let Some(ac) = self.access_control {
574 self.sim.world.set_access_control(eid, ac);
575 }
576
577 Ok(RiderId::wrap_unchecked(eid))
578 }
579}