Skip to main content

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}