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}