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::{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        // Same origin & destination = no hall call gets registered (the
80        // direction is undefined), so the rider would sit Waiting forever
81        // while inflating `total_spawned`. Reject up front. (#273)
82        if origin == destination {
83            return Err(SimError::InvalidConfig {
84                field: "destination",
85                reason: "origin and destination must differ; same-stop \
86                         spawns deadlock with no hall call to summon a car"
87                    .into(),
88            });
89        }
90        let weight: Weight = weight.into();
91        let group = self.auto_detect_group(origin, destination)?;
92
93        let route = Route::direct(origin, destination, group);
94        Ok(RiderId::from(self.spawn_rider_inner(
95            origin,
96            destination,
97            weight,
98            route,
99        )))
100    }
101
102    /// Find the single group that serves both `origin` and `destination`.
103    ///
104    /// Returns `Ok(group)` when exactly one group serves both stops.
105    /// Returns [`SimError::NoRoute`] when no group does.
106    /// Returns [`SimError::AmbiguousRoute`] when more than one does.
107    pub(super) fn auto_detect_group(
108        &self,
109        origin: EntityId,
110        destination: EntityId,
111    ) -> Result<GroupId, SimError> {
112        let matching: Vec<GroupId> = self
113            .groups
114            .iter()
115            .filter(|g| {
116                g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
117            })
118            .map(ElevatorGroup::id)
119            .collect();
120
121        match matching.len() {
122            0 => {
123                let origin_groups: Vec<GroupId> = self
124                    .groups
125                    .iter()
126                    .filter(|g| g.stop_entities().contains(&origin))
127                    .map(ElevatorGroup::id)
128                    .collect();
129                let destination_groups: Vec<GroupId> = self
130                    .groups
131                    .iter()
132                    .filter(|g| g.stop_entities().contains(&destination))
133                    .map(ElevatorGroup::id)
134                    .collect();
135                Err(SimError::NoRoute {
136                    origin,
137                    destination,
138                    origin_groups,
139                    destination_groups,
140                })
141            }
142            1 => Ok(matching[0]),
143            _ => Err(SimError::AmbiguousRoute {
144                origin,
145                destination,
146                groups: matching,
147            }),
148        }
149    }
150
151    /// Internal helper: spawn a rider entity with the given route.
152    pub(super) fn spawn_rider_inner(
153        &mut self,
154        origin: EntityId,
155        destination: EntityId,
156        weight: Weight,
157        route: Route,
158    ) -> EntityId {
159        let eid = self.world.spawn();
160        self.world.set_rider(
161            eid,
162            Rider {
163                weight,
164                phase: RiderPhase::Waiting,
165                current_stop: Some(origin),
166                spawn_tick: self.tick,
167                board_tick: None,
168                tag: 0,
169            },
170        );
171        self.world.set_route(eid, route);
172        self.rider_index.insert_waiting(origin, eid);
173        if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
174            log.record(self.tick, origin);
175        }
176        if let Some(log) = self
177            .world
178            .resource_mut::<crate::arrival_log::DestinationLog>()
179        {
180            log.record(self.tick, destination);
181        }
182        self.events.emit(Event::RiderSpawned {
183            rider: eid,
184            origin,
185            destination,
186            tag: 0,
187            tick: self.tick,
188        });
189
190        // Auto-press the hall button for this rider. Direction is the
191        // sign of `dest_pos - origin_pos`; if the two coincide (walk
192        // leg, identity trip) no call is registered.
193        if let (Some(op), Some(dp)) = (
194            self.world.stop_position(origin),
195            self.world.stop_position(destination),
196        ) && let Some(direction) = CallDirection::between(op, dp)
197        {
198            self.register_hall_call_for_rider(origin, direction, eid, destination);
199        }
200
201        // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
202        let stop_tag = self
203            .world
204            .stop(origin)
205            .map(|s| format!("stop:{}", s.name()));
206
207        // Inherit metric tags from the origin stop.
208        if let Some(tags_res) = self
209            .world
210            .resource_mut::<crate::tagged_metrics::MetricTags>()
211        {
212            let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
213            for tag in origin_tags {
214                tags_res.tag(eid, tag);
215            }
216            // Apply the origin stop tag.
217            if let Some(tag) = stop_tag {
218                tags_res.tag(eid, tag);
219            }
220        }
221
222        eid
223    }
224
225    /// Drain all pending events from completed ticks.
226    ///
227    /// Events emitted during `step()` (or per-phase methods) are buffered
228    /// and made available here after `advance_tick()` is called.
229    /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
230    /// are also included.
231    ///
232    /// ```
233    /// use elevator_core::prelude::*;
234    ///
235    /// let mut sim = SimulationBuilder::demo().build().unwrap();
236    ///
237    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
238    /// sim.step();
239    ///
240    /// let events = sim.drain_events();
241    /// assert!(!events.is_empty());
242    /// ```
243    pub fn drain_events(&mut self) -> Vec<Event> {
244        // Flush any events still in the bus (from spawn_rider, disable, etc.)
245        self.pending_output.extend(self.events.drain());
246        std::mem::take(&mut self.pending_output)
247    }
248
249    /// Push an event into the pending output buffer (crate-internal).
250    pub(crate) fn push_event(&mut self, event: Event) {
251        self.pending_output.push(event);
252    }
253
254    /// Drain only events matching a predicate.
255    ///
256    /// Events that don't match the predicate remain in the buffer
257    /// and will be returned by future `drain_events` or
258    /// `drain_events_where` calls.
259    ///
260    /// ```
261    /// use elevator_core::prelude::*;
262    ///
263    /// let mut sim = SimulationBuilder::demo().build().unwrap();
264    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
265    /// sim.step();
266    ///
267    /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
268    ///     matches!(e, Event::RiderSpawned { .. })
269    /// });
270    /// ```
271    pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
272        // Flush bus into pending_output first.
273        self.pending_output.extend(self.events.drain());
274
275        let mut matched = Vec::new();
276        let mut remaining = Vec::new();
277        for event in std::mem::take(&mut self.pending_output) {
278            if predicate(&event) {
279                matched.push(event);
280            } else {
281                remaining.push(event);
282            }
283        }
284        self.pending_output = remaining;
285        matched
286    }
287
288    // ── Rider tag (opaque consumer-attached id) ──────────────────────
289
290    /// Read the opaque tag attached to a rider.
291    ///
292    /// Consumers use [`set_rider_tag`](Self::set_rider_tag) to stash an
293    /// external identifier on the rider (a game-side sim id, a player
294    /// id, a freight shipment id) and read it back here without keeping
295    /// a parallel `RiderId → u64` map. The engine never interprets the
296    /// value; it survives snapshot round-trip.
297    ///
298    /// Returns `0` for the default "untagged" state.
299    ///
300    /// # Errors
301    ///
302    /// Returns [`SimError::EntityNotFound`] if `id` does not correspond
303    /// to a live rider.
304    pub fn rider_tag(&self, id: RiderId) -> Result<u64, SimError> {
305        let eid = id.entity();
306        self.world
307            .rider(eid)
308            .map(Rider::tag)
309            .ok_or(SimError::EntityNotFound(eid))
310    }
311
312    /// Attach an opaque tag to a rider. The engine doesn't interpret the
313    /// value — pick whatever encoding your consumer needs (e.g. a 32-bit
314    /// external id zero-extended to `u64`, or two 32-bit half-words).
315    /// Pass `0` to clear the tag (the reserved "untagged" sentinel).
316    ///
317    /// # Errors
318    ///
319    /// Returns [`SimError::EntityNotFound`] if `id` does not correspond
320    /// to a live rider.
321    pub fn set_rider_tag(&mut self, id: RiderId, tag: u64) -> Result<(), SimError> {
322        let eid = id.entity();
323        let rider = self
324            .world
325            .rider_mut(eid)
326            .ok_or(SimError::EntityNotFound(eid))?;
327        rider.tag = tag;
328        Ok(())
329    }
330
331    /// Register (or aggregate) a hall call on behalf of a specific
332    /// rider, including their destination in DCS mode.
333    fn register_hall_call_for_rider(
334        &mut self,
335        stop: EntityId,
336        direction: CallDirection,
337        rider: EntityId,
338        destination: EntityId,
339    ) {
340        let mode = self
341            .groups
342            .iter()
343            .find(|g| g.stop_entities().contains(&stop))
344            .map(ElevatorGroup::hall_call_mode);
345        let dest = match mode {
346            Some(HallCallMode::Destination) => Some(destination),
347            _ => None,
348        };
349        self.ensure_hall_call(stop, direction, Some(rider), dest);
350    }
351}