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    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}