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