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        /// Opaque consumer tag attached to the rider at emit time. `0`
92        /// when the rider is untagged. See [`Simulation::set_rider_tag`]
93        /// for the back-pointer pattern this enables.
94        ///
95        /// [`Simulation::set_rider_tag`]: crate::sim::Simulation::set_rider_tag
96        #[serde(default)]
97        tag: u64,
98        /// The tick when the rider spawned.
99        tick: u64,
100    },
101    /// A rider boarded an elevator.
102    RiderBoarded {
103        /// The rider that boarded.
104        rider: EntityId,
105        /// The elevator the rider boarded.
106        elevator: EntityId,
107        /// Opaque consumer tag attached to the rider at emit time. `0`
108        /// when the rider is untagged.
109        #[serde(default)]
110        tag: u64,
111        /// The tick when boarding occurred.
112        tick: u64,
113    },
114    /// A rider exited an elevator at a stop.
115    #[serde(alias = "RiderAlighted")]
116    RiderExited {
117        /// The rider that exited.
118        rider: EntityId,
119        /// The elevator the rider exited.
120        elevator: EntityId,
121        /// The stop where the rider exited.
122        stop: EntityId,
123        /// Opaque consumer tag attached to the rider at emit time. `0`
124        /// when the rider is untagged. Sampled before the rider is freed
125        /// so consumers can correlate the exit with external state even
126        /// after the [`RiderId`](crate::entity::RiderId) becomes stale.
127        #[serde(default)]
128        tag: u64,
129        /// The tick when exiting occurred.
130        tick: u64,
131    },
132    /// A rider was rejected from boarding (e.g., over capacity).
133    RiderRejected {
134        /// The rider that was rejected.
135        rider: EntityId,
136        /// The elevator that rejected the rider.
137        elevator: EntityId,
138        /// The reason for rejection.
139        reason: RejectionReason,
140        /// Additional numeric context for the rejection.
141        context: Option<RejectionContext>,
142        /// Opaque consumer tag attached to the rider at emit time. `0`
143        /// when the rider is untagged.
144        #[serde(default)]
145        tag: u64,
146        /// The tick when rejection occurred.
147        tick: u64,
148    },
149    /// A rider gave up waiting and left the stop.
150    RiderAbandoned {
151        /// The rider that abandoned.
152        rider: EntityId,
153        /// The stop the rider left.
154        stop: EntityId,
155        /// Opaque consumer tag attached to the rider at emit time. `0`
156        /// when the rider is untagged. Sampled before the rider is freed.
157        #[serde(default)]
158        tag: u64,
159        /// The tick when abandonment occurred.
160        tick: u64,
161    },
162
163    /// A rider was ejected from an elevator (due to disable or despawn).
164    ///
165    /// The rider is moved to `Waiting` phase at the nearest stop.
166    RiderEjected {
167        /// The rider that was ejected.
168        rider: EntityId,
169        /// The elevator the rider was ejected from.
170        elevator: EntityId,
171        /// The stop the rider was placed at.
172        stop: EntityId,
173        /// Opaque consumer tag attached to the rider at emit time. `0`
174        /// when the rider is untagged.
175        #[serde(default)]
176        tag: u64,
177        /// The tick when ejection occurred.
178        tick: u64,
179    },
180
181    // -- Dispatch events --
182    /// An elevator was assigned to serve a stop by the dispatcher.
183    ElevatorAssigned {
184        /// The elevator that was assigned.
185        elevator: EntityId,
186        /// The stop it was assigned to serve.
187        stop: EntityId,
188        /// The tick when the assignment occurred.
189        tick: u64,
190    },
191
192    // -- Topology lifecycle events --
193    /// A new stop was added to the simulation.
194    StopAdded {
195        /// The new stop entity.
196        stop: EntityId,
197        /// The line the stop was added to.
198        line: EntityId,
199        /// The group the stop was added to.
200        group: GroupId,
201        /// The tick when the stop was added.
202        tick: u64,
203    },
204    /// A new elevator was added to the simulation.
205    ElevatorAdded {
206        /// The new elevator entity.
207        elevator: EntityId,
208        /// The line the elevator was added to.
209        line: EntityId,
210        /// The group the elevator was added to.
211        group: GroupId,
212        /// The tick when the elevator was added.
213        tick: u64,
214    },
215    /// An entity was disabled.
216    EntityDisabled {
217        /// The entity that was disabled.
218        entity: EntityId,
219        /// The tick when it was disabled.
220        tick: u64,
221    },
222    /// An entity was re-enabled.
223    EntityEnabled {
224        /// The entity that was re-enabled.
225        entity: EntityId,
226        /// The tick when it was enabled.
227        tick: u64,
228    },
229    /// A rider's route was invalidated due to topology change.
230    ///
231    /// Emitted when a stop on a rider's route is disabled or removed.
232    /// If no alternative is found, the rider will abandon after a grace period.
233    RouteInvalidated {
234        /// The affected rider.
235        rider: EntityId,
236        /// The stop that caused the invalidation.
237        affected_stop: EntityId,
238        /// Why the route was invalidated.
239        reason: RouteInvalidReason,
240        /// Opaque consumer tag attached to the rider at emit time. `0`
241        /// when the rider is untagged.
242        #[serde(default)]
243        tag: u64,
244        /// The tick when invalidation occurred.
245        tick: u64,
246    },
247    /// A rider was manually rerouted via `sim.reroute()`.
248    RiderRerouted {
249        /// The rerouted rider.
250        rider: EntityId,
251        /// The new destination stop.
252        new_destination: EntityId,
253        /// Opaque consumer tag attached to the rider at emit time. `0`
254        /// when the rider is untagged.
255        #[serde(default)]
256        tag: u64,
257        /// The tick when rerouting occurred.
258        tick: u64,
259    },
260
261    /// A rider settled at a stop, becoming a resident.
262    RiderSettled {
263        /// The rider that settled.
264        rider: EntityId,
265        /// The stop where the rider settled.
266        stop: EntityId,
267        /// Opaque consumer tag attached to the rider at emit time. `0`
268        /// when the rider is untagged.
269        #[serde(default)]
270        tag: u64,
271        /// The tick when settlement occurred.
272        tick: u64,
273    },
274    /// A rider was removed from the simulation.
275    RiderDespawned {
276        /// The rider that was removed.
277        rider: EntityId,
278        /// Opaque consumer tag attached to the rider at emit time. `0`
279        /// when the rider is untagged. Sampled before the rider is freed.
280        #[serde(default)]
281        tag: u64,
282        /// The tick when despawn occurred.
283        tick: u64,
284    },
285
286    // -- Line lifecycle events --
287    /// A line was added to the simulation.
288    LineAdded {
289        /// The new line entity.
290        line: EntityId,
291        /// The group the line was added to.
292        group: GroupId,
293        /// The tick when the line was added.
294        tick: u64,
295    },
296    /// A line was removed from the simulation.
297    LineRemoved {
298        /// The removed line entity.
299        line: EntityId,
300        /// The group the line belonged to.
301        group: GroupId,
302        /// The tick when the line was removed.
303        tick: u64,
304    },
305    /// A line was reassigned to a different group.
306    LineReassigned {
307        /// The line entity that was reassigned.
308        line: EntityId,
309        /// The group the line was previously in.
310        old_group: GroupId,
311        /// The group the line was moved to.
312        new_group: GroupId,
313        /// The tick when reassignment occurred.
314        tick: u64,
315    },
316    /// An elevator was reassigned to a different line.
317    ElevatorReassigned {
318        /// The elevator that was reassigned.
319        elevator: EntityId,
320        /// The line the elevator was previously on.
321        old_line: EntityId,
322        /// The line the elevator was moved to.
323        new_line: EntityId,
324        /// The tick when reassignment occurred.
325        tick: u64,
326    },
327
328    // -- Repositioning events --
329    /// An elevator is being repositioned to improve coverage.
330    ///
331    /// Emitted when an idle elevator begins moving to a new position
332    /// as decided by the [`RepositionStrategy`](crate::dispatch::RepositionStrategy).
333    ElevatorRepositioning {
334        /// The elevator being repositioned.
335        elevator: EntityId,
336        /// The stop it is being sent to.
337        to_stop: EntityId,
338        /// The tick when repositioning began.
339        tick: u64,
340    },
341    /// An elevator completed repositioning and arrived at its target.
342    ///
343    /// Note: this is detected by the movement system — the elevator
344    /// arrives just like any other movement. Games can distinguish
345    /// repositioning arrivals from dispatch arrivals by tracking
346    /// which elevators received `ElevatorRepositioning` events.
347    ElevatorRepositioned {
348        /// The elevator that completed repositioning.
349        elevator: EntityId,
350        /// The stop it arrived at.
351        at_stop: EntityId,
352        /// The tick when it arrived.
353        tick: u64,
354    },
355
356    /// An elevator was recalled to a specific stop via
357    /// [`Simulation::recall_to`](crate::sim::Simulation::recall_to).
358    ///
359    /// The car's destination queue has been replaced with the recall
360    /// target. If the car was mid-flight, it continues to its current
361    /// target, then proceeds to the recall stop. If idle, it departs
362    /// on the next tick.
363    ElevatorRecalled {
364        /// The elevator that was recalled.
365        elevator: EntityId,
366        /// The stop the elevator is being sent to.
367        to_stop: EntityId,
368        /// The tick when the recall was issued.
369        tick: u64,
370    },
371
372    /// An elevator's service mode was changed.
373    ServiceModeChanged {
374        /// The elevator whose mode changed.
375        elevator: EntityId,
376        /// The previous service mode.
377        from: crate::components::ServiceMode,
378        /// The new service mode.
379        to: crate::components::ServiceMode,
380        /// The tick when the change occurred.
381        tick: u64,
382    },
383
384    // -- Observability events --
385    /// Energy consumed/regenerated by an elevator this tick.
386    ///
387    /// Requires the `energy` feature.
388    #[cfg(feature = "energy")]
389    EnergyConsumed {
390        /// The elevator that consumed energy.
391        elevator: EntityId,
392        /// Energy consumed this tick.
393        consumed: OrderedFloat<f64>,
394        /// Energy regenerated this tick.
395        regenerated: OrderedFloat<f64>,
396        /// The tick when energy was recorded.
397        tick: u64,
398    },
399
400    /// An elevator's load changed (rider boarded or exited).
401    ///
402    /// Emitted immediately after [`RiderBoarded`](Self::RiderBoarded) or
403    /// [`RiderExited`](Self::RiderExited). Useful for real-time capacity
404    /// bar displays in game UIs.
405    ///
406    /// Load values use [`OrderedFloat`] for
407    /// `Eq` compatibility. Dereference to get the inner `f64`:
408    ///
409    /// ```rust,no_run
410    /// use elevator_core::events::Event;
411    /// # fn handle(event: Event) {
412    /// if let Event::CapacityChanged { current_load, capacity, .. } = event {
413    ///     let pct = *current_load / *capacity * 100.0;
414    ///     println!("Elevator at {pct:.0}% capacity");
415    /// }
416    /// # }
417    /// ```
418    CapacityChanged {
419        /// The elevator whose load changed.
420        elevator: EntityId,
421        /// Current total weight aboard after the change.
422        current_load: OrderedFloat<f64>,
423        /// Maximum weight capacity of the elevator.
424        capacity: OrderedFloat<f64>,
425        /// The tick when the change occurred.
426        tick: u64,
427    },
428
429    /// An elevator became idle (no more assignments or repositioning).
430    ElevatorIdle {
431        /// The elevator that became idle.
432        elevator: EntityId,
433        /// The stop where it became idle (if at a stop).
434        at_stop: Option<EntityId>,
435        /// The tick when it became idle.
436        tick: u64,
437    },
438
439    /// An elevator's direction indicator lamps changed.
440    ///
441    /// Emitted by the dispatch phase when the pair
442    /// `(going_up, going_down)` transitions to a new value — e.g. when
443    /// a car is assigned a target above it (up-only), below it (down-only),
444    /// or returns to idle (both lamps lit).
445    ///
446    /// Games can use this for UI (lighting up-arrow / down-arrow indicators
447    /// on a car) without polling the elevator each tick.
448    DirectionIndicatorChanged {
449        /// The elevator whose indicator lamps changed.
450        elevator: EntityId,
451        /// New state of the up-direction lamp.
452        going_up: bool,
453        /// New state of the down-direction lamp.
454        going_down: bool,
455        /// The tick when the change occurred.
456        tick: u64,
457    },
458
459    /// An elevator was permanently removed from the simulation.
460    ///
461    /// Distinct from [`Event::EntityDisabled`] — a disabled elevator can be
462    /// re-enabled, but a removed elevator is despawned.
463    ElevatorRemoved {
464        /// The elevator that was removed.
465        elevator: EntityId,
466        /// The line it belonged to.
467        line: EntityId,
468        /// The group it belonged to.
469        group: GroupId,
470        /// The tick when removal occurred.
471        tick: u64,
472    },
473
474    /// A stop was queued as a destination for an elevator.
475    ///
476    /// Emitted by [`Simulation::push_destination`](crate::sim::Simulation::push_destination),
477    /// [`Simulation::push_destination_front`](crate::sim::Simulation::push_destination_front),
478    /// and the built-in dispatch phase whenever it actually appends a stop
479    /// to an elevator's [`DestinationQueue`](crate::components::DestinationQueue).
480    /// Adjacent-duplicate pushes (that are deduplicated) do not emit.
481    DestinationQueued {
482        /// The elevator whose queue was updated.
483        elevator: EntityId,
484        /// The stop that was queued.
485        stop: EntityId,
486        /// The tick when the push occurred.
487        tick: u64,
488    },
489
490    /// A stop was permanently removed from the simulation.
491    ///
492    /// Distinct from [`Event::EntityDisabled`] — a disabled stop can be
493    /// re-enabled, but a removed stop is despawned.
494    StopRemoved {
495        /// The stop that was removed.
496        stop: EntityId,
497        /// The tick when removal occurred.
498        tick: u64,
499    },
500
501    /// A manual door-control command was received and either applied
502    /// immediately or stored for later.
503    ///
504    /// Emitted by
505    /// [`Simulation::open_door`](crate::sim::Simulation::open_door),
506    /// [`Simulation::close_door`](crate::sim::Simulation::close_door),
507    /// [`Simulation::hold_door`](crate::sim::Simulation::hold_door),
508    /// and [`Simulation::cancel_door_hold`](crate::sim::Simulation::cancel_door_hold)
509    /// when the command is accepted. Paired with
510    /// [`Event::DoorCommandApplied`] when the command eventually takes effect.
511    DoorCommandQueued {
512        /// The elevator targeted by the command.
513        elevator: EntityId,
514        /// The command that was queued.
515        command: crate::door::DoorCommand,
516        /// The tick when the command was submitted.
517        tick: u64,
518    },
519    /// A queued door-control command actually took effect — doors began
520    /// opening/closing or a hold was applied.
521    DoorCommandApplied {
522        /// The elevator the command applied to.
523        elevator: EntityId,
524        /// The command that was applied.
525        command: crate::door::DoorCommand,
526        /// The tick when the command was applied.
527        tick: u64,
528    },
529
530    /// An elevator parameter was mutated at runtime via one of the
531    /// `Simulation::set_*` upgrade setters (e.g. buying a speed upgrade
532    /// in an RPG, or a scripted event changing capacity mid-game).
533    ///
534    /// Emitted immediately when the setter succeeds. Games can use this
535    /// to trigger score popups, SFX, or UI updates.
536    ElevatorUpgraded {
537        /// The elevator whose parameter changed.
538        elevator: EntityId,
539        /// Which field was changed.
540        field: UpgradeField,
541        /// Previous value of the field.
542        old: UpgradeValue,
543        /// New value of the field.
544        new: UpgradeValue,
545        /// The tick when the upgrade was applied.
546        tick: u64,
547    },
548
549    /// A velocity command was submitted to an elevator running in
550    /// [`ServiceMode::Manual`](crate::components::ServiceMode::Manual).
551    ///
552    /// Emitted by
553    /// [`Simulation::set_target_velocity`](crate::sim::Simulation::set_target_velocity)
554    /// and [`Simulation::emergency_stop`](crate::sim::Simulation::emergency_stop).
555    ManualVelocityCommanded {
556        /// The elevator targeted by the command.
557        elevator: EntityId,
558        /// The new target velocity (clamped to `[-max_speed, max_speed]`),
559        /// or `None` when the command clears the target.
560        target_velocity: Option<ordered_float::OrderedFloat<f64>>,
561        /// The tick when the command was submitted.
562        tick: u64,
563    },
564
565    // -- Hall & car call events --
566    /// A hall button was pressed. Fires immediately, before any ack
567    /// latency delay. Games use this for button-light animations or SFX.
568    HallButtonPressed {
569        /// Stop where the press occurred.
570        stop: EntityId,
571        /// Direction the button requests service in.
572        direction: CallDirection,
573        /// Tick of the press.
574        tick: u64,
575    },
576    /// A hall call has elapsed its ack latency and is now visible to
577    /// dispatch. Useful for UI confirmations ("your call has been
578    /// received"). In groups with `ack_latency_ticks = 0` this fires on
579    /// the same tick as `HallButtonPressed`.
580    HallCallAcknowledged {
581        /// Stop the call was placed at.
582        stop: EntityId,
583        /// Direction of the call.
584        direction: CallDirection,
585        /// Tick acknowledgement completed.
586        tick: u64,
587    },
588    /// A hall call was cleared when an assigned car arrived at the stop
589    /// with matching direction indicators. Corresponds to the real-world
590    /// button-light turning off.
591    HallCallCleared {
592        /// Stop whose call was cleared.
593        stop: EntityId,
594        /// Direction of the cleared call.
595        direction: CallDirection,
596        /// Car that cleared the call by arriving.
597        car: EntityId,
598        /// Tick the call was cleared.
599        tick: u64,
600    },
601    /// A rider inside a car pressed a floor button (Classic mode only).
602    /// In Destination mode riders reveal destinations at the hall
603    /// kiosk, so no car buttons are pressed.
604    CarButtonPressed {
605        /// Elevator the button was pressed inside.
606        car: EntityId,
607        /// Floor the rider requested.
608        floor: EntityId,
609        /// Rider who pressed the button. `None` when the press is
610        /// synthetic — e.g. issued via
611        /// [`Simulation::press_car_button`](crate::sim::Simulation::press_car_button)
612        /// for scripted events, player input, or cutscene cues with no
613        /// associated rider entity.
614        rider: Option<EntityId>,
615        /// Opaque consumer tag attached to the pressing rider, mirroring
616        /// the optionality of [`rider`](Self::CarButtonPressed::rider).
617        /// `Some(0)` for a present-but-untagged rider; `None` for a
618        /// synthetic press with no rider entity.
619        #[serde(default)]
620        tag: Option<u64>,
621        /// Tick of the press.
622        tick: u64,
623    },
624    /// A rider skipped boarding a car they considered too crowded
625    /// (their preferences filtered the car out). The rider remains
626    /// Waiting and may board a later car.
627    #[serde(alias = "RiderBalked")]
628    RiderSkipped {
629        /// Rider who skipped.
630        rider: EntityId,
631        /// Elevator they declined to board.
632        elevator: EntityId,
633        /// Stop where the skip happened.
634        at_stop: EntityId,
635        /// Opaque consumer tag attached to the rider at emit time. `0`
636        /// when the rider is untagged.
637        #[serde(default)]
638        tag: u64,
639        /// Tick of the skip.
640        tick: u64,
641    },
642    /// A snapshot restore encountered an entity reference that could not
643    /// be remapped. The original (now-invalid) ID was kept. This signals
644    /// a corrupted or hand-edited snapshot.
645    SnapshotDanglingReference {
646        /// The entity ID from the snapshot that had no mapping.
647        stale_id: EntityId,
648        /// Tick from the snapshot at restore time.
649        tick: u64,
650    },
651    /// A snapshot restore could not re-instantiate the reposition strategy
652    /// for a group (e.g. custom strategy not registered). Idle elevator
653    /// positioning will not work for this group until the game calls
654    /// [`Simulation::set_reposition`](crate::sim::Simulation::set_reposition).
655    RepositionStrategyNotRestored {
656        /// The group whose strategy was lost.
657        group: GroupId,
658    },
659    /// A snapshot restore instantiated the dispatch strategy for a
660    /// group but couldn't replay its tunable configuration (the
661    /// serialized form in the snapshot didn't parse). The strategy
662    /// runs with its default weights — identical to a legacy snapshot
663    /// that never carried dispatch config at all.
664    DispatchConfigNotRestored {
665        /// The group whose dispatcher ran with defaults.
666        group: GroupId,
667        /// The parse error from
668        /// [`DispatchStrategy::restore_config`](crate::dispatch::DispatchStrategy::restore_config).
669        reason: String,
670    },
671    /// A stop was removed while resident riders were present.
672    /// The game must relocate or despawn these riders.
673    ResidentsAtRemovedStop {
674        /// The removed stop.
675        stop: EntityId,
676        /// Riders that were resident at the stop.
677        residents: Vec<EntityId>,
678    },
679}
680
681/// Discriminant for [`Event`] — the unit-variant projection of the
682/// payload-carrying enum.
683///
684/// Strategies, hosts, and FFI consumers use [`EventKind`] to filter
685/// events without pattern-matching against payload-laden variants.
686/// Filtering is the primary use case;
687/// [`Simulation::drain_events_by_kind`](crate::sim::Simulation::drain_events_by_kind)
688/// takes a `&[EventKind]` so closure-free filtering crosses the
689/// FFI/wasm/gdext boundary that closure-based
690/// [`drain_events_where`](crate::sim::Simulation::drain_events_where)
691/// can't.
692///
693/// One-to-one with [`Event`] variants. Mirrors the same `#[non_exhaustive]`
694/// and `#[cfg(feature = "energy")]` gates so hosts compiled without the
695/// `energy` feature don't see [`EventKind::EnergyConsumed`].
696#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
697#[non_exhaustive]
698pub enum EventKind {
699    /// See [`Event::ElevatorDeparted`].
700    ElevatorDeparted,
701    /// See [`Event::ElevatorArrived`].
702    ElevatorArrived,
703    /// See [`Event::DoorOpened`].
704    DoorOpened,
705    /// See [`Event::DoorClosed`].
706    DoorClosed,
707    /// See [`Event::PassingFloor`].
708    PassingFloor,
709    /// See [`Event::MovementAborted`].
710    MovementAborted,
711    /// See [`Event::RiderSpawned`].
712    RiderSpawned,
713    /// See [`Event::RiderBoarded`].
714    RiderBoarded,
715    /// See [`Event::RiderExited`].
716    RiderExited,
717    /// See [`Event::RiderRejected`].
718    RiderRejected,
719    /// See [`Event::RiderAbandoned`].
720    RiderAbandoned,
721    /// See [`Event::RiderEjected`].
722    RiderEjected,
723    /// See [`Event::ElevatorAssigned`].
724    ElevatorAssigned,
725    /// See [`Event::StopAdded`].
726    StopAdded,
727    /// See [`Event::ElevatorAdded`].
728    ElevatorAdded,
729    /// See [`Event::EntityDisabled`].
730    EntityDisabled,
731    /// See [`Event::EntityEnabled`].
732    EntityEnabled,
733    /// See [`Event::RouteInvalidated`].
734    RouteInvalidated,
735    /// See [`Event::RiderRerouted`].
736    RiderRerouted,
737    /// See [`Event::RiderSettled`].
738    RiderSettled,
739    /// See [`Event::RiderDespawned`].
740    RiderDespawned,
741    /// See [`Event::LineAdded`].
742    LineAdded,
743    /// See [`Event::LineRemoved`].
744    LineRemoved,
745    /// See [`Event::LineReassigned`].
746    LineReassigned,
747    /// See [`Event::ElevatorReassigned`].
748    ElevatorReassigned,
749    /// See [`Event::ElevatorRepositioning`].
750    ElevatorRepositioning,
751    /// See [`Event::ElevatorRepositioned`].
752    ElevatorRepositioned,
753    /// See [`Event::ElevatorRecalled`].
754    ElevatorRecalled,
755    /// See [`Event::ServiceModeChanged`].
756    ServiceModeChanged,
757    /// See [`Event::EnergyConsumed`].
758    #[cfg(feature = "energy")]
759    EnergyConsumed,
760    /// See [`Event::CapacityChanged`].
761    CapacityChanged,
762    /// See [`Event::ElevatorIdle`].
763    ElevatorIdle,
764    /// See [`Event::DirectionIndicatorChanged`].
765    DirectionIndicatorChanged,
766    /// See [`Event::ElevatorRemoved`].
767    ElevatorRemoved,
768    /// See [`Event::DestinationQueued`].
769    DestinationQueued,
770    /// See [`Event::StopRemoved`].
771    StopRemoved,
772    /// See [`Event::DoorCommandQueued`].
773    DoorCommandQueued,
774    /// See [`Event::DoorCommandApplied`].
775    DoorCommandApplied,
776    /// See [`Event::ElevatorUpgraded`].
777    ElevatorUpgraded,
778    /// See [`Event::ManualVelocityCommanded`].
779    ManualVelocityCommanded,
780    /// See [`Event::HallButtonPressed`].
781    HallButtonPressed,
782    /// See [`Event::HallCallAcknowledged`].
783    HallCallAcknowledged,
784    /// See [`Event::HallCallCleared`].
785    HallCallCleared,
786    /// See [`Event::CarButtonPressed`].
787    CarButtonPressed,
788    /// See [`Event::RiderSkipped`].
789    RiderSkipped,
790    /// See [`Event::SnapshotDanglingReference`].
791    SnapshotDanglingReference,
792    /// See [`Event::RepositionStrategyNotRestored`].
793    RepositionStrategyNotRestored,
794    /// See [`Event::DispatchConfigNotRestored`].
795    DispatchConfigNotRestored,
796    /// See [`Event::ResidentsAtRemovedStop`].
797    ResidentsAtRemovedStop,
798}
799
800impl Event {
801    /// Project this [`Event`] to its [`EventKind`] discriminant.
802    ///
803    /// ```
804    /// # use elevator_core::events::{Event, EventKind};
805    /// # use elevator_core::entity::EntityId;
806    /// let e = Event::DoorOpened {
807    ///     elevator: EntityId::default(),
808    ///     tick: 0,
809    /// };
810    /// assert_eq!(e.kind(), EventKind::DoorOpened);
811    /// ```
812    #[must_use]
813    pub const fn kind(&self) -> EventKind {
814        match self {
815            Self::ElevatorDeparted { .. } => EventKind::ElevatorDeparted,
816            Self::ElevatorArrived { .. } => EventKind::ElevatorArrived,
817            Self::DoorOpened { .. } => EventKind::DoorOpened,
818            Self::DoorClosed { .. } => EventKind::DoorClosed,
819            Self::PassingFloor { .. } => EventKind::PassingFloor,
820            Self::MovementAborted { .. } => EventKind::MovementAborted,
821            Self::RiderSpawned { .. } => EventKind::RiderSpawned,
822            Self::RiderBoarded { .. } => EventKind::RiderBoarded,
823            Self::RiderExited { .. } => EventKind::RiderExited,
824            Self::RiderRejected { .. } => EventKind::RiderRejected,
825            Self::RiderAbandoned { .. } => EventKind::RiderAbandoned,
826            Self::RiderEjected { .. } => EventKind::RiderEjected,
827            Self::ElevatorAssigned { .. } => EventKind::ElevatorAssigned,
828            Self::StopAdded { .. } => EventKind::StopAdded,
829            Self::ElevatorAdded { .. } => EventKind::ElevatorAdded,
830            Self::EntityDisabled { .. } => EventKind::EntityDisabled,
831            Self::EntityEnabled { .. } => EventKind::EntityEnabled,
832            Self::RouteInvalidated { .. } => EventKind::RouteInvalidated,
833            Self::RiderRerouted { .. } => EventKind::RiderRerouted,
834            Self::RiderSettled { .. } => EventKind::RiderSettled,
835            Self::RiderDespawned { .. } => EventKind::RiderDespawned,
836            Self::LineAdded { .. } => EventKind::LineAdded,
837            Self::LineRemoved { .. } => EventKind::LineRemoved,
838            Self::LineReassigned { .. } => EventKind::LineReassigned,
839            Self::ElevatorReassigned { .. } => EventKind::ElevatorReassigned,
840            Self::ElevatorRepositioning { .. } => EventKind::ElevatorRepositioning,
841            Self::ElevatorRepositioned { .. } => EventKind::ElevatorRepositioned,
842            Self::ElevatorRecalled { .. } => EventKind::ElevatorRecalled,
843            Self::ServiceModeChanged { .. } => EventKind::ServiceModeChanged,
844            #[cfg(feature = "energy")]
845            Self::EnergyConsumed { .. } => EventKind::EnergyConsumed,
846            Self::CapacityChanged { .. } => EventKind::CapacityChanged,
847            Self::ElevatorIdle { .. } => EventKind::ElevatorIdle,
848            Self::DirectionIndicatorChanged { .. } => EventKind::DirectionIndicatorChanged,
849            Self::ElevatorRemoved { .. } => EventKind::ElevatorRemoved,
850            Self::DestinationQueued { .. } => EventKind::DestinationQueued,
851            Self::StopRemoved { .. } => EventKind::StopRemoved,
852            Self::DoorCommandQueued { .. } => EventKind::DoorCommandQueued,
853            Self::DoorCommandApplied { .. } => EventKind::DoorCommandApplied,
854            Self::ElevatorUpgraded { .. } => EventKind::ElevatorUpgraded,
855            Self::ManualVelocityCommanded { .. } => EventKind::ManualVelocityCommanded,
856            Self::HallButtonPressed { .. } => EventKind::HallButtonPressed,
857            Self::HallCallAcknowledged { .. } => EventKind::HallCallAcknowledged,
858            Self::HallCallCleared { .. } => EventKind::HallCallCleared,
859            Self::CarButtonPressed { .. } => EventKind::CarButtonPressed,
860            Self::RiderSkipped { .. } => EventKind::RiderSkipped,
861            Self::SnapshotDanglingReference { .. } => EventKind::SnapshotDanglingReference,
862            Self::RepositionStrategyNotRestored { .. } => EventKind::RepositionStrategyNotRestored,
863            Self::DispatchConfigNotRestored { .. } => EventKind::DispatchConfigNotRestored,
864            Self::ResidentsAtRemovedStop { .. } => EventKind::ResidentsAtRemovedStop,
865        }
866    }
867
868    /// Whether this event references the given entity in any of its
869    /// payload fields.
870    ///
871    /// Used by
872    /// [`Simulation::drain_events_for_entity`](crate::sim::Simulation::drain_events_for_entity)
873    /// to provide a closure-free filter that crosses the FFI / wasm /
874    /// gdext boundary. Returns `true` when `id` matches any [`EntityId`]
875    /// field on the variant — for example,
876    /// [`Event::RiderBoarded`] references both `rider` and `elevator`,
877    /// so a query for the elevator and a query for the rider both match
878    /// the same event.
879    ///
880    /// Variants whose only entity-shaped reference is a [`GroupId`]
881    /// (e.g. [`Event::RepositionStrategyNotRestored`]) never match —
882    /// [`GroupId`] is a separate identifier space from [`EntityId`].
883    ///
884    /// ```
885    /// # use elevator_core::events::Event;
886    /// # use elevator_core::entity::EntityId;
887    /// let car = EntityId::default();
888    /// let evt = Event::DoorOpened { elevator: car, tick: 0 };
889    /// assert!(evt.involves(car));
890    /// ```
891    #[must_use]
892    #[allow(clippy::too_many_lines)]
893    pub fn involves(&self, id: EntityId) -> bool {
894        match self {
895            Self::ElevatorDeparted {
896                elevator,
897                from_stop,
898                ..
899            } => *elevator == id || *from_stop == id,
900            Self::ElevatorArrived {
901                elevator, at_stop, ..
902            } => *elevator == id || *at_stop == id,
903            Self::DoorOpened { elevator, .. } | Self::DoorClosed { elevator, .. } => {
904                *elevator == id
905            }
906            Self::PassingFloor { elevator, stop, .. } => *elevator == id || *stop == id,
907            Self::MovementAborted {
908                elevator,
909                brake_target,
910                ..
911            } => *elevator == id || *brake_target == id,
912            Self::RiderSpawned {
913                rider,
914                origin,
915                destination,
916                ..
917            } => *rider == id || *origin == id || *destination == id,
918            Self::RiderBoarded {
919                rider, elevator, ..
920            } => *rider == id || *elevator == id,
921            Self::RiderExited {
922                rider,
923                elevator,
924                stop,
925                ..
926            } => *rider == id || *elevator == id || *stop == id,
927            Self::RiderRejected {
928                rider, elevator, ..
929            } => *rider == id || *elevator == id,
930            Self::RiderAbandoned { rider, stop, .. } => *rider == id || *stop == id,
931            Self::RiderEjected {
932                rider,
933                elevator,
934                stop,
935                ..
936            } => *rider == id || *elevator == id || *stop == id,
937            Self::ElevatorAssigned { elevator, stop, .. } => *elevator == id || *stop == id,
938            Self::StopAdded { stop, line, .. } => *stop == id || *line == id,
939            Self::ElevatorAdded { elevator, line, .. } => *elevator == id || *line == id,
940            Self::EntityDisabled { entity, .. } | Self::EntityEnabled { entity, .. } => {
941                *entity == id
942            }
943            Self::RouteInvalidated {
944                rider,
945                affected_stop,
946                ..
947            } => *rider == id || *affected_stop == id,
948            Self::RiderRerouted {
949                rider,
950                new_destination,
951                ..
952            } => *rider == id || *new_destination == id,
953            Self::RiderSettled { rider, stop, .. } => *rider == id || *stop == id,
954            Self::RiderDespawned { rider, .. } => *rider == id,
955            Self::LineAdded { line, .. } | Self::LineRemoved { line, .. } => *line == id,
956            Self::LineReassigned { line, .. } => *line == id,
957            Self::ElevatorReassigned {
958                elevator,
959                old_line,
960                new_line,
961                ..
962            } => *elevator == id || *old_line == id || *new_line == id,
963            Self::ElevatorRepositioning {
964                elevator, to_stop, ..
965            } => *elevator == id || *to_stop == id,
966            Self::ElevatorRepositioned {
967                elevator, at_stop, ..
968            } => *elevator == id || *at_stop == id,
969            Self::ElevatorRecalled {
970                elevator, to_stop, ..
971            } => *elevator == id || *to_stop == id,
972            Self::ServiceModeChanged { elevator, .. } => *elevator == id,
973            #[cfg(feature = "energy")]
974            Self::EnergyConsumed { elevator, .. } => *elevator == id,
975            Self::CapacityChanged { elevator, .. } => *elevator == id,
976            Self::ElevatorIdle {
977                elevator, at_stop, ..
978            } => *elevator == id || at_stop.is_some_and(|s| s == id),
979            Self::DirectionIndicatorChanged { elevator, .. } => *elevator == id,
980            Self::ElevatorRemoved { elevator, line, .. } => *elevator == id || *line == id,
981            Self::DestinationQueued { elevator, stop, .. } => *elevator == id || *stop == id,
982            Self::StopRemoved { stop, .. } => *stop == id,
983            Self::DoorCommandQueued { elevator, .. }
984            | Self::DoorCommandApplied { elevator, .. } => *elevator == id,
985            Self::ElevatorUpgraded { elevator, .. } => *elevator == id,
986            Self::ManualVelocityCommanded { elevator, .. } => *elevator == id,
987            Self::HallButtonPressed { stop, .. } | Self::HallCallAcknowledged { stop, .. } => {
988                *stop == id
989            }
990            Self::HallCallCleared { stop, car, .. } => *stop == id || *car == id,
991            Self::CarButtonPressed {
992                car, floor, rider, ..
993            } => *car == id || *floor == id || rider.is_some_and(|r| r == id),
994            Self::RiderSkipped {
995                rider,
996                elevator,
997                at_stop,
998                ..
999            } => *rider == id || *elevator == id || *at_stop == id,
1000            Self::SnapshotDanglingReference { stale_id, .. } => *stale_id == id,
1001            // GroupId-only payloads have no EntityId to match on.
1002            Self::RepositionStrategyNotRestored { .. } | Self::DispatchConfigNotRestored { .. } => {
1003                false
1004            }
1005            Self::ResidentsAtRemovedStop {
1006                stop, residents, ..
1007            } => *stop == id || residents.contains(&id),
1008        }
1009    }
1010}
1011
1012/// Identifies which elevator parameter was changed in an
1013/// [`Event::ElevatorUpgraded`].
1014#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1015#[non_exhaustive]
1016pub enum UpgradeField {
1017    /// Maximum travel speed (distance/tick).
1018    MaxSpeed,
1019    /// Acceleration rate (distance/tick^2).
1020    Acceleration,
1021    /// Deceleration rate (distance/tick^2).
1022    Deceleration,
1023    /// Maximum weight the car can carry.
1024    WeightCapacity,
1025    /// Ticks for a door open/close transition.
1026    DoorTransitionTicks,
1027    /// Ticks the door stays fully open.
1028    DoorOpenTicks,
1029}
1030
1031impl std::fmt::Display for UpgradeField {
1032    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1033        match self {
1034            Self::MaxSpeed => write!(f, "max_speed"),
1035            Self::Acceleration => write!(f, "acceleration"),
1036            Self::Deceleration => write!(f, "deceleration"),
1037            Self::WeightCapacity => write!(f, "weight_capacity"),
1038            Self::DoorTransitionTicks => write!(f, "door_transition_ticks"),
1039            Self::DoorOpenTicks => write!(f, "door_open_ticks"),
1040        }
1041    }
1042}
1043
1044/// Old-or-new value carried by [`Event::ElevatorUpgraded`].
1045///
1046/// Uses [`OrderedFloat`] for the float variant so the event enum
1047/// remains `Eq`-comparable alongside the other observability events.
1048#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1049#[non_exhaustive]
1050pub enum UpgradeValue {
1051    /// A floating-point parameter value (speed, accel, decel, capacity).
1052    Float(OrderedFloat<f64>),
1053    /// An integral tick-count parameter value (door timings).
1054    Ticks(u32),
1055}
1056
1057impl UpgradeValue {
1058    /// Construct a float-valued upgrade payload.
1059    #[must_use]
1060    pub const fn float(v: f64) -> Self {
1061        Self::Float(OrderedFloat(v))
1062    }
1063
1064    /// Construct a tick-valued upgrade payload.
1065    #[must_use]
1066    pub const fn ticks(v: u32) -> Self {
1067        Self::Ticks(v)
1068    }
1069}
1070
1071impl std::fmt::Display for UpgradeValue {
1072    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1073        match self {
1074            Self::Float(v) => write!(f, "{}", **v),
1075            Self::Ticks(v) => write!(f, "{v}"),
1076        }
1077    }
1078}
1079
1080/// Reason a rider's route was invalidated.
1081#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1082#[non_exhaustive]
1083pub enum RouteInvalidReason {
1084    /// A stop on the route was disabled (transient — the stop entity
1085    /// still exists).
1086    StopDisabled,
1087    /// Reserved. No code currently emits this — the "no alternative
1088    /// found" signal is conveyed by the accompanying `RiderAbandoned`
1089    /// event so this enum stays dedicated to *why* the route was
1090    /// invalidated rather than *what the rerouter could do about it*.
1091    NoAlternative,
1092    /// A stop on the route was permanently removed (despawned).
1093    /// Distinguishes the removal path from a transient disable.
1094    StopRemoved,
1095}
1096
1097/// Coarse-grained classification of an [`Event`].
1098///
1099/// Exposes the same grouping that the `Event` variants are already
1100/// commented under, so consumers can filter a drained event stream with
1101/// one match arm per category rather than enumerating ~25 variants.
1102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1103#[non_exhaustive]
1104pub enum EventCategory {
1105    /// Elevator motion, arrival/departure, and door state.
1106    Elevator,
1107    /// Rider lifecycle: spawn, board, exit, reject, abandon, despawn, settle, reroute.
1108    Rider,
1109    /// Dispatch decisions and bookkeeping.
1110    Dispatch,
1111    /// Runtime topology mutations (entities/lines/groups added, removed, reassigned).
1112    Topology,
1113    /// Idle-elevator repositioning activity.
1114    Reposition,
1115    /// Direction indicator lamp state changes.
1116    Direction,
1117    /// Observability and misc signals (capacity, service mode, energy, etc.).
1118    Observability,
1119}
1120
1121impl Event {
1122    /// Classify this event into a coarse-grained [`EventCategory`].
1123    ///
1124    /// Useful when a consumer only cares about, say, rider activity and
1125    /// wants to skip elevator-motion and topology chatter without
1126    /// enumerating every variant. The exhaustive match inside guarantees
1127    /// the method stays in sync when new variants are added.
1128    #[must_use]
1129    pub const fn category(&self) -> EventCategory {
1130        match self {
1131            Self::ElevatorDeparted { .. }
1132            | Self::ElevatorArrived { .. }
1133            | Self::DoorOpened { .. }
1134            | Self::DoorClosed { .. }
1135            | Self::DoorCommandQueued { .. }
1136            | Self::DoorCommandApplied { .. }
1137            | Self::PassingFloor { .. }
1138            | Self::MovementAborted { .. }
1139            | Self::ElevatorIdle { .. } => EventCategory::Elevator,
1140            Self::RiderSpawned { .. }
1141            | Self::RiderBoarded { .. }
1142            | Self::RiderExited { .. }
1143            | Self::RiderRejected { .. }
1144            | Self::RiderAbandoned { .. }
1145            | Self::RiderEjected { .. }
1146            | Self::RouteInvalidated { .. }
1147            | Self::RiderRerouted { .. }
1148            | Self::RiderSettled { .. }
1149            | Self::RiderDespawned { .. } => EventCategory::Rider,
1150            Self::ElevatorAssigned { .. } | Self::DestinationQueued { .. } => {
1151                EventCategory::Dispatch
1152            }
1153            Self::StopAdded { .. }
1154            | Self::StopRemoved { .. }
1155            | Self::ElevatorAdded { .. }
1156            | Self::ElevatorRemoved { .. }
1157            | Self::EntityDisabled { .. }
1158            | Self::EntityEnabled { .. }
1159            | Self::LineAdded { .. }
1160            | Self::LineRemoved { .. }
1161            | Self::LineReassigned { .. }
1162            | Self::ElevatorReassigned { .. } => EventCategory::Topology,
1163            Self::ElevatorRepositioning { .. } | Self::ElevatorRepositioned { .. } => {
1164                EventCategory::Reposition
1165            }
1166            Self::ElevatorRecalled { .. } => EventCategory::Elevator,
1167            Self::DirectionIndicatorChanged { .. } => EventCategory::Direction,
1168            Self::ServiceModeChanged { .. }
1169            | Self::CapacityChanged { .. }
1170            | Self::ElevatorUpgraded { .. }
1171            | Self::ManualVelocityCommanded { .. } => EventCategory::Observability,
1172            #[cfg(feature = "energy")]
1173            Self::EnergyConsumed { .. } => EventCategory::Observability,
1174            Self::HallButtonPressed { .. }
1175            | Self::HallCallAcknowledged { .. }
1176            | Self::HallCallCleared { .. }
1177            | Self::CarButtonPressed { .. } => EventCategory::Dispatch,
1178            Self::RiderSkipped { .. } => EventCategory::Rider,
1179            Self::SnapshotDanglingReference { .. }
1180            | Self::RepositionStrategyNotRestored { .. }
1181            | Self::DispatchConfigNotRestored { .. } => EventCategory::Observability,
1182            Self::ResidentsAtRemovedStop { .. } => EventCategory::Topology,
1183        }
1184    }
1185}
1186
1187/// Collects simulation events for consumers to drain.
1188#[derive(Debug, Default)]
1189pub struct EventBus {
1190    /// The pending events not yet consumed.
1191    events: Vec<Event>,
1192}
1193
1194impl EventBus {
1195    /// Pushes a new event onto the bus.
1196    pub fn emit(&mut self, event: Event) {
1197        self.events.push(event);
1198    }
1199
1200    /// Returns and clears all pending events.
1201    pub fn drain(&mut self) -> Vec<Event> {
1202        std::mem::take(&mut self.events)
1203    }
1204
1205    /// Returns a slice of all pending events without clearing them.
1206    #[must_use]
1207    pub fn peek(&self) -> &[Event] {
1208        &self.events
1209    }
1210}
1211
1212/// A typed event channel for game-specific events.
1213///
1214/// Games insert this as a global resource on `World`:
1215///
1216/// ```
1217/// use elevator_core::world::World;
1218/// use elevator_core::events::EventChannel;
1219///
1220/// #[derive(Debug)]
1221/// enum MyGameEvent { Foo, Bar }
1222///
1223/// let mut world = World::new();
1224/// world.insert_resource(EventChannel::<MyGameEvent>::new());
1225/// // Later:
1226/// world.resource_mut::<EventChannel<MyGameEvent>>().unwrap().emit(MyGameEvent::Foo);
1227/// ```
1228#[derive(Debug)]
1229pub struct EventChannel<T> {
1230    /// Pending events not yet consumed.
1231    events: Vec<T>,
1232}
1233
1234impl<T> EventChannel<T> {
1235    /// Create an empty event channel.
1236    #[must_use]
1237    pub const fn new() -> Self {
1238        Self { events: Vec::new() }
1239    }
1240
1241    /// Emit an event into the channel.
1242    pub fn emit(&mut self, event: T) {
1243        self.events.push(event);
1244    }
1245
1246    /// Drain and return all pending events.
1247    pub fn drain(&mut self) -> Vec<T> {
1248        std::mem::take(&mut self.events)
1249    }
1250
1251    /// Peek at pending events without clearing.
1252    #[must_use]
1253    pub fn peek(&self) -> &[T] {
1254        &self.events
1255    }
1256
1257    /// Check if the channel has no pending events.
1258    #[must_use]
1259    pub const fn is_empty(&self) -> bool {
1260        self.events.is_empty()
1261    }
1262
1263    /// Number of pending events.
1264    #[must_use]
1265    pub const fn len(&self) -> usize {
1266        self.events.len()
1267    }
1268}
1269
1270impl<T> Default for EventChannel<T> {
1271    fn default() -> Self {
1272        Self::new()
1273    }
1274}
1275
1276/// Helpers for surfacing simulation events as log-style records.
1277///
1278/// The FFI host exposes a "log drain" API ([`ev_drain_log_messages`])
1279/// that polling consumers (e.g. `GameMaker`) call to surface
1280/// debug-formatted strings for events emitted during the most recent
1281/// step. Other binding crates wrap the same primitive via this
1282/// module so every host produces the same message text and severity
1283/// for a given `Event`.
1284///
1285/// [`ev_drain_log_messages`]: https://docs.rs/elevator-ffi/latest/elevator_ffi/fn.ev_drain_log_messages.html
1286pub mod log_format {
1287    use super::Event;
1288
1289    /// `trace`-level severity (`0`).
1290    ///
1291    /// The full set (`LEVEL_TRACE` … `LEVEL_ERROR`) is the shared
1292    /// vocabulary for cross-host log surfaces — see the repository's
1293    /// `docs/src/host-binding-parity.md` for the parity contract.
1294    pub const LEVEL_TRACE: u8 = 0;
1295    /// `debug`-level severity (`1`). The default for events surfaced
1296    /// via [`format_event`].
1297    pub const LEVEL_DEBUG: u8 = 1;
1298    /// `info`-level severity (`2`).
1299    pub const LEVEL_INFO: u8 = 2;
1300    /// `warn`-level severity (`3`).
1301    pub const LEVEL_WARN: u8 = 3;
1302    /// `error`-level severity (`4`).
1303    pub const LEVEL_ERROR: u8 = 4;
1304
1305    /// Format an event for log-style consumption. Returns
1306    /// `(level, message)` where `message` is the `Debug` rendering
1307    /// of the event and `level` is currently always
1308    /// [`LEVEL_DEBUG`].
1309    ///
1310    /// Hosts wrap this in their idiomatic surface — e.g. a
1311    /// `Vec<JsValue>` for wasm, a Godot `Array` of dictionaries,
1312    /// or the FFI's borrowed `EvLogMessage` struct.
1313    #[must_use]
1314    pub fn format_event(event: &Event) -> (u8, String) {
1315        (LEVEL_DEBUG, format!("{event:?}"))
1316    }
1317}