elevator_core/events.rs
1//! Simulation event bus and typed event channels.
2
3use crate::components::CallDirection;
4use crate::entity::EntityId;
5use crate::error::{RejectionContext, RejectionReason};
6use crate::ids::GroupId;
7use ordered_float::OrderedFloat;
8use serde::{Deserialize, Serialize};
9
10/// Events emitted by the simulation during ticks.
11///
12/// All entity references use `EntityId`. Games can look up additional
13/// component data on the referenced entity if needed.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[non_exhaustive]
16pub enum Event {
17 // -- Elevator events --
18 /// An elevator departed from a stop.
19 ElevatorDeparted {
20 /// The elevator that departed.
21 elevator: EntityId,
22 /// The stop it departed from.
23 from_stop: EntityId,
24 /// The tick when departure occurred.
25 tick: u64,
26 },
27 /// An elevator arrived at a stop.
28 ElevatorArrived {
29 /// The elevator that arrived.
30 elevator: EntityId,
31 /// The stop it arrived at.
32 at_stop: EntityId,
33 /// The tick when arrival occurred.
34 tick: u64,
35 },
36 /// An elevator's doors finished opening.
37 DoorOpened {
38 /// The elevator whose doors opened.
39 elevator: EntityId,
40 /// The tick when the doors opened.
41 tick: u64,
42 },
43 /// An elevator's doors finished closing.
44 DoorClosed {
45 /// The elevator whose doors closed.
46 elevator: EntityId,
47 /// The tick when the doors closed.
48 tick: u64,
49 },
50 /// Emitted when an elevator passes a stop without stopping.
51 /// Games/dispatch can use this to decide whether to add stops mid-travel.
52 PassingFloor {
53 /// The elevator passing by.
54 elevator: EntityId,
55 /// The stop being passed.
56 stop: EntityId,
57 /// Direction: true = moving up, false = moving down.
58 moving_up: bool,
59 /// The tick when the pass occurred.
60 tick: u64,
61 },
62
63 // -- Rider events (unified: passengers, cargo, any rideable entity) --
64 /// A new rider appeared at a stop and wants to travel.
65 RiderSpawned {
66 /// The spawned rider entity.
67 rider: EntityId,
68 /// The stop where the rider appeared.
69 origin: EntityId,
70 /// The stop the rider wants to reach.
71 destination: EntityId,
72 /// The tick when the rider spawned.
73 tick: u64,
74 },
75 /// A rider boarded an elevator.
76 RiderBoarded {
77 /// The rider that boarded.
78 rider: EntityId,
79 /// The elevator the rider boarded.
80 elevator: EntityId,
81 /// The tick when boarding occurred.
82 tick: u64,
83 },
84 /// A rider exited an elevator at a stop.
85 #[serde(alias = "RiderAlighted")]
86 RiderExited {
87 /// The rider that exited.
88 rider: EntityId,
89 /// The elevator the rider exited.
90 elevator: EntityId,
91 /// The stop where the rider exited.
92 stop: EntityId,
93 /// The tick when exiting occurred.
94 tick: u64,
95 },
96 /// A rider was rejected from boarding (e.g., over capacity).
97 RiderRejected {
98 /// The rider that was rejected.
99 rider: EntityId,
100 /// The elevator that rejected the rider.
101 elevator: EntityId,
102 /// The reason for rejection.
103 reason: RejectionReason,
104 /// Additional numeric context for the rejection.
105 context: Option<RejectionContext>,
106 /// The tick when rejection occurred.
107 tick: u64,
108 },
109 /// A rider gave up waiting and left the stop.
110 RiderAbandoned {
111 /// The rider that abandoned.
112 rider: EntityId,
113 /// The stop the rider left.
114 stop: EntityId,
115 /// The tick when abandonment occurred.
116 tick: u64,
117 },
118
119 /// A rider was ejected from an elevator (due to disable or despawn).
120 ///
121 /// The rider is moved to `Waiting` phase at the nearest stop.
122 RiderEjected {
123 /// The rider that was ejected.
124 rider: EntityId,
125 /// The elevator the rider was ejected from.
126 elevator: EntityId,
127 /// The stop the rider was placed at.
128 stop: EntityId,
129 /// The tick when ejection occurred.
130 tick: u64,
131 },
132
133 // -- Dispatch events --
134 /// An elevator was assigned to serve a stop by the dispatcher.
135 ElevatorAssigned {
136 /// The elevator that was assigned.
137 elevator: EntityId,
138 /// The stop it was assigned to serve.
139 stop: EntityId,
140 /// The tick when the assignment occurred.
141 tick: u64,
142 },
143
144 // -- Topology lifecycle events --
145 /// A new stop was added to the simulation.
146 StopAdded {
147 /// The new stop entity.
148 stop: EntityId,
149 /// The line the stop was added to.
150 line: EntityId,
151 /// The group the stop was added to.
152 group: GroupId,
153 /// The tick when the stop was added.
154 tick: u64,
155 },
156 /// A new elevator was added to the simulation.
157 ElevatorAdded {
158 /// The new elevator entity.
159 elevator: EntityId,
160 /// The line the elevator was added to.
161 line: EntityId,
162 /// The group the elevator was added to.
163 group: GroupId,
164 /// The tick when the elevator was added.
165 tick: u64,
166 },
167 /// An entity was disabled.
168 EntityDisabled {
169 /// The entity that was disabled.
170 entity: EntityId,
171 /// The tick when it was disabled.
172 tick: u64,
173 },
174 /// An entity was re-enabled.
175 EntityEnabled {
176 /// The entity that was re-enabled.
177 entity: EntityId,
178 /// The tick when it was enabled.
179 tick: u64,
180 },
181 /// A rider's route was invalidated due to topology change.
182 ///
183 /// Emitted when a stop on a rider's route is disabled or removed.
184 /// If no alternative is found, the rider will abandon after a grace period.
185 RouteInvalidated {
186 /// The affected rider.
187 rider: EntityId,
188 /// The stop that caused the invalidation.
189 affected_stop: EntityId,
190 /// Why the route was invalidated.
191 reason: RouteInvalidReason,
192 /// The tick when invalidation occurred.
193 tick: u64,
194 },
195 /// A rider was manually rerouted via `sim.reroute()` or `sim.reroute_rider()`.
196 RiderRerouted {
197 /// The rerouted rider.
198 rider: EntityId,
199 /// The new destination stop.
200 new_destination: EntityId,
201 /// The tick when rerouting occurred.
202 tick: u64,
203 },
204
205 /// A rider settled at a stop, becoming a resident.
206 RiderSettled {
207 /// The rider that settled.
208 rider: EntityId,
209 /// The stop where the rider settled.
210 stop: EntityId,
211 /// The tick when settlement occurred.
212 tick: u64,
213 },
214 /// A rider was removed from the simulation.
215 RiderDespawned {
216 /// The rider that was removed.
217 rider: EntityId,
218 /// The tick when despawn occurred.
219 tick: u64,
220 },
221
222 // -- Line lifecycle events --
223 /// A line was added to the simulation.
224 LineAdded {
225 /// The new line entity.
226 line: EntityId,
227 /// The group the line was added to.
228 group: GroupId,
229 /// The tick when the line was added.
230 tick: u64,
231 },
232 /// A line was removed from the simulation.
233 LineRemoved {
234 /// The removed line entity.
235 line: EntityId,
236 /// The group the line belonged to.
237 group: GroupId,
238 /// The tick when the line was removed.
239 tick: u64,
240 },
241 /// A line was reassigned to a different group.
242 LineReassigned {
243 /// The line entity that was reassigned.
244 line: EntityId,
245 /// The group the line was previously in.
246 old_group: GroupId,
247 /// The group the line was moved to.
248 new_group: GroupId,
249 /// The tick when reassignment occurred.
250 tick: u64,
251 },
252 /// An elevator was reassigned to a different line.
253 ElevatorReassigned {
254 /// The elevator that was reassigned.
255 elevator: EntityId,
256 /// The line the elevator was previously on.
257 old_line: EntityId,
258 /// The line the elevator was moved to.
259 new_line: EntityId,
260 /// The tick when reassignment occurred.
261 tick: u64,
262 },
263
264 // -- Repositioning events --
265 /// An elevator is being repositioned to improve coverage.
266 ///
267 /// Emitted when an idle elevator begins moving to a new position
268 /// as decided by the [`RepositionStrategy`](crate::dispatch::RepositionStrategy).
269 ElevatorRepositioning {
270 /// The elevator being repositioned.
271 elevator: EntityId,
272 /// The stop it is being sent to.
273 to_stop: EntityId,
274 /// The tick when repositioning began.
275 tick: u64,
276 },
277 /// An elevator completed repositioning and arrived at its target.
278 ///
279 /// Note: this is detected by the movement system — the elevator
280 /// arrives just like any other movement. Games can distinguish
281 /// repositioning arrivals from dispatch arrivals by tracking
282 /// which elevators received `ElevatorRepositioning` events.
283 ElevatorRepositioned {
284 /// The elevator that completed repositioning.
285 elevator: EntityId,
286 /// The stop it arrived at.
287 at_stop: EntityId,
288 /// The tick when it arrived.
289 tick: u64,
290 },
291
292 /// An elevator's service mode was changed.
293 ServiceModeChanged {
294 /// The elevator whose mode changed.
295 elevator: EntityId,
296 /// The previous service mode.
297 from: crate::components::ServiceMode,
298 /// The new service mode.
299 to: crate::components::ServiceMode,
300 /// The tick when the change occurred.
301 tick: u64,
302 },
303
304 // -- Observability events --
305 /// Energy consumed/regenerated by an elevator this tick.
306 ///
307 /// Requires the `energy` feature.
308 #[cfg(feature = "energy")]
309 EnergyConsumed {
310 /// The elevator that consumed energy.
311 elevator: EntityId,
312 /// Energy consumed this tick.
313 consumed: OrderedFloat<f64>,
314 /// Energy regenerated this tick.
315 regenerated: OrderedFloat<f64>,
316 /// The tick when energy was recorded.
317 tick: u64,
318 },
319
320 /// An elevator's load changed (rider boarded or exited).
321 ///
322 /// Emitted immediately after [`RiderBoarded`](Self::RiderBoarded) or
323 /// [`RiderExited`](Self::RiderExited). Useful for real-time capacity
324 /// bar displays in game UIs.
325 ///
326 /// Load values use [`OrderedFloat`] for
327 /// `Eq` compatibility. Dereference to get the inner `f64`:
328 ///
329 /// ```rust,ignore
330 /// use elevator_core::events::Event;
331 ///
332 /// if let Event::CapacityChanged { current_load, capacity, .. } = event {
333 /// let pct = *current_load / *capacity * 100.0;
334 /// println!("Elevator at {pct:.0}% capacity");
335 /// }
336 /// ```
337 CapacityChanged {
338 /// The elevator whose load changed.
339 elevator: EntityId,
340 /// Current total weight aboard after the change.
341 current_load: OrderedFloat<f64>,
342 /// Maximum weight capacity of the elevator.
343 capacity: OrderedFloat<f64>,
344 /// The tick when the change occurred.
345 tick: u64,
346 },
347
348 /// An elevator became idle (no more assignments or repositioning).
349 ElevatorIdle {
350 /// The elevator that became idle.
351 elevator: EntityId,
352 /// The stop where it became idle (if at a stop).
353 at_stop: Option<EntityId>,
354 /// The tick when it became idle.
355 tick: u64,
356 },
357
358 /// An elevator's direction indicator lamps changed.
359 ///
360 /// Emitted by the dispatch phase when the pair
361 /// `(going_up, going_down)` transitions to a new value — e.g. when
362 /// a car is assigned a target above it (up-only), below it (down-only),
363 /// or returns to idle (both lamps lit).
364 ///
365 /// Games can use this for UI (lighting up-arrow / down-arrow indicators
366 /// on a car) without polling the elevator each tick.
367 DirectionIndicatorChanged {
368 /// The elevator whose indicator lamps changed.
369 elevator: EntityId,
370 /// New state of the up-direction lamp.
371 going_up: bool,
372 /// New state of the down-direction lamp.
373 going_down: bool,
374 /// The tick when the change occurred.
375 tick: u64,
376 },
377
378 /// An elevator was permanently removed from the simulation.
379 ///
380 /// Distinct from [`Event::EntityDisabled`] — a disabled elevator can be
381 /// re-enabled, but a removed elevator is despawned.
382 ElevatorRemoved {
383 /// The elevator that was removed.
384 elevator: EntityId,
385 /// The line it belonged to.
386 line: EntityId,
387 /// The group it belonged to.
388 group: GroupId,
389 /// The tick when removal occurred.
390 tick: u64,
391 },
392
393 /// A stop was queued as a destination for an elevator.
394 ///
395 /// Emitted by [`Simulation::push_destination`](crate::sim::Simulation::push_destination),
396 /// [`Simulation::push_destination_front`](crate::sim::Simulation::push_destination_front),
397 /// and the built-in dispatch phase whenever it actually appends a stop
398 /// to an elevator's [`DestinationQueue`](crate::components::DestinationQueue).
399 /// Adjacent-duplicate pushes (that are deduplicated) do not emit.
400 DestinationQueued {
401 /// The elevator whose queue was updated.
402 elevator: EntityId,
403 /// The stop that was queued.
404 stop: EntityId,
405 /// The tick when the push occurred.
406 tick: u64,
407 },
408
409 /// A stop was permanently removed from the simulation.
410 ///
411 /// Distinct from [`Event::EntityDisabled`] — a disabled stop can be
412 /// re-enabled, but a removed stop is despawned.
413 StopRemoved {
414 /// The stop that was removed.
415 stop: EntityId,
416 /// The tick when removal occurred.
417 tick: u64,
418 },
419
420 /// A manual door-control command was received and either applied
421 /// immediately or stored for later.
422 ///
423 /// Emitted by
424 /// [`Simulation::open_door`](crate::sim::Simulation::open_door),
425 /// [`Simulation::close_door`](crate::sim::Simulation::close_door),
426 /// [`Simulation::hold_door`](crate::sim::Simulation::hold_door),
427 /// and [`Simulation::cancel_door_hold`](crate::sim::Simulation::cancel_door_hold)
428 /// when the command is accepted. Paired with
429 /// [`Event::DoorCommandApplied`] when the command eventually takes effect.
430 DoorCommandQueued {
431 /// The elevator targeted by the command.
432 elevator: EntityId,
433 /// The command that was queued.
434 command: crate::door::DoorCommand,
435 /// The tick when the command was submitted.
436 tick: u64,
437 },
438 /// A queued door-control command actually took effect — doors began
439 /// opening/closing or a hold was applied.
440 DoorCommandApplied {
441 /// The elevator the command applied to.
442 elevator: EntityId,
443 /// The command that was applied.
444 command: crate::door::DoorCommand,
445 /// The tick when the command was applied.
446 tick: u64,
447 },
448
449 /// An elevator parameter was mutated at runtime via one of the
450 /// `Simulation::set_*` upgrade setters (e.g. buying a speed upgrade
451 /// in an RPG, or a scripted event changing capacity mid-game).
452 ///
453 /// Emitted immediately when the setter succeeds. Games can use this
454 /// to trigger score popups, SFX, or UI updates.
455 ElevatorUpgraded {
456 /// The elevator whose parameter changed.
457 elevator: EntityId,
458 /// Which field was changed.
459 field: UpgradeField,
460 /// Previous value of the field.
461 old: UpgradeValue,
462 /// New value of the field.
463 new: UpgradeValue,
464 /// The tick when the upgrade was applied.
465 tick: u64,
466 },
467
468 /// A velocity command was submitted to an elevator running in
469 /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
470 ///
471 /// Emitted by
472 /// [`Simulation::set_target_velocity`](crate::sim::Simulation::set_target_velocity)
473 /// and [`Simulation::emergency_stop`](crate::sim::Simulation::emergency_stop).
474 ManualVelocityCommanded {
475 /// The elevator targeted by the command.
476 elevator: EntityId,
477 /// The new target velocity (clamped to `[-max_speed, max_speed]`),
478 /// or `None` when the command clears the target.
479 target_velocity: Option<ordered_float::OrderedFloat<f64>>,
480 /// The tick when the command was submitted.
481 tick: u64,
482 },
483
484 // -- Hall & car call events --
485 /// A hall button was pressed. Fires immediately, before any ack
486 /// latency delay. Games use this for button-light animations or SFX.
487 HallButtonPressed {
488 /// Stop where the press occurred.
489 stop: EntityId,
490 /// Direction the button requests service in.
491 direction: CallDirection,
492 /// Tick of the press.
493 tick: u64,
494 },
495 /// A hall call has elapsed its ack latency and is now visible to
496 /// dispatch. Useful for UI confirmations ("your call has been
497 /// received"). In groups with `ack_latency_ticks = 0` this fires on
498 /// the same tick as `HallButtonPressed`.
499 HallCallAcknowledged {
500 /// Stop the call was placed at.
501 stop: EntityId,
502 /// Direction of the call.
503 direction: CallDirection,
504 /// Tick acknowledgement completed.
505 tick: u64,
506 },
507 /// A hall call was cleared when an assigned car arrived at the stop
508 /// with matching direction indicators. Corresponds to the real-world
509 /// button-light turning off.
510 HallCallCleared {
511 /// Stop whose call was cleared.
512 stop: EntityId,
513 /// Direction of the cleared call.
514 direction: CallDirection,
515 /// Car that cleared the call by arriving.
516 car: EntityId,
517 /// Tick the call was cleared.
518 tick: u64,
519 },
520 /// A rider inside a car pressed a floor button (Classic mode only).
521 /// In Destination mode riders reveal destinations at the hall
522 /// kiosk, so no car buttons are pressed.
523 CarButtonPressed {
524 /// Elevator the button was pressed inside.
525 car: EntityId,
526 /// Floor the rider requested.
527 floor: EntityId,
528 /// Rider who pressed the button. `None` when the press is
529 /// synthetic — e.g. issued via
530 /// [`Simulation::press_car_button`](crate::sim::Simulation::press_car_button)
531 /// for scripted events, player input, or cutscene cues with no
532 /// associated rider entity.
533 rider: Option<EntityId>,
534 /// Tick of the press.
535 tick: u64,
536 },
537 /// A rider balked at boarding a car they considered too crowded
538 /// (their preferences filtered the car out). The rider remains
539 /// Waiting and may board a later car.
540 RiderBalked {
541 /// Rider who balked.
542 rider: EntityId,
543 /// Elevator they declined to board.
544 elevator: EntityId,
545 /// Stop where the balking happened.
546 at_stop: EntityId,
547 /// Tick of the balk.
548 tick: u64,
549 },
550}
551
552/// Identifies which elevator parameter was changed in an
553/// [`Event::ElevatorUpgraded`].
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
555#[non_exhaustive]
556pub enum UpgradeField {
557 /// Maximum travel speed (distance/tick).
558 MaxSpeed,
559 /// Acceleration rate (distance/tick^2).
560 Acceleration,
561 /// Deceleration rate (distance/tick^2).
562 Deceleration,
563 /// Maximum weight the car can carry.
564 WeightCapacity,
565 /// Ticks for a door open/close transition.
566 DoorTransitionTicks,
567 /// Ticks the door stays fully open.
568 DoorOpenTicks,
569}
570
571impl std::fmt::Display for UpgradeField {
572 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
573 match self {
574 Self::MaxSpeed => write!(f, "max_speed"),
575 Self::Acceleration => write!(f, "acceleration"),
576 Self::Deceleration => write!(f, "deceleration"),
577 Self::WeightCapacity => write!(f, "weight_capacity"),
578 Self::DoorTransitionTicks => write!(f, "door_transition_ticks"),
579 Self::DoorOpenTicks => write!(f, "door_open_ticks"),
580 }
581 }
582}
583
584/// Old-or-new value carried by [`Event::ElevatorUpgraded`].
585///
586/// Uses [`OrderedFloat`] for the float variant so the event enum
587/// remains `Eq`-comparable alongside the other observability events.
588#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
589#[non_exhaustive]
590pub enum UpgradeValue {
591 /// A floating-point parameter value (speed, accel, decel, capacity).
592 Float(OrderedFloat<f64>),
593 /// An integral tick-count parameter value (door timings).
594 Ticks(u32),
595}
596
597impl UpgradeValue {
598 /// Construct a float-valued upgrade payload.
599 #[must_use]
600 pub const fn float(v: f64) -> Self {
601 Self::Float(OrderedFloat(v))
602 }
603
604 /// Construct a tick-valued upgrade payload.
605 #[must_use]
606 pub const fn ticks(v: u32) -> Self {
607 Self::Ticks(v)
608 }
609}
610
611impl std::fmt::Display for UpgradeValue {
612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613 match self {
614 Self::Float(v) => write!(f, "{}", **v),
615 Self::Ticks(v) => write!(f, "{v}"),
616 }
617 }
618}
619
620/// Reason a rider's route was invalidated.
621#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
622#[non_exhaustive]
623pub enum RouteInvalidReason {
624 /// A stop on the route was disabled.
625 StopDisabled,
626 /// No alternative stop is available in the same group.
627 NoAlternative,
628}
629
630/// Coarse-grained classification of an [`Event`].
631///
632/// Exposes the same grouping that the `Event` variants are already
633/// commented under, so consumers can filter a drained event stream with
634/// one match arm per category rather than enumerating ~25 variants.
635#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
636#[non_exhaustive]
637pub enum EventCategory {
638 /// Elevator motion, arrival/departure, and door state.
639 Elevator,
640 /// Rider lifecycle: spawn, board, exit, reject, abandon, despawn, settle, reroute.
641 Rider,
642 /// Dispatch decisions and bookkeeping.
643 Dispatch,
644 /// Runtime topology mutations (entities/lines/groups added, removed, reassigned).
645 Topology,
646 /// Idle-elevator repositioning activity.
647 Reposition,
648 /// Direction indicator lamp state changes.
649 Direction,
650 /// Observability and misc signals (capacity, service mode, energy, etc.).
651 Observability,
652}
653
654impl Event {
655 /// Classify this event into a coarse-grained [`EventCategory`].
656 ///
657 /// Useful when a consumer only cares about, say, rider activity and
658 /// wants to skip elevator-motion and topology chatter without
659 /// enumerating every variant. The exhaustive match inside guarantees
660 /// the method stays in sync when new variants are added.
661 #[must_use]
662 pub const fn category(&self) -> EventCategory {
663 match self {
664 Self::ElevatorDeparted { .. }
665 | Self::ElevatorArrived { .. }
666 | Self::DoorOpened { .. }
667 | Self::DoorClosed { .. }
668 | Self::DoorCommandQueued { .. }
669 | Self::DoorCommandApplied { .. }
670 | Self::PassingFloor { .. }
671 | Self::ElevatorIdle { .. } => EventCategory::Elevator,
672 Self::RiderSpawned { .. }
673 | Self::RiderBoarded { .. }
674 | Self::RiderExited { .. }
675 | Self::RiderRejected { .. }
676 | Self::RiderAbandoned { .. }
677 | Self::RiderEjected { .. }
678 | Self::RouteInvalidated { .. }
679 | Self::RiderRerouted { .. }
680 | Self::RiderSettled { .. }
681 | Self::RiderDespawned { .. } => EventCategory::Rider,
682 Self::ElevatorAssigned { .. } | Self::DestinationQueued { .. } => {
683 EventCategory::Dispatch
684 }
685 Self::StopAdded { .. }
686 | Self::StopRemoved { .. }
687 | Self::ElevatorAdded { .. }
688 | Self::ElevatorRemoved { .. }
689 | Self::EntityDisabled { .. }
690 | Self::EntityEnabled { .. }
691 | Self::LineAdded { .. }
692 | Self::LineRemoved { .. }
693 | Self::LineReassigned { .. }
694 | Self::ElevatorReassigned { .. } => EventCategory::Topology,
695 Self::ElevatorRepositioning { .. } | Self::ElevatorRepositioned { .. } => {
696 EventCategory::Reposition
697 }
698 Self::DirectionIndicatorChanged { .. } => EventCategory::Direction,
699 Self::ServiceModeChanged { .. }
700 | Self::CapacityChanged { .. }
701 | Self::ElevatorUpgraded { .. }
702 | Self::ManualVelocityCommanded { .. } => EventCategory::Observability,
703 #[cfg(feature = "energy")]
704 Self::EnergyConsumed { .. } => EventCategory::Observability,
705 Self::HallButtonPressed { .. }
706 | Self::HallCallAcknowledged { .. }
707 | Self::HallCallCleared { .. }
708 | Self::CarButtonPressed { .. } => EventCategory::Dispatch,
709 Self::RiderBalked { .. } => EventCategory::Rider,
710 }
711 }
712}
713
714/// Collects simulation events for consumers to drain.
715#[derive(Debug, Default)]
716pub struct EventBus {
717 /// The pending events not yet consumed.
718 events: Vec<Event>,
719}
720
721impl EventBus {
722 /// Pushes a new event onto the bus.
723 pub fn emit(&mut self, event: Event) {
724 self.events.push(event);
725 }
726
727 /// Returns and clears all pending events.
728 pub fn drain(&mut self) -> Vec<Event> {
729 std::mem::take(&mut self.events)
730 }
731
732 /// Returns a slice of all pending events without clearing them.
733 #[must_use]
734 pub fn peek(&self) -> &[Event] {
735 &self.events
736 }
737}
738
739/// A typed event channel for game-specific events.
740///
741/// Games insert this as a global resource on `World`:
742///
743/// ```
744/// use elevator_core::world::World;
745/// use elevator_core::events::EventChannel;
746///
747/// #[derive(Debug)]
748/// enum MyGameEvent { Foo, Bar }
749///
750/// let mut world = World::new();
751/// world.insert_resource(EventChannel::<MyGameEvent>::new());
752/// // Later:
753/// world.resource_mut::<EventChannel<MyGameEvent>>().unwrap().emit(MyGameEvent::Foo);
754/// ```
755#[derive(Debug)]
756pub struct EventChannel<T> {
757 /// Pending events not yet consumed.
758 events: Vec<T>,
759}
760
761impl<T> EventChannel<T> {
762 /// Create an empty event channel.
763 #[must_use]
764 pub const fn new() -> Self {
765 Self { events: Vec::new() }
766 }
767
768 /// Emit an event into the channel.
769 pub fn emit(&mut self, event: T) {
770 self.events.push(event);
771 }
772
773 /// Drain and return all pending events.
774 pub fn drain(&mut self) -> Vec<T> {
775 std::mem::take(&mut self.events)
776 }
777
778 /// Peek at pending events without clearing.
779 #[must_use]
780 pub fn peek(&self) -> &[T] {
781 &self.events
782 }
783
784 /// Check if the channel has no pending events.
785 #[must_use]
786 pub const fn is_empty(&self) -> bool {
787 self.events.is_empty()
788 }
789
790 /// Number of pending events.
791 #[must_use]
792 pub const fn len(&self) -> usize {
793 self.events.len()
794 }
795}
796
797impl<T> Default for EventChannel<T> {
798 fn default() -> Self {
799 Self::new()
800 }
801}