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}