Skip to main content

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