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        let weight: Weight = weight.into();
80        let group = self.auto_detect_group(origin, destination)?;
81
82        let route = Route::direct(origin, destination, group);
83        Ok(RiderId::from(self.spawn_rider_inner(
84            origin,
85            destination,
86            weight,
87            route,
88        )))
89    }
90
91    /// Find the single group that serves both `origin` and `destination`.
92    ///
93    /// Returns `Ok(group)` when exactly one group serves both stops.
94    /// Returns [`SimError::NoRoute`] when no group does.
95    /// Returns [`SimError::AmbiguousRoute`] when more than one does.
96    pub(super) fn auto_detect_group(
97        &self,
98        origin: EntityId,
99        destination: EntityId,
100    ) -> Result<GroupId, SimError> {
101        let matching: Vec<GroupId> = self
102            .groups
103            .iter()
104            .filter(|g| {
105                g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
106            })
107            .map(ElevatorGroup::id)
108            .collect();
109
110        match matching.len() {
111            0 => {
112                let origin_groups: Vec<GroupId> = self
113                    .groups
114                    .iter()
115                    .filter(|g| g.stop_entities().contains(&origin))
116                    .map(ElevatorGroup::id)
117                    .collect();
118                let destination_groups: Vec<GroupId> = self
119                    .groups
120                    .iter()
121                    .filter(|g| g.stop_entities().contains(&destination))
122                    .map(ElevatorGroup::id)
123                    .collect();
124                Err(SimError::NoRoute {
125                    origin,
126                    destination,
127                    origin_groups,
128                    destination_groups,
129                })
130            }
131            1 => Ok(matching[0]),
132            _ => Err(SimError::AmbiguousRoute {
133                origin,
134                destination,
135                groups: matching,
136            }),
137        }
138    }
139
140    /// Internal helper: spawn a rider entity with the given route.
141    pub(super) fn spawn_rider_inner(
142        &mut self,
143        origin: EntityId,
144        destination: EntityId,
145        weight: Weight,
146        route: Route,
147    ) -> EntityId {
148        let eid = self.world.spawn();
149        self.world.set_rider(
150            eid,
151            Rider {
152                weight,
153                phase: RiderPhase::Waiting,
154                current_stop: Some(origin),
155                spawn_tick: self.tick,
156                board_tick: None,
157            },
158        );
159        self.world.set_route(eid, route);
160        self.rider_index.insert_waiting(origin, eid);
161        self.events.emit(Event::RiderSpawned {
162            rider: eid,
163            origin,
164            destination,
165            tick: self.tick,
166        });
167
168        // Auto-press the hall button for this rider. Direction is the
169        // sign of `dest_pos - origin_pos`; if the two coincide (walk
170        // leg, identity trip) no call is registered.
171        if let (Some(op), Some(dp)) = (
172            self.world.stop_position(origin),
173            self.world.stop_position(destination),
174        ) && let Some(direction) = CallDirection::between(op, dp)
175        {
176            self.register_hall_call_for_rider(origin, direction, eid, destination);
177        }
178
179        // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
180        let stop_tag = self
181            .world
182            .stop(origin)
183            .map(|s| format!("stop:{}", s.name()));
184
185        // Inherit metric tags from the origin stop.
186        if let Some(tags_res) = self
187            .world
188            .resource_mut::<crate::tagged_metrics::MetricTags>()
189        {
190            let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
191            for tag in origin_tags {
192                tags_res.tag(eid, tag);
193            }
194            // Apply the origin stop tag.
195            if let Some(tag) = stop_tag {
196                tags_res.tag(eid, tag);
197            }
198        }
199
200        eid
201    }
202
203    /// Drain all pending events from completed ticks.
204    ///
205    /// Events emitted during `step()` (or per-phase methods) are buffered
206    /// and made available here after `advance_tick()` is called.
207    /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
208    /// are also included.
209    ///
210    /// ```
211    /// use elevator_core::prelude::*;
212    ///
213    /// let mut sim = SimulationBuilder::demo().build().unwrap();
214    ///
215    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
216    /// sim.step();
217    ///
218    /// let events = sim.drain_events();
219    /// assert!(!events.is_empty());
220    /// ```
221    pub fn drain_events(&mut self) -> Vec<Event> {
222        // Flush any events still in the bus (from spawn_rider, disable, etc.)
223        self.pending_output.extend(self.events.drain());
224        std::mem::take(&mut self.pending_output)
225    }
226
227    /// Push an event into the pending output buffer (crate-internal).
228    pub(crate) fn push_event(&mut self, event: Event) {
229        self.pending_output.push(event);
230    }
231
232    /// Drain only events matching a predicate.
233    ///
234    /// Events that don't match the predicate remain in the buffer
235    /// and will be returned by future `drain_events` or
236    /// `drain_events_where` calls.
237    ///
238    /// ```
239    /// use elevator_core::prelude::*;
240    ///
241    /// let mut sim = SimulationBuilder::demo().build().unwrap();
242    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
243    /// sim.step();
244    ///
245    /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
246    ///     matches!(e, Event::RiderSpawned { .. })
247    /// });
248    /// ```
249    pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
250        // Flush bus into pending_output first.
251        self.pending_output.extend(self.events.drain());
252
253        let mut matched = Vec::new();
254        let mut remaining = Vec::new();
255        for event in std::mem::take(&mut self.pending_output) {
256            if predicate(&event) {
257                matched.push(event);
258            } else {
259                remaining.push(event);
260            }
261        }
262        self.pending_output = remaining;
263        matched
264    }
265
266    /// Register (or aggregate) a hall call on behalf of a specific
267    /// rider, including their destination in DCS mode.
268    fn register_hall_call_for_rider(
269        &mut self,
270        stop: EntityId,
271        direction: CallDirection,
272        rider: EntityId,
273        destination: EntityId,
274    ) {
275        let mode = self
276            .groups
277            .iter()
278            .find(|g| g.stop_entities().contains(&stop))
279            .map(ElevatorGroup::hall_call_mode);
280        let dest = match mode {
281            Some(HallCallMode::Destination) => Some(destination),
282            _ => None,
283        };
284        self.ensure_hall_call(stop, direction, Some(rider), dest);
285    }
286}