elevator_core/events.rs
1//! Simulation event bus and typed event channels.
2
3use crate::entity::EntityId;
4use crate::error::{RejectionContext, RejectionReason};
5use crate::ids::GroupId;
6use ordered_float::OrderedFloat;
7use serde::{Deserialize, Serialize};
8
9/// Events emitted by the simulation during ticks.
10///
11/// All entity references use `EntityId`. Games can look up additional
12/// component data on the referenced entity if needed.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[non_exhaustive]
15pub enum Event {
16 // -- Elevator events --
17 /// An elevator departed from a stop.
18 ElevatorDeparted {
19 /// The elevator that departed.
20 elevator: EntityId,
21 /// The stop it departed from.
22 from_stop: EntityId,
23 /// The tick when departure occurred.
24 tick: u64,
25 },
26 /// An elevator arrived at a stop.
27 ElevatorArrived {
28 /// The elevator that arrived.
29 elevator: EntityId,
30 /// The stop it arrived at.
31 at_stop: EntityId,
32 /// The tick when arrival occurred.
33 tick: u64,
34 },
35 /// An elevator's doors finished opening.
36 DoorOpened {
37 /// The elevator whose doors opened.
38 elevator: EntityId,
39 /// The tick when the doors opened.
40 tick: u64,
41 },
42 /// An elevator's doors finished closing.
43 DoorClosed {
44 /// The elevator whose doors closed.
45 elevator: EntityId,
46 /// The tick when the doors closed.
47 tick: u64,
48 },
49 /// Emitted when an elevator passes a stop without stopping.
50 /// Games/dispatch can use this to decide whether to add stops mid-travel.
51 PassingFloor {
52 /// The elevator passing by.
53 elevator: EntityId,
54 /// The stop being passed.
55 stop: EntityId,
56 /// Direction: true = moving up, false = moving down.
57 moving_up: bool,
58 /// The tick when the pass occurred.
59 tick: u64,
60 },
61
62 // -- Rider events (unified: passengers, cargo, any rideable entity) --
63 /// A new rider appeared at a stop and wants to travel.
64 RiderSpawned {
65 /// The spawned rider entity.
66 rider: EntityId,
67 /// The stop where the rider appeared.
68 origin: EntityId,
69 /// The stop the rider wants to reach.
70 destination: EntityId,
71 /// The tick when the rider spawned.
72 tick: u64,
73 },
74 /// A rider boarded an elevator.
75 RiderBoarded {
76 /// The rider that boarded.
77 rider: EntityId,
78 /// The elevator the rider boarded.
79 elevator: EntityId,
80 /// The tick when boarding occurred.
81 tick: u64,
82 },
83 /// A rider exited an elevator at a stop.
84 #[serde(alias = "RiderAlighted")]
85 RiderExited {
86 /// The rider that exited.
87 rider: EntityId,
88 /// The elevator the rider exited.
89 elevator: EntityId,
90 /// The stop where the rider exited.
91 stop: EntityId,
92 /// The tick when exiting occurred.
93 tick: u64,
94 },
95 /// A rider was rejected from boarding (e.g., over capacity).
96 RiderRejected {
97 /// The rider that was rejected.
98 rider: EntityId,
99 /// The elevator that rejected the rider.
100 elevator: EntityId,
101 /// The reason for rejection.
102 reason: RejectionReason,
103 /// Additional numeric context for the rejection.
104 context: Option<RejectionContext>,
105 /// The tick when rejection occurred.
106 tick: u64,
107 },
108 /// A rider gave up waiting and left the stop.
109 RiderAbandoned {
110 /// The rider that abandoned.
111 rider: EntityId,
112 /// The stop the rider left.
113 stop: EntityId,
114 /// The tick when abandonment occurred.
115 tick: u64,
116 },
117
118 /// A rider was ejected from an elevator (due to disable or despawn).
119 ///
120 /// The rider is moved to `Waiting` phase at the nearest stop.
121 RiderEjected {
122 /// The rider that was ejected.
123 rider: EntityId,
124 /// The elevator the rider was ejected from.
125 elevator: EntityId,
126 /// The stop the rider was placed at.
127 stop: EntityId,
128 /// The tick when ejection occurred.
129 tick: u64,
130 },
131
132 // -- Dispatch events --
133 /// An elevator was assigned to serve a stop by the dispatcher.
134 ElevatorAssigned {
135 /// The elevator that was assigned.
136 elevator: EntityId,
137 /// The stop it was assigned to serve.
138 stop: EntityId,
139 /// The tick when the assignment occurred.
140 tick: u64,
141 },
142
143 // -- Topology lifecycle events --
144 /// A new stop was added to the simulation.
145 StopAdded {
146 /// The new stop entity.
147 stop: EntityId,
148 /// The line the stop was added to.
149 line: EntityId,
150 /// The group the stop was added to.
151 group: GroupId,
152 /// The tick when the stop was added.
153 tick: u64,
154 },
155 /// A new elevator was added to the simulation.
156 ElevatorAdded {
157 /// The new elevator entity.
158 elevator: EntityId,
159 /// The line the elevator was added to.
160 line: EntityId,
161 /// The group the elevator was added to.
162 group: GroupId,
163 /// The tick when the elevator was added.
164 tick: u64,
165 },
166 /// An entity was disabled.
167 EntityDisabled {
168 /// The entity that was disabled.
169 entity: EntityId,
170 /// The tick when it was disabled.
171 tick: u64,
172 },
173 /// An entity was re-enabled.
174 EntityEnabled {
175 /// The entity that was re-enabled.
176 entity: EntityId,
177 /// The tick when it was enabled.
178 tick: u64,
179 },
180 /// A rider's route was invalidated due to topology change.
181 ///
182 /// Emitted when a stop on a rider's route is disabled or removed.
183 /// If no alternative is found, the rider will abandon after a grace period.
184 RouteInvalidated {
185 /// The affected rider.
186 rider: EntityId,
187 /// The stop that caused the invalidation.
188 affected_stop: EntityId,
189 /// Why the route was invalidated.
190 reason: RouteInvalidReason,
191 /// The tick when invalidation occurred.
192 tick: u64,
193 },
194 /// A rider was manually rerouted via `sim.reroute()` or `sim.reroute_rider()`.
195 RiderRerouted {
196 /// The rerouted rider.
197 rider: EntityId,
198 /// The new destination stop.
199 new_destination: EntityId,
200 /// The tick when rerouting occurred.
201 tick: u64,
202 },
203
204 /// A rider settled at a stop, becoming a resident.
205 RiderSettled {
206 /// The rider that settled.
207 rider: EntityId,
208 /// The stop where the rider settled.
209 stop: EntityId,
210 /// The tick when settlement occurred.
211 tick: u64,
212 },
213 /// A rider was removed from the simulation.
214 RiderDespawned {
215 /// The rider that was removed.
216 rider: EntityId,
217 /// The tick when despawn occurred.
218 tick: u64,
219 },
220
221 // -- Line lifecycle events --
222 /// A line was added to the simulation.
223 LineAdded {
224 /// The new line entity.
225 line: EntityId,
226 /// The group the line was added to.
227 group: GroupId,
228 /// The tick when the line was added.
229 tick: u64,
230 },
231 /// A line was removed from the simulation.
232 LineRemoved {
233 /// The removed line entity.
234 line: EntityId,
235 /// The group the line belonged to.
236 group: GroupId,
237 /// The tick when the line was removed.
238 tick: u64,
239 },
240 /// A line was reassigned to a different group.
241 LineReassigned {
242 /// The line entity that was reassigned.
243 line: EntityId,
244 /// The group the line was previously in.
245 old_group: GroupId,
246 /// The group the line was moved to.
247 new_group: GroupId,
248 /// The tick when reassignment occurred.
249 tick: u64,
250 },
251 /// An elevator was reassigned to a different line.
252 ElevatorReassigned {
253 /// The elevator that was reassigned.
254 elevator: EntityId,
255 /// The line the elevator was previously on.
256 old_line: EntityId,
257 /// The line the elevator was moved to.
258 new_line: EntityId,
259 /// The tick when reassignment occurred.
260 tick: u64,
261 },
262
263 // -- Repositioning events --
264 /// An elevator is being repositioned to improve coverage.
265 ///
266 /// Emitted when an idle elevator begins moving to a new position
267 /// as decided by the [`RepositionStrategy`](crate::dispatch::RepositionStrategy).
268 ElevatorRepositioning {
269 /// The elevator being repositioned.
270 elevator: EntityId,
271 /// The stop it is being sent to.
272 to_stop: EntityId,
273 /// The tick when repositioning began.
274 tick: u64,
275 },
276 /// An elevator completed repositioning and arrived at its target.
277 ///
278 /// Note: this is detected by the movement system — the elevator
279 /// arrives just like any other movement. Games can distinguish
280 /// repositioning arrivals from dispatch arrivals by tracking
281 /// which elevators received `ElevatorRepositioning` events.
282 ElevatorRepositioned {
283 /// The elevator that completed repositioning.
284 elevator: EntityId,
285 /// The stop it arrived at.
286 at_stop: EntityId,
287 /// The tick when it arrived.
288 tick: u64,
289 },
290
291 /// An elevator's service mode was changed.
292 ServiceModeChanged {
293 /// The elevator whose mode changed.
294 elevator: EntityId,
295 /// The previous service mode.
296 from: crate::components::ServiceMode,
297 /// The new service mode.
298 to: crate::components::ServiceMode,
299 /// The tick when the change occurred.
300 tick: u64,
301 },
302
303 // -- Observability events --
304 /// Energy consumed/regenerated by an elevator this tick.
305 ///
306 /// Requires the `energy` feature.
307 #[cfg(feature = "energy")]
308 EnergyConsumed {
309 /// The elevator that consumed energy.
310 elevator: EntityId,
311 /// Energy consumed this tick.
312 consumed: OrderedFloat<f64>,
313 /// Energy regenerated this tick.
314 regenerated: OrderedFloat<f64>,
315 /// The tick when energy was recorded.
316 tick: u64,
317 },
318
319 /// An elevator's load changed (rider boarded or exited).
320 ///
321 /// Emitted immediately after [`RiderBoarded`](Self::RiderBoarded) or
322 /// [`RiderExited`](Self::RiderExited). Useful for real-time capacity
323 /// bar displays in game UIs.
324 ///
325 /// Load values use [`OrderedFloat`](ordered_float::OrderedFloat) for
326 /// `Eq` compatibility. Dereference to get the inner `f64`:
327 ///
328 /// ```rust,ignore
329 /// use elevator_core::events::Event;
330 ///
331 /// if let Event::CapacityChanged { current_load, capacity, .. } = event {
332 /// let pct = *current_load / *capacity * 100.0;
333 /// println!("Elevator at {pct:.0}% capacity");
334 /// }
335 /// ```
336 CapacityChanged {
337 /// The elevator whose load changed.
338 elevator: EntityId,
339 /// Current total weight aboard after the change.
340 current_load: OrderedFloat<f64>,
341 /// Maximum weight capacity of the elevator.
342 capacity: OrderedFloat<f64>,
343 /// The tick when the change occurred.
344 tick: u64,
345 },
346
347 /// An elevator became idle (no more assignments or repositioning).
348 ElevatorIdle {
349 /// The elevator that became idle.
350 elevator: EntityId,
351 /// The stop where it became idle (if at a stop).
352 at_stop: Option<EntityId>,
353 /// The tick when it became idle.
354 tick: u64,
355 },
356
357 /// An elevator's direction indicator lamps changed.
358 ///
359 /// Emitted by the dispatch phase when the pair
360 /// `(going_up, going_down)` transitions to a new value — e.g. when
361 /// a car is assigned a target above it (up-only), below it (down-only),
362 /// or returns to idle (both lamps lit).
363 ///
364 /// Games can use this for UI (lighting up-arrow / down-arrow indicators
365 /// on a car) without polling the elevator each tick.
366 DirectionIndicatorChanged {
367 /// The elevator whose indicator lamps changed.
368 elevator: EntityId,
369 /// New state of the up-direction lamp.
370 going_up: bool,
371 /// New state of the down-direction lamp.
372 going_down: bool,
373 /// The tick when the change occurred.
374 tick: u64,
375 },
376
377 /// An elevator was permanently removed from the simulation.
378 ///
379 /// Distinct from [`EntityDisabled`] — a disabled elevator can be
380 /// re-enabled, but a removed elevator is despawned.
381 ElevatorRemoved {
382 /// The elevator that was removed.
383 elevator: EntityId,
384 /// The line it belonged to.
385 line: EntityId,
386 /// The group it belonged to.
387 group: GroupId,
388 /// The tick when removal occurred.
389 tick: u64,
390 },
391
392 /// A stop was permanently removed from the simulation.
393 ///
394 /// Distinct from [`EntityDisabled`] — a disabled stop can be
395 /// re-enabled, but a removed stop is despawned.
396 StopRemoved {
397 /// The stop that was removed.
398 stop: EntityId,
399 /// The tick when removal occurred.
400 tick: u64,
401 },
402}
403
404/// Reason a rider's route was invalidated.
405#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
406#[non_exhaustive]
407pub enum RouteInvalidReason {
408 /// A stop on the route was disabled.
409 StopDisabled,
410 /// No alternative stop is available in the same group.
411 NoAlternative,
412}
413
414/// Collects simulation events for consumers to drain.
415#[derive(Debug, Default)]
416pub struct EventBus {
417 /// The pending events not yet consumed.
418 events: Vec<Event>,
419}
420
421impl EventBus {
422 /// Pushes a new event onto the bus.
423 pub fn emit(&mut self, event: Event) {
424 self.events.push(event);
425 }
426
427 /// Returns and clears all pending events.
428 pub fn drain(&mut self) -> Vec<Event> {
429 std::mem::take(&mut self.events)
430 }
431
432 /// Returns a slice of all pending events without clearing them.
433 #[must_use]
434 pub fn peek(&self) -> &[Event] {
435 &self.events
436 }
437}
438
439/// A typed event channel for game-specific events.
440///
441/// Games insert this as a global resource on `World`:
442///
443/// ```
444/// use elevator_core::world::World;
445/// use elevator_core::events::EventChannel;
446///
447/// #[derive(Debug)]
448/// enum MyGameEvent { Foo, Bar }
449///
450/// let mut world = World::new();
451/// world.insert_resource(EventChannel::<MyGameEvent>::new());
452/// // Later:
453/// world.resource_mut::<EventChannel<MyGameEvent>>().unwrap().emit(MyGameEvent::Foo);
454/// ```
455#[derive(Debug)]
456pub struct EventChannel<T> {
457 /// Pending events not yet consumed.
458 events: Vec<T>,
459}
460
461impl<T> EventChannel<T> {
462 /// Create an empty event channel.
463 #[must_use]
464 pub const fn new() -> Self {
465 Self { events: Vec::new() }
466 }
467
468 /// Emit an event into the channel.
469 pub fn emit(&mut self, event: T) {
470 self.events.push(event);
471 }
472
473 /// Drain and return all pending events.
474 pub fn drain(&mut self) -> Vec<T> {
475 std::mem::take(&mut self.events)
476 }
477
478 /// Peek at pending events without clearing.
479 #[must_use]
480 pub fn peek(&self) -> &[T] {
481 &self.events
482 }
483
484 /// Check if the channel has no pending events.
485 #[must_use]
486 pub const fn is_empty(&self) -> bool {
487 self.events.is_empty()
488 }
489
490 /// Number of pending events.
491 #[must_use]
492 pub const fn len(&self) -> usize {
493 self.events.len()
494 }
495}
496
497impl<T> Default for EventChannel<T> {
498 fn default() -> Self {
499 Self::new()
500 }
501}