Skip to main content

subtr_actor/stats/calculators/
event_definition.rs

1#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
2
3use serde::Serialize;
4use ts_rs::TS;
5
6#[cfg(not(target_arch = "wasm32"))]
7use linkme::distributed_slice;
8
9use super::{
10    BackboardBounceEvent, BallCarryEvent, BallDepthEvent, BallHalfEvent, BallProximityEvent,
11    BallThirdEvent, BoostPickupEvent, BumpEvent, CeilingShotEvent, CenterEvent,
12    ControlledPlayEvent, CorePlayerScoreboardEvent, DemolitionEvent, DepthRoleEvent, DodgeEvent,
13    DodgeResetEvent, DoubleTapEvent, FieldHalfEvent, FieldThirdEvent, FiftyFiftyEvent,
14    FirstManChangeEvent, FlickEvent, FlipResetEvent, HalfFlipEvent, HalfVolleyEvent,
15    LoosePossessionEvent, MovementEvent, OneTimerEvent, PassEvent, PlayerActivityEvent,
16    PlayerPossessionEvent, PossessionEvent, PowerslideEvent, RespawnEvent, RotationRoleEvent,
17    RushEvent, SpeedFlipEvent, TerritorialPressureEvent, TimelineEvent, TouchClassificationEvent,
18    WallAerialEvent, WallAerialShotEvent, WavedashEvent, WhiffEvent,
19};
20use crate::stats::timeline::{Event, EventPayload, EventScope};
21
22/// Static, English-language metadata for a stat event type.
23///
24/// Event structs own this definition through [`StatsEvent`]. Analysis nodes
25/// then link event definitions to the calculator code that produces them via
26/// [`EmittedEvent`].
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
28pub struct EventDefinition {
29    pub id: &'static str,
30    pub label: &'static str,
31    pub category: EventCategory,
32    /// How a timeline client fans this event's stream out into lanes. `Match`
33    /// (the default) keeps everything on one shared row; `Team`/`Player` are the
34    /// opt-in exceptions that split into one lane per team or per player. Declared
35    /// here so fan-out travels with the event type rather than a side table; the
36    /// shipped `EventMeta.scope` reads from this via [`EventPayload::scope`].
37    pub scope: EventScope,
38    pub confidence: DetectionConfidence,
39    pub summary: &'static str,
40    pub approach: &'static [&'static str],
41    pub limitations: &'static [&'static str],
42    /// When true this definition is a label-like or expansion-parent row that
43    /// should not be offered as a selectable event type in the review UI.
44    pub hidden_from_review: bool,
45    /// Concrete event-type keys this definition expands into at serialization
46    /// time (e.g. `boost_ledger` -> `boost_ledger_collected`). Expansion parents
47    /// are typically also `hidden_from_review`; their variants are surfaced
48    /// instead. Empty for ordinary events.
49    pub variants: &'static [EventVariant],
50}
51
52impl EventDefinition {
53    /// Set whether this definition is hidden from the review picker. Named to
54    /// double as a `define_stats_event!` modifier (`hidden = true`).
55    pub const fn hidden(self, hidden: bool) -> Self {
56        let mut def = self;
57        def.hidden_from_review = hidden;
58        def
59    }
60
61    /// Attach the concrete variant keys this definition expands into. Named to
62    /// double as a `define_stats_event!` modifier (`variants = SLICE`).
63    pub const fn variants(self, variants: &'static [EventVariant]) -> Self {
64        let mut def = self;
65        def.variants = variants;
66        def
67    }
68
69    /// Set how this event's stream fans out into timeline lanes. Named to double
70    /// as a `define_stats_event!` modifier (`scope = EventScope::Player`). The
71    /// default is [`EventScope::Match`] (single shared row); declare `Player` or
72    /// `Team` to opt a stream into per-entity lanes.
73    pub const fn scope(self, scope: EventScope) -> Self {
74        let mut def = self;
75        def.scope = scope;
76        def
77    }
78}
79
80impl EventPayload {
81    /// The lane fan-out scope for this event, taken from the payload type's
82    /// declared [`EventDefinition::scope`]. This is the single authoritative
83    /// source for the `EventMeta.scope` shipped on every timeline event, so the
84    /// `scope =` declared next to each `define_stats_event!` is what reaches the
85    /// client. The match is exhaustive on purpose: a newly added payload variant
86    /// will not compile until its scope is declared here.
87    pub fn scope(&self) -> EventScope {
88        match self {
89            Self::Timeline(_) => TimelineEvent::DEFINITION.scope,
90            Self::CorePlayer(_) => CorePlayerScoreboardEvent::DEFINITION.scope,
91            Self::Possession(_) => PossessionEvent::DEFINITION.scope,
92            Self::LoosePossession(_) => LoosePossessionEvent::DEFINITION.scope,
93            Self::PlayerPossession(_) => PlayerPossessionEvent::DEFINITION.scope,
94            Self::BallHalf(_) => BallHalfEvent::DEFINITION.scope,
95            Self::BallThird(_) => BallThirdEvent::DEFINITION.scope,
96            Self::TerritorialPressure(_) => TerritorialPressureEvent::DEFINITION.scope,
97            Self::Movement(_) => MovementEvent::DEFINITION.scope,
98            Self::PlayerActivity(_) => PlayerActivityEvent::DEFINITION.scope,
99            Self::FieldThird(_) => FieldThirdEvent::DEFINITION.scope,
100            Self::FieldHalf(_) => FieldHalfEvent::DEFINITION.scope,
101            Self::BallDepth(_) => BallDepthEvent::DEFINITION.scope,
102            Self::DepthRole(_) => DepthRoleEvent::DEFINITION.scope,
103            Self::BallProximity(_) => BallProximityEvent::DEFINITION.scope,
104            // ShadowDefenseEvent has no `define_stats_event!` definition; it is a
105            // per-player positioning span like its sibling positioning streams.
106            Self::ShadowDefense(_) => EventScope::Player,
107            Self::RotationRole(_) => RotationRoleEvent::DEFINITION.scope,
108            Self::FirstManChange(_) => FirstManChangeEvent::DEFINITION.scope,
109            Self::GoalContext(_) => GOAL_CONTEXT_EVENT_DEFINITION.scope,
110            Self::Backboard(_) => BackboardBounceEvent::DEFINITION.scope,
111            Self::CeilingShot(_) => CeilingShotEvent::DEFINITION.scope,
112            Self::WallAerial(_) => WallAerialEvent::DEFINITION.scope,
113            Self::WallAerialShot(_) => WallAerialShotEvent::DEFINITION.scope,
114            Self::Center(_) => CenterEvent::DEFINITION.scope,
115            Self::Flick(_) => FlickEvent::DEFINITION.scope,
116            Self::DodgeReset(_) => DodgeResetEvent::DEFINITION.scope,
117            Self::FlipReset(_) => FlipResetEvent::DEFINITION.scope,
118            Self::DoubleTap(_) => DoubleTapEvent::DEFINITION.scope,
119            Self::FiftyFifty(_) => FiftyFiftyEvent::DEFINITION.scope,
120            Self::Kickoff(_) => KICKOFF_EVENT_DEFINITION.scope,
121            Self::OneTimer(_) => OneTimerEvent::DEFINITION.scope,
122            Self::Pass(_) => PassEvent::DEFINITION.scope,
123            Self::BallCarry(_) => BallCarryEvent::DEFINITION.scope,
124            Self::ControlledPlay(_) => ControlledPlayEvent::DEFINITION.scope,
125            Self::Rush(_) => RushEvent::DEFINITION.scope,
126            Self::Dodge(_) => DodgeEvent::DEFINITION.scope,
127            Self::SpeedFlip(_) => SpeedFlipEvent::DEFINITION.scope,
128            Self::HalfFlip(_) => HalfFlipEvent::DEFINITION.scope,
129            Self::HalfVolley(_) => HalfVolleyEvent::DEFINITION.scope,
130            Self::Wavedash(_) => WavedashEvent::DEFINITION.scope,
131            Self::Whiff(_) => WhiffEvent::DEFINITION.scope,
132            Self::Powerslide(_) => PowerslideEvent::DEFINITION.scope,
133            Self::Touch(_) => TouchClassificationEvent::DEFINITION.scope,
134            Self::BoostPickup(_) => BoostPickupEvent::DEFINITION.scope,
135            Self::Respawn(_) => RespawnEvent::DEFINITION.scope,
136            Self::Bump(_) => BumpEvent::DEFINITION.scope,
137            Self::Demolition(_) => DemolitionEvent::DEFINITION.scope,
138        }
139    }
140}
141
142/// A concrete event-type key produced by expanding a parent [`EventDefinition`]
143/// (for example a boost-ledger transaction or a rotation role/depth state).
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
145pub struct EventVariant {
146    pub key: &'static str,
147    pub label: &'static str,
148    pub category: EventCategory,
149}
150
151impl EventVariant {
152    pub const fn new(key: &'static str, label: &'static str, category: EventCategory) -> Self {
153        Self {
154            key,
155            label,
156            category,
157        }
158    }
159}
160
161/// Coarse product/domain grouping for an event definition.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)]
163#[ts(export)]
164#[serde(rename_all = "snake_case")]
165pub enum EventCategory {
166    Core,
167    Basic,
168    Mechanic,
169    Positioning,
170    Annotation,
171    Other,
172    /// Label-like metadata rows (e.g. goal context). These are hidden from the
173    /// review picker by default via [`EventDefinition::hidden_from_review`].
174    Context,
175}
176
177/// Multi-dimensional confidence metadata for an event detector.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
179pub struct DetectionConfidence {
180    pub approach: ApproachConfidenceLevel,
181    pub true_positive_evidence: TruePositiveEvidenceLevel,
182    pub false_positive_evidence: DetectionIssueEvidenceLevel,
183    pub false_negative_evidence: DetectionIssueEvidenceLevel,
184    pub testing: TestingThoroughnessLevel,
185    pub known_issues: &'static [KnownIssueRef],
186}
187
188/// How plausible and stable the current detector approach is by design.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
190#[serde(rename_all = "snake_case")]
191pub enum ApproachConfidenceLevel {
192    Unknown,
193    High,
194    Medium,
195    Low,
196    Experimental,
197}
198
199/// Whether the detector is known to produce correct detections.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
201#[serde(rename_all = "snake_case")]
202pub enum TruePositiveEvidenceLevel {
203    NotEvaluated,
204    Plausible,
205    ManuallyConfirmed,
206    AutomatedTestCovered,
207    RepeatedlyConfirmed,
208}
209
210/// Whether the detector is known to produce incorrect detections or misses.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
212#[serde(rename_all = "snake_case")]
213pub enum DetectionIssueEvidenceLevel {
214    NotEvaluated,
215    NoneKnown,
216    Suspected,
217    Observed,
218}
219
220/// Rough level of testing behind the detector definition.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
222#[serde(rename_all = "snake_case")]
223pub enum TestingThoroughnessLevel {
224    Untested,
225    ManualSpotCheck,
226    TargetedAutomatedTest,
227    MultipleTargetedTests,
228    CuratedSuite,
229    CorpusSample,
230}
231
232/// Lightweight reference to a known detector issue.
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
234pub struct KnownIssueRef {
235    pub id: &'static str,
236    pub summary: &'static str,
237    pub url: Option<&'static str>,
238}
239
240pub const UNKNOWN_DETECTION_CONFIDENCE: DetectionConfidence = DetectionConfidence {
241    approach: ApproachConfidenceLevel::Unknown,
242    true_positive_evidence: TruePositiveEvidenceLevel::NotEvaluated,
243    false_positive_evidence: DetectionIssueEvidenceLevel::NotEvaluated,
244    false_negative_evidence: DetectionIssueEvidenceLevel::NotEvaluated,
245    testing: TestingThoroughnessLevel::Untested,
246    known_issues: &[],
247};
248
249pub const fn pending_event_definition(
250    id: &'static str,
251    label: &'static str,
252    category: EventCategory,
253) -> EventDefinition {
254    event_definition(id, label, category, "Definition pending.", &[])
255}
256
257pub const fn event_definition(
258    id: &'static str,
259    label: &'static str,
260    category: EventCategory,
261    summary: &'static str,
262    approach: &'static [&'static str],
263) -> EventDefinition {
264    EventDefinition {
265        id,
266        label,
267        category,
268        // Fan-out is opt-in: streams stay on one shared row unless a definition
269        // declares `Player`/`Team` via the `scope =` modifier.
270        scope: EventScope::Match,
271        confidence: UNKNOWN_DETECTION_CONFIDENCE,
272        summary,
273        approach,
274        limitations: &[],
275        hidden_from_review: false,
276        variants: &[],
277    }
278}
279
280pub const fn produced_event(
281    event: &'static EventDefinition,
282    node_name: &'static str,
283    node_type: &'static str,
284    calculator_type: &'static str,
285) -> EmittedEvent {
286    EmittedEvent {
287        event,
288        producer: ProducerDefinition {
289            node_name,
290            node_type,
291            calculator_type,
292            implementation_notes: &[],
293        },
294    }
295}
296
297pub const fn produced_event_for<E: StatsEvent>(
298    node_name: &'static str,
299    node_type: &'static str,
300    calculator_type: &'static str,
301) -> EmittedEvent {
302    produced_event(&E::DEFINITION, node_name, node_type, calculator_type)
303}
304
305/// Trait implemented by typed stat event payloads.
306pub trait StatsEvent {
307    const DEFINITION: EventDefinition;
308}
309
310/// Static metadata for the analysis node and calculator that produce an event.
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
312pub struct ProducerDefinition {
313    pub node_name: &'static str,
314    pub node_type: &'static str,
315    pub calculator_type: &'static str,
316    pub implementation_notes: &'static [&'static str],
317}
318
319/// Link between an event definition and the graph node that emits it.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
321pub struct EmittedEvent {
322    pub event: &'static EventDefinition,
323    pub producer: ProducerDefinition,
324}
325
326/// Distributed catalog of every [`EventDefinition`].
327///
328/// `define_stats_event!` (and `register_event_definition!` for payload-less
329/// rows) register into this slice automatically, so defining an event is the
330/// only step required for it to appear everywhere definitions are consumed —
331/// there is no separate central list to keep in sync. Read it through
332/// [`all_event_definitions`], which sorts and de-duplicates by `id`.
333#[cfg(not(target_arch = "wasm32"))]
334#[distributed_slice]
335pub static EVENT_DEFINITIONS: [EventDefinition];
336
337/// All registered event definitions, sorted by `id` and de-duplicated.
338///
339/// `linkme` does not guarantee registration order, so this sorts for stable
340/// output and panics if two registrations share an `id` but disagree on
341/// contents (a real double-registration bug rather than something to hide).
342#[cfg(not(target_arch = "wasm32"))]
343pub fn all_event_definitions() -> &'static [EventDefinition] {
344    use std::sync::OnceLock;
345    static SORTED: OnceLock<Vec<EventDefinition>> = OnceLock::new();
346    SORTED.get_or_init(|| {
347        let mut defs: Vec<EventDefinition> = EVENT_DEFINITIONS.iter().copied().collect();
348        defs.sort_by(|left, right| left.id.cmp(right.id));
349        let mut deduped: Vec<EventDefinition> = Vec::with_capacity(defs.len());
350        for def in defs {
351            match deduped.last() {
352                Some(last) if last.id == def.id => {
353                    assert!(
354                        *last == def,
355                        "conflicting EventDefinition registrations for id {:?}",
356                        def.id
357                    );
358                }
359                _ => deduped.push(def),
360            }
361        }
362        deduped
363    })
364}
365
366// `linkme` is unavailable on wasm32, so the registry and `all_event_definitions()`
367// are host-only — there is intentionally no wasm fallback. The catalog is consumed
368// only by host/server tooling and by the build-time TypeScript codegen
369// (`event_definition_catalog()` + its export test); wasm/browser consumers use the
370// generated TS catalog instead. A wasm caller referencing it is a compile error
371// rather than a silently-empty list.
372
373/// A variant entry in the TypeScript event catalog (owned, ts-rs-exportable).
374#[derive(Debug, Clone, Serialize, TS)]
375#[ts(export)]
376pub struct EventVariantTs {
377    pub key: String,
378    pub label: String,
379    pub category: EventCategory,
380}
381
382/// One entry in the TypeScript event catalog: the slim, viewer-relevant view of
383/// an [`EventDefinition`] (id/label/category/hidden + expansion variants). The
384/// browser viewer derives its event list from a generated array of these so it
385/// can never drift from the Rust registry. Confidence/approach metadata is
386/// intentionally omitted — it is host/docs-only.
387#[derive(Debug, Clone, Serialize, TS)]
388#[ts(export)]
389pub struct EventDefinitionCatalogEntry {
390    pub key: String,
391    pub label: String,
392    pub category: EventCategory,
393    pub hidden_from_review: bool,
394    pub variants: Vec<EventVariantTs>,
395}
396
397/// Build the TypeScript-facing catalog from the registry. Sorted/de-duplicated by
398/// id (inherited from [`all_event_definitions`]) so codegen output is stable.
399#[cfg(not(target_arch = "wasm32"))]
400pub fn event_definition_catalog() -> Vec<EventDefinitionCatalogEntry> {
401    all_event_definitions()
402        .iter()
403        .map(|definition| EventDefinitionCatalogEntry {
404            key: definition.id.to_owned(),
405            label: definition.label.to_owned(),
406            category: definition.category,
407            hidden_from_review: definition.hidden_from_review,
408            variants: definition
409                .variants
410                .iter()
411                .map(|variant| EventVariantTs {
412                    key: variant.key.to_owned(),
413                    label: variant.label.to_owned(),
414                    category: variant.category,
415                })
416                .collect(),
417        })
418        .collect()
419}
420
421/// Build-time codegen for the TypeScript event catalog data file. ts-rs only
422/// generates *types*; this writes the *data* array next to them. Runs as part of
423/// the `generate:stats-types` npm script via the `export_bindings` test filter,
424/// writing to `$TS_RS_EXPORT_DIR` when set. Without the env var it still validates
425/// serialization but writes nothing, so a plain `cargo test` never touches the tree.
426#[cfg(test)]
427#[test]
428fn export_bindings_event_definition_catalog() {
429    let catalog = event_definition_catalog();
430    let json = serde_json::to_string_pretty(&catalog).expect("serialize event catalog");
431    let contents = format!(
432        "// This file was generated from the subtr-actor event-definition registry. \
433Do not edit this file manually.\n\
434import type {{ EventDefinitionCatalogEntry }} from \"./EventDefinitionCatalogEntry.ts\";\n\
435\n\
436export const EVENT_DEFINITION_CATALOG: EventDefinitionCatalogEntry[] = {json};\n"
437    );
438
439    if let Ok(dir) = std::env::var("TS_RS_EXPORT_DIR") {
440        let path = std::path::Path::new(&dir).join("eventDefinitionCatalog.generated.ts");
441        std::fs::write(&path, contents).expect("write event catalog data file");
442    }
443}
444
445#[cfg(test)]
446#[path = "event_definition_scope_tests.rs"]
447mod scope_tests;
448
449/// Register an already-declared `EventDefinition` const into the
450/// [`EVENT_DEFINITIONS`] catalog. Used for payload-less rows (core scoreboard
451/// stats, goal context, expansion fallbacks) that have no [`StatsEvent`] type.
452macro_rules! register_stats_event_definition {
453    ($definition:ident) => {
454        paste::paste! {
455            #[cfg(not(target_arch = "wasm32"))]
456            #[distributed_slice(EVENT_DEFINITIONS)]
457            static [<$definition _REGISTRATION>]: EventDefinition = $definition;
458        }
459    };
460}
461
462macro_rules! define_stats_event {
463    (
464        $event_type:ty,
465        $definition:ident,
466        $id:literal,
467        $label:literal,
468        $category:expr_2021,
469        summary = $summary:literal,
470        approach = [$($approach:literal),* $(,)?]
471        $(, $modifier:ident = $modval:expr_2021)* $(,)?
472    ) => {
473        pub const $definition: EventDefinition =
474            event_definition($id, $label, $category, $summary, &[$($approach),*])
475                $(.$modifier($modval))*;
476
477        impl StatsEvent for $event_type {
478            const DEFINITION: EventDefinition = $definition;
479        }
480
481        register_stats_event_definition!($definition);
482    };
483
484    (
485        $event_type:ty,
486        $definition:ident,
487        $id:literal,
488        $label:literal,
489        $category:expr_2021
490        $(, $modifier:ident = $modval:expr_2021)* $(,)?
491    ) => {
492        pub const $definition: EventDefinition =
493            pending_event_definition($id, $label, $category)
494                $(.$modifier($modval))*;
495
496        impl StatsEvent for $event_type {
497            const DEFINITION: EventDefinition = $definition;
498        }
499
500        register_stats_event_definition!($definition);
501    };
502}
503
504// Variant tables for expansion-parent definitions. Each parent is
505// `hidden_from_review` and surfaces these concrete keys instead. The keys must
506// match the ones serialized at runtime in the server's timeline expansion.
507// All pickups surface under one key; the `detection` payload field
508// (`both` | `inferred_only` | `reported_only`) records corroboration provenance and is a
509// filter facet, not an event-type split.
510const BOOST_PICKUP_VARIANTS: &[EventVariant] = &[EventVariant::new(
511    "boost_pickup",
512    "Boost Pickup",
513    EventCategory::Other,
514)];
515
516// Payload-less event definitions: native Rocket League scoreboard stats, goal
517// context labels, and the air-dribble mechanic kind. These have no `StatsEvent`
518// payload type but still belong in the catalog so they surface in the review
519// picker (or are explicitly hidden) without a separate hand-maintained list.
520pub const ASSIST_EVENT_DEFINITION: EventDefinition =
521    pending_event_definition("assist", "Assist", EventCategory::Core);
522register_stats_event_definition!(ASSIST_EVENT_DEFINITION);
523
524pub const GOAL_EVENT_DEFINITION: EventDefinition =
525    pending_event_definition("goal", "Goal", EventCategory::Core);
526register_stats_event_definition!(GOAL_EVENT_DEFINITION);
527
528pub const SAVE_EVENT_DEFINITION: EventDefinition =
529    pending_event_definition("save", "Save", EventCategory::Core);
530register_stats_event_definition!(SAVE_EVENT_DEFINITION);
531
532pub const SHOT_EVENT_DEFINITION: EventDefinition =
533    pending_event_definition("shot", "Shot", EventCategory::Core);
534register_stats_event_definition!(SHOT_EVENT_DEFINITION);
535
536pub const KICKOFF_EVENT_DEFINITION: EventDefinition =
537    pending_event_definition("kickoff", "Kickoff", EventCategory::Core).scope(EventScope::Player);
538register_stats_event_definition!(KICKOFF_EVENT_DEFINITION);
539
540pub const GOAL_CONTEXT_EVENT_DEFINITION: EventDefinition =
541    pending_event_definition("goal_context", "Goal Context", EventCategory::Context).hidden(true);
542register_stats_event_definition!(GOAL_CONTEXT_EVENT_DEFINITION);
543
544pub const AIR_DRIBBLE_EVENT_DEFINITION: EventDefinition = event_definition(
545    "air_dribble",
546    "Air Dribble",
547    EventCategory::Mechanic,
548    "An airborne ball-control sequence where a player keeps the ball under control off the ground.",
549    &[
550        "Reuse the ball-carry sequence sampler's air-dribble carry kind, which tracks player-owned ball control while airborne.",
551        "Surface the span when a completed ball-carry sequence is classified as an air dribble rather than a grounded carry.",
552    ],
553);
554register_stats_event_definition!(AIR_DRIBBLE_EVENT_DEFINITION);
555
556define_stats_event!(
557    TimelineEvent,
558    TIMELINE_EVENT_DEFINITION,
559    "timeline",
560    "Replay Timeline Event",
561    EventCategory::Core,
562    hidden = true
563);
564define_stats_event!(
565    CorePlayerScoreboardEvent,
566    CORE_PLAYER_SCOREBOARD_EVENT_DEFINITION,
567    "core_player_scoreboard",
568    "Core Player Scoreboard",
569    EventCategory::Core,
570    hidden = true
571);
572define_stats_event!(
573    BackboardBounceEvent,
574    BACKBOARD_BOUNCE_EVENT_DEFINITION,
575    "backboard_bounce",
576    "Backboard Hit",
577    EventCategory::Basic,
578    summary = "A ball rebound off the opponent backboard attributed to the player who sent the ball there.",
579    approach = [
580        "Track the last touch during live play and attribute a later backboard rebound to that touch when it occurs within the configured attribution window.",
581        "Require the ball to be high, near the backboard face, and moving toward the backboard before contact.",
582        "Confirm the contact either by rebound velocity away from the backboard or by a same-player simultaneous touch at the backboard face.",
583    ],
584    scope = EventScope::Player
585);
586define_stats_event!(
587    CeilingShotEvent,
588    CEILING_SHOT_EVENT_DEFINITION,
589    "ceiling_shot",
590    "Ceiling Shot",
591    EventCategory::Mechanic,
592    summary = "A shot touch shortly after the player contacts the ceiling and drops back toward the ball.",
593    approach = [
594        "Record recent ceiling contacts when the car is near the ceiling and oriented roof-first against it.",
595        "Match a later touch by the same player within the ceiling-contact window after the player has separated from the ceiling.",
596        "Score the candidate from contact timing, height, separation, forward alignment, approach speed, ball impulse, and ceiling-contact alignment.",
597    ],
598    scope = EventScope::Player
599);
600define_stats_event!(
601    WallAerialEvent,
602    WALL_AERIAL_EVENT_DEFINITION,
603    "wall_aerial",
604    "Wall Aerial",
605    EventCategory::Mechanic,
606    summary = "An aerial launched off a side, end, or corner wall, whether or not the player is carrying the ball.",
607    approach = [
608        "Track how long each player rides the wall surface (a side or end wall), regardless of whether they have the ball.",
609        "Arm a wall-aerial candidate when a player who rode the wall long enough leaves it while airborne.",
610        "Classify the takeoff wall relative to the player's attack direction (left/right side, front/back end, or a corner) from the car's surface normal at the last wall contact.",
611        "Emit on a later aerial touch by the same player while the player and ball are high enough and the takeoff-to-touch window holds.",
612    ],
613    scope = EventScope::Player
614);
615define_stats_event!(
616    WallAerialShotEvent,
617    WALL_AERIAL_SHOT_EVENT_DEFINITION,
618    "wall_aerial_shot",
619    "Wall Shot",
620    EventCategory::Mechanic,
621    summary = "A shot credited to a player shortly after taking off from a wall.",
622    approach = [
623        "Track recent wall contact for each player and arm a candidate when the player leaves the wall while still above the ground threshold.",
624        "Classify the takeoff wall relative to the player's attack direction (left/right side, front/back end, or a corner) from the car's surface normal at the last wall contact.",
625        "Match a subsequent shot stat event by that player within the takeoff-to-shot window.",
626        "Require the shot touch to occur off the wall with sufficient player and ball height, then score confidence from timing, height, goal alignment, and ball speed.",
627    ],
628    scope = EventScope::Player
629);
630define_stats_event!(
631    CenterEvent,
632    CENTER_EVENT_DEFINITION,
633    "center",
634    "Center",
635    EventCategory::Basic,
636    summary = "A touch that moves the ball from a wide attacking position toward the central attacking area.",
637    approach = [
638        "Start a pending center from a live-play touch, unless that player immediately has a shot or goal event.",
639        "Watch the ball for a short window after the touch and require meaningful travel from a wide x-position toward a more central x-position in the attacking half.",
640        "Clear the candidate when it ages out, loses attribution, or becomes a shot/goal by the same player instead of a center.",
641    ],
642    scope = EventScope::Player
643);
644define_stats_event!(
645    FlickEvent,
646    FLICK_EVENT_DEFINITION,
647    "flick",
648    "Flick",
649    EventCategory::Mechanic,
650    summary = "A dodge-powered touch following a short controlled carry setup.",
651    approach = [
652        "Track controlled setup windows where the current controlling player keeps the ball close above the car within local-position and gap thresholds.",
653        "Record dodge starts that happen immediately after, or during, a qualifying setup, capturing the dodge torque (the flip axis) and the run's travel direction.",
654        "Classify the flick kind from the dodge direction: decompose the dodge torque in the travel frame into forward/back and side components, labeling a backflip that still launches the ball forward as a reverse flick, a sideways-dominant dodge as a side flick, and a forward dodge as a forward flick.",
655        "Tag handedness left/right from the ball's lateral deflection relative to travel.",
656        "Emit on a same-player touch shortly after the dodge when the ball impulse is large and directed away from the player, with confidence from setup duration, timing, impulse, and separation.",
657    ],
658    scope = EventScope::Player
659);
660define_stats_event!(
661    DodgeResetEvent,
662    DODGE_RESET_EVENT_DEFINITION,
663    "dodge_reset",
664    "Dodge Reset",
665    EventCategory::Basic,
666    summary = "A frame-level dodge refresh observed from replay state, marked as occurring on the ball (a flip reset) and as used when later converted by a dodge-powered touch.",
667    approach = [
668        "Consume dodge-refreshed replay events and preserve the player, team, frame, time, and counter value.",
669        "Classify the refresh as on-ball (a flip reset) when the player and ball are both airborne enough, close together, and the ball is positioned under the car in local space.",
670        "Keep on-ball resets pending in an in-flight ledger; if the player dodges into the ball within the reset-to-touch window, mark the originating reset event `used` with its reset-to-use latency.",
671        "Resolve every pending reset into an outcome: used, landed, superseded by a newer reset, expired, or cut off by a goal, live play ending, or the replay ending.",
672    ],
673    scope = EventScope::Player
674);
675define_stats_event!(
676    DoubleTapEvent,
677    DOUBLE_TAP_EVENT_DEFINITION,
678    "double_tap",
679    "Double Tap",
680    EventCategory::Mechanic,
681    summary = "A same-player follow-up touch after an attributed backboard bounce that creates a shot-like trajectory.",
682    approach = [
683        "Arm a pending double tap from a backboard-bounce event attributed to the player who sent the ball to the backboard.",
684        "Require the same player and team to touch the ball again during live play within the follow-up window.",
685        "Accept the follow-up only when the post-touch straight-line ball trajectory projects into or close to the opponent goal mouth.",
686    ],
687    scope = EventScope::Player
688);
689define_stats_event!(
690    OneTimerEvent,
691    ONE_TIMER_EVENT_DEFINITION,
692    "one_timer",
693    "One Timer",
694    EventCategory::Mechanic,
695    summary =
696        "A fast receiver touch from a completed pass that is immediately directed toward goal.",
697    approach = [
698        "Consume newly completed pass events on the frame they are recorded.",
699        "Require the current ball speed after the receiver's touch to exceed the one-timer speed threshold.",
700        "Require the post-touch ball velocity to align with the opponent goal center direction.",
701    ],
702    scope = EventScope::Player
703);
704define_stats_event!(
705    PassEvent,
706    PASS_EVENT_DEFINITION,
707    "pass",
708    "Pass",
709    EventCategory::Basic,
710    summary = "A same-team touch sequence where one player sends the ball to a different teammate.",
711    approach = [
712        "Track the last attributed touch in live play and compare it to each new touch.",
713        "Emit when a different teammate touches the ball within the pass window after the ball has traveled far enough.",
714        "Classify the pass as direct, backboard, fifty-fifty, or fifty-fifty backboard using intervening backboard-bounce and fifty-fifty state.",
715    ],
716    scope = EventScope::Player
717);
718define_stats_event!(
719    BallCarryEvent,
720    BALL_CARRY_EVENT_DEFINITION,
721    "ball_carry",
722    "Ball Carry",
723    EventCategory::Mechanic,
724    summary =
725        "A sustained player-ball control sequence, covering grounded carries and air dribbles.",
726    approach = [
727        "Use continuous ball-control tracking to build player-owned sequences while live play is active.",
728        "Sample grounded carries from close horizontal/vertical ball gaps over the car, excluding wall contact.",
729        "Sample air dribbles with the air-dribble policy, then emit completed sequences that meet the duration and validity rules for their carry kind.",
730    ],
731    scope = EventScope::Player
732);
733define_stats_event!(
734    ControlledPlayEvent,
735    CONTROLLED_PLAY_EVENT_DEFINITION,
736    "controlled_play",
737    "Controlled Play",
738    EventCategory::Other,
739    summary =
740        "A same-player possession episode with multiple touches and sustained close-ball time.",
741    approach = [
742        "Start a player-owned candidate from an attributed touch during live play.",
743        "Require at least two distinct touches by the same player with at least one second between the first and last touch.",
744        "Require sustained proximity to the ball and finish the candidate when another player touches, live play ends, or the touch chain times out.",
745    ],
746    scope = EventScope::Team
747);
748define_stats_event!(
749    FiftyFiftyEvent,
750    FIFTY_FIFTY_EVENT_DEFINITION,
751    "fifty_fifty",
752    "50/50",
753    EventCategory::Other,
754    summary = "A contested ball interaction involving touches or pressure from both teams in a short window.",
755    approach = [
756        "Start an active 50/50 when a frame contains touches from both teams, including kickoff-specific tracking.",
757        "Continue the contest for short follow-up touch windows while either involved team remains in contact.",
758        "Resolve after a delay once ball movement, possession state, or max duration gives a winner, possession outcome, or neutral result.",
759    ],
760    scope = EventScope::Team
761);
762define_stats_event!(
763    RushEvent,
764    RUSH_EVENT_DEFINITION,
765    "rush",
766    "Rush",
767    EventCategory::Other,
768    summary = "A quick possession transition where the attacking team has numbers moving out of its defensive half.",
769    approach = [
770        "Start from a possession change when the ball is still in the new attacking team's defensive half.",
771        "Count non-demoed attackers near or ahead of the ball and defenders between the ball and their own goal.",
772        "Emit once the new attacking team retains possession long enough with at least two attackers and at least one defender in the rush shape.",
773    ],
774    scope = EventScope::Team
775);
776define_stats_event!(
777    DodgeEvent,
778    DODGE_EVENT_DEFINITION,
779    "dodge",
780    "Dodge",
781    EventCategory::Basic,
782    summary = "A dodge-start event, optionally carrying a rough estimated dodge impulse when the velocity change is measurable.",
783    approach = [
784        "Start on the replay's dodge-active rising edge for each player.",
785        "Sample the player's velocity change over the early dodge window and subtract an approximate forward boost contribution when boost is active.",
786        "Store the impulse estimate as dodge_impulse, including car-local direction classification plus raw and compensated world-space vectors for visualization and downstream mechanic detectors.",
787    ],
788    scope = EventScope::Player
789);
790define_stats_event!(
791    SpeedFlipEvent,
792    SPEED_FLIP_EVENT_DEFINITION,
793    "speed_flip",
794    "Speed Flip",
795    EventCategory::Mechanic,
796    summary = "A ground-started diagonal dodge/cancel acceleration pattern, primarily intended for kickoff speed flips.",
797    approach = [
798        "Start candidates on dodge rising edges while the player is grounded, moving in the car's forward direction, and, for kickoff cases, within the kickoff-start window.",
799        "Track speed, forward alignment, boost alignment, diagonal angular-velocity balance, and early forward acceleration during a short evaluation window.",
800        "Emit when the combined diagonal, cancel, speed, and alignment confidence score clears the speed-flip threshold before the candidate expires.",
801    ],
802    scope = EventScope::Player
803);
804define_stats_event!(
805    HalfFlipEvent,
806    HALF_FLIP_EVENT_DEFINITION,
807    "half_flip",
808    "Half Flip",
809    EventCategory::Mechanic,
810    summary = "A dodge sequence that cancels a flip into an opposite facing direction.",
811    approach = [
812        "Start candidates on low grounded or low-air dodge rising edges.",
813        "Track the car's forward vector through the evaluation window, including vertical flip evidence and final horizontal facing direction.",
814        "Emit when the candidate has pitched through a flip, reaches and retains roughly opposite facing instead of rotating through a full end-over-end flip, and finishes with a meaningful horizontal facing direction.",
815    ],
816    scope = EventScope::Player
817);
818define_stats_event!(
819    HalfVolleyEvent,
820    HALF_VOLLEY_EVENT_DEFINITION,
821    "half_volley",
822    "Half Volley",
823    EventCategory::Mechanic,
824    summary = "A fast touch shortly after the ball bounces off the floor, paired with a recent player dodge.",
825    approach = [
826        "Detect floor bounces from ball height and vertical velocity reversal when no touch occurs on the bounce frame.",
827        "Track each player's recent ground contact and dodge start.",
828        "Emit on a same-player touch shortly after the floor bounce and dodge when the post-touch ball speed clears the configured threshold.",
829    ],
830    scope = EventScope::Player
831);
832define_stats_event!(
833    WavedashEvent,
834    WAVEDASH_EVENT_DEFINITION,
835    "wavedash",
836    "Wavedash",
837    EventCategory::Mechanic,
838    summary = "A low airborne dodge that lands quickly and converts the dodge into ground speed.",
839    approach = [
840        "Start candidates on dodge rising edges from a low but airborne height.",
841        "Watch for a landing within the wavedash window while the car is sufficiently upright.",
842        "Score confidence from dodge-to-landing timing, starting height, speed gain or landing speed, and landing uprightness.",
843    ],
844    scope = EventScope::Player
845);
846define_stats_event!(
847    WhiffEvent,
848    WHIFF_EVENT_DEFINITION,
849    "whiff",
850    "Whiff",
851    EventCategory::Other,
852    summary = "A committed attempt near the ball that does not result in that player touching it.",
853    approach = [
854        "Start candidates when a player gets within hitbox distance of the ball while moving or dodging toward it with sufficient alignment and closing speed.",
855        "Track the closest approach while the candidate remains near the ball.",
856        "Resolve as a whiff when the player exits the candidate window without touching, or as beaten-to-ball when an opponent touches first.",
857    ],
858    scope = EventScope::Player
859);
860define_stats_event!(
861    PowerslideEvent,
862    POWERSLIDE_EVENT_DEFINITION,
863    "powerslide",
864    "Powerslide",
865    EventCategory::Basic,
866    summary = "A state-change event for effective grounded powerslide use.",
867    approach = [
868        "Read each player's powerslide-active input/state on every frame.",
869        "Treat powerslide as effective only while the player is close enough to the ground.",
870        "Emit when a player's effective powerslide state changes between active and inactive.",
871    ],
872    scope = EventScope::Player
873);
874define_stats_event!(
875    TouchClassificationEvent,
876    TOUCH_CLASSIFICATION_EVENT_DEFINITION,
877    "touch",
878    "Touch",
879    EventCategory::Basic,
880    summary = "A classified ball touch carrying a set of independent tags: strength, surface/height context, action, and an outcome-based possession tag.",
881    approach = [
882        "Carry classification as a set of (group, value) tags rather than rivalrous fields, so independent reads coexist: a boom that the hitter recovers is tagged both action=boom and possession=advance.",
883        "Tag strength kind (control, medium hit, hard hit) from the ball speed change, plus surface, height band, and dodge context for the touching player at contact time.",
884        "Resolve a single mutually-exclusive action by precedence: replay-confirmed saves and shots first, then geometric save/shot trajectory projections, then clears out of the defensive third, then passes led toward a teammate, then booms hit hard downfield into space. A touch matching none of these has no action tag at all, rather than a catch-all value.",
885        "Retroactively raise the action to shot/save by outcome when stronger evidence arrives after the touch: a scored goal (the scorer's touch), a replay shot/save stat event that lands after the touch, or a settled post-touch trajectory that crosses the goal mouth. Upgrades only ever raise the action, never downgrade it.",
886        "Tag a touch contested independently of its action (a contested shot stays a shot and is also flagged contested), rather than collapsing contests into the action.",
887        "Retroactively add a possession tag by outcome on pass/clear/boom and action-less touches: control when the toucher stays close to the ball while matching its velocity for most of a short follow window or wins the follow-up touch with the ball kept near, advance when they win the follow-up touch after playing the ball clear into space.",
888        "Tag a touch's reception as a first touch when it starts a new reception: the previous global touch was by a different player or far enough in the past.",
889    ],
890    scope = EventScope::Player
891);
892define_stats_event!(
893    BoostPickupEvent,
894    BOOST_PICKUP_EVENT_DEFINITION,
895    "boost_pickups",
896    "Boost Pickup",
897    EventCategory::Other,
898    hidden = true,
899    variants = BOOST_PICKUP_VARIANTS,
900    scope = EventScope::Player
901);
902define_stats_event!(
903    RespawnEvent,
904    BOOST_RESPAWN_EVENT_DEFINITION,
905    "boost_respawn",
906    "Respawn",
907    EventCategory::Other,
908    scope = EventScope::Player
909);
910define_stats_event!(
911    BumpEvent,
912    BUMP_EVENT_DEFINITION,
913    "bump",
914    "Bump",
915    EventCategory::Other,
916    scope = EventScope::Player
917);
918define_stats_event!(
919    DemolitionEvent,
920    DEMOLITION_EVENT_DEFINITION,
921    "demolition",
922    "Demolition",
923    EventCategory::Basic,
924    scope = EventScope::Player
925);
926define_stats_event!(
927    PossessionEvent,
928    POSSESSION_EVENT_DEFINITION,
929    "possession",
930    "Possession",
931    EventCategory::Other,
932    scope = EventScope::Team
933);
934define_stats_event!(
935    LoosePossessionEvent,
936    LOOSE_POSSESSION_EVENT_DEFINITION,
937    "loose_possession",
938    "Loose Possession",
939    EventCategory::Other,
940    summary = "A team-possession span under the loose definition: the last team to touch owns the ball until the opponent takes it away.",
941    approach = [
942        "Track the last team to touch the ball, keeping possession through loose balls, teammate passes, and repelled 50-50 challenges.",
943        "Transfer possession only when the opponent demonstrably wins the ball, backdating the boundary to the opponent's takeover touch so there is no neutral gap.",
944        "Credit neutral only before the first touch of a live stretch or during a contested scramble off a neutral ball.",
945    ],
946    scope = EventScope::Team
947);
948define_stats_event!(
949    PlayerPossessionEvent,
950    PLAYER_POSSESSION_EVENT_DEFINITION,
951    "player_possession",
952    "Player Possession",
953    EventCategory::Other,
954    summary = "A contiguous single-player possession span enriched with touch, ball-progress, and sustained-control activity.",
955    approach = [
956        "Follow the shared possession tracker's controlling player and open a span when a player establishes control.",
957        "Bridge contested or pending-turnover interruptions shorter than the merge gap when the same player re-establishes control, excluding the gap from possessed duration.",
958        "Accumulate distinct touches (with aerial/wall classification), signed ball travel toward the opponent goal, and per-frame carry/air-dribble samples while the span is active.",
959    ],
960    scope = EventScope::Player
961);
962define_stats_event!(
963    BallHalfEvent,
964    PRESSURE_EVENT_DEFINITION,
965    "ball_half",
966    "Ball Half",
967    EventCategory::Other,
968    scope = EventScope::Team
969);
970define_stats_event!(
971    TerritorialPressureEvent,
972    TERRITORIAL_PRESSURE_EVENT_DEFINITION,
973    "territorial_pressure",
974    "Territorial Pressure",
975    EventCategory::Other,
976    scope = EventScope::Team
977);
978define_stats_event!(
979    MovementEvent,
980    MOVEMENT_EVENT_DEFINITION,
981    "movement",
982    "Movement",
983    EventCategory::Other,
984    scope = EventScope::Player
985);
986define_stats_event!(
987    PlayerActivityEvent,
988    PLAYER_ACTIVITY_EVENT_DEFINITION,
989    "player_activity",
990    "Player Activity",
991    EventCategory::Positioning,
992    scope = EventScope::Player
993);
994define_stats_event!(
995    FieldThirdEvent,
996    FIELD_THIRD_EVENT_DEFINITION,
997    "field_third",
998    "Field Third",
999    EventCategory::Positioning,
1000    scope = EventScope::Player
1001);
1002define_stats_event!(
1003    FieldHalfEvent,
1004    FIELD_HALF_EVENT_DEFINITION,
1005    "field_half",
1006    "Field Half",
1007    EventCategory::Positioning,
1008    scope = EventScope::Player
1009);
1010define_stats_event!(
1011    BallDepthEvent,
1012    BALL_DEPTH_EVENT_DEFINITION,
1013    "ball_depth",
1014    "Ball Depth",
1015    EventCategory::Positioning,
1016    scope = EventScope::Player
1017);
1018define_stats_event!(
1019    BallThirdEvent,
1020    BALL_THIRD_EVENT_DEFINITION,
1021    "ball_third",
1022    "Ball Third",
1023    EventCategory::Positioning,
1024    scope = EventScope::Player
1025);
1026define_stats_event!(
1027    DepthRoleEvent,
1028    DEPTH_ROLE_EVENT_DEFINITION,
1029    "depth_role",
1030    "Depth Role",
1031    EventCategory::Positioning,
1032    scope = EventScope::Player
1033);
1034define_stats_event!(
1035    BallProximityEvent,
1036    BALL_PROXIMITY_EVENT_DEFINITION,
1037    "ball_proximity",
1038    "Ball Proximity",
1039    EventCategory::Positioning,
1040    scope = EventScope::Player
1041);
1042define_stats_event!(
1043    RotationRoleEvent,
1044    ROTATION_ROLE_EVENT_DEFINITION,
1045    "rotation_role",
1046    "Rotation Role",
1047    EventCategory::Positioning,
1048    scope = EventScope::Player
1049);
1050define_stats_event!(
1051    FirstManChangeEvent,
1052    FIRST_MAN_CHANGE_EVENT_DEFINITION,
1053    "first_man_change",
1054    "First-Man Change",
1055    EventCategory::Positioning,
1056    scope = EventScope::Player
1057);
1058define_stats_event!(
1059    FlipResetEvent,
1060    FLIP_RESET_EVENT_DEFINITION,
1061    "flip_reset",
1062    "Flip Reset",
1063    EventCategory::Mechanic,
1064    summary = "An on-ball dodge refresh that is confirmed when the player uses the gained dodge and touches the ball again before landing.",
1065    approach = [
1066        "Consume on-ball dodge refreshes detected from replay state as pending flip-reset candidates.",
1067        "Require a later dodge start by the same player while the reset is still pending.",
1068        "Confirm only when that player touches the ball while dodge-active before landing and within the reset-to-touch window.",
1069    ],
1070    scope = EventScope::Player
1071);
1072define_stats_event!(
1073    Event,
1074    TIMELINE_ENVELOPE_EVENT_DEFINITION,
1075    "event",
1076    "Event",
1077    EventCategory::Basic,
1078    summary = "A shared event envelope with common metadata and a typed event payload.",
1079    approach = [
1080        "Collect completed events from the analysis graph at finish time.",
1081        "Wrap each typed event payload with common timing, participant, team, position, confidence, and stream metadata.",
1082        "Serialize timeline events as a single heterogeneous event list for playback and analysis consumers.",
1083    ]
1084);
1085
1086// The former hand-maintained `ALL_EVENT_DEFINITIONS` array has been replaced by
1087// the auto-populated `EVENT_DEFINITIONS` distributed slice; read it through
1088// `all_event_definitions()`. Defining an event via `define_stats_event!` (or
1089// `register_stats_event_definition!`) is now the only registration step.
1090
1091pub(crate) const MATCH_STATS_EMITTED_EVENTS: &[EmittedEvent] = &[
1092    produced_event(
1093        &TIMELINE_EVENT_DEFINITION,
1094        "match_stats",
1095        "MatchStatsNode",
1096        "MatchStatsCalculator",
1097    ),
1098    produced_event(
1099        &CORE_PLAYER_SCOREBOARD_EVENT_DEFINITION,
1100        "match_stats",
1101        "MatchStatsNode",
1102        "MatchStatsCalculator",
1103    ),
1104];
1105
1106pub(crate) const DEMO_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1107    &DEMOLITION_EVENT_DEFINITION,
1108    "demo",
1109    "DemoNode",
1110    "DemoCalculator",
1111)];
1112
1113pub(crate) const BACKBOARD_BOUNCE_STATE_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1114    &BACKBOARD_BOUNCE_EVENT_DEFINITION,
1115    "backboard_bounce_state",
1116    "BackboardBounceStateNode",
1117    "BackboardBounceCalculator",
1118)];
1119
1120pub(crate) const CEILING_SHOT_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1121    &CEILING_SHOT_EVENT_DEFINITION,
1122    "ceiling_shot",
1123    "CeilingShotNode",
1124    "CeilingShotCalculator",
1125)];
1126
1127pub(crate) const WALL_AERIAL_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1128    &WALL_AERIAL_EVENT_DEFINITION,
1129    "wall_aerial",
1130    "WallAerialNode",
1131    "WallAerialCalculator",
1132)];
1133
1134pub(crate) const WALL_AERIAL_SHOT_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1135    &WALL_AERIAL_SHOT_EVENT_DEFINITION,
1136    "wall_aerial_shot",
1137    "WallAerialShotNode",
1138    "WallAerialShotCalculator",
1139)];
1140
1141pub(crate) const CENTER_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1142    &CENTER_EVENT_DEFINITION,
1143    "center",
1144    "CenterNode",
1145    "CenterCalculator",
1146)];
1147
1148pub(crate) const FLICK_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1149    &FLICK_EVENT_DEFINITION,
1150    "flick",
1151    "FlickNode",
1152    "FlickCalculator",
1153)];
1154
1155pub(crate) const DODGE_RESET_EMITTED_EVENTS: &[EmittedEvent] = &[
1156    produced_event(
1157        &DODGE_RESET_EVENT_DEFINITION,
1158        "dodge_reset",
1159        "DodgeResetNode",
1160        "DodgeResetCalculator",
1161    ),
1162    produced_event(
1163        &FLIP_RESET_EVENT_DEFINITION,
1164        "flip_reset",
1165        "DodgeResetNode",
1166        "DodgeResetCalculator",
1167    ),
1168];
1169
1170pub(crate) const DOUBLE_TAP_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1171    &DOUBLE_TAP_EVENT_DEFINITION,
1172    "double_tap",
1173    "DoubleTapNode",
1174    "DoubleTapCalculator",
1175)];
1176
1177pub(crate) const ONE_TIMER_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1178    &ONE_TIMER_EVENT_DEFINITION,
1179    "one_timer",
1180    "OneTimerNode",
1181    "OneTimerCalculator",
1182)];
1183
1184pub(crate) const PASS_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1185    &PASS_EVENT_DEFINITION,
1186    "pass",
1187    "PassNode",
1188    "PassCalculator",
1189)];
1190
1191pub(crate) const BALL_CARRY_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1192    &BALL_CARRY_EVENT_DEFINITION,
1193    "ball_carry",
1194    "BallCarryNode",
1195    "BallCarryCalculator",
1196)];
1197
1198pub(crate) const AIR_DRIBBLE_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1199    &BALL_CARRY_EVENT_DEFINITION,
1200    "air_dribble",
1201    "AirDribbleNode",
1202    "AirDribbleCalculator",
1203)];
1204
1205pub(crate) const CONTROLLED_PLAY_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1206    &CONTROLLED_PLAY_EVENT_DEFINITION,
1207    "controlled_play",
1208    "ControlledPlayNode",
1209    "ControlledPlayCalculator",
1210)];
1211
1212pub(crate) const FIFTY_FIFTY_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1213    &FIFTY_FIFTY_EVENT_DEFINITION,
1214    "fifty_fifty",
1215    "FiftyFiftyNode",
1216    "FiftyFiftyCalculator",
1217)];
1218
1219pub(crate) const RUSH_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1220    &RUSH_EVENT_DEFINITION,
1221    "rush",
1222    "RushNode",
1223    "RushCalculator",
1224)];
1225
1226pub(crate) const DODGE_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1227    &DODGE_EVENT_DEFINITION,
1228    "dodge",
1229    "FlipImpulseNode",
1230    "FlipImpulseCalculator",
1231)];
1232
1233pub(crate) const SPEED_FLIP_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1234    &SPEED_FLIP_EVENT_DEFINITION,
1235    "speed_flip",
1236    "SpeedFlipNode",
1237    "SpeedFlipCalculator",
1238)];
1239
1240pub(crate) const HALF_FLIP_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1241    &HALF_FLIP_EVENT_DEFINITION,
1242    "half_flip",
1243    "HalfFlipNode",
1244    "HalfFlipCalculator",
1245)];
1246
1247pub(crate) const HALF_VOLLEY_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1248    &HALF_VOLLEY_EVENT_DEFINITION,
1249    "half_volley",
1250    "HalfVolleyNode",
1251    "HalfVolleyCalculator",
1252)];
1253
1254pub(crate) const WAVEDASH_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1255    &WAVEDASH_EVENT_DEFINITION,
1256    "wavedash",
1257    "WavedashNode",
1258    "WavedashCalculator",
1259)];
1260
1261pub(crate) const WHIFF_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1262    &WHIFF_EVENT_DEFINITION,
1263    "whiff",
1264    "WhiffNode",
1265    "WhiffCalculator",
1266)];
1267
1268pub(crate) const POWERSLIDE_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1269    &POWERSLIDE_EVENT_DEFINITION,
1270    "powerslide",
1271    "PowerslideNode",
1272    "PowerslideCalculator",
1273)];
1274
1275pub(crate) const TOUCH_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1276    &TOUCH_CLASSIFICATION_EVENT_DEFINITION,
1277    "touch",
1278    "TouchNode",
1279    "TouchCalculator",
1280)];
1281
1282pub(crate) const BOOST_EMITTED_EVENTS: &[EmittedEvent] = &[
1283    produced_event(
1284        &BOOST_PICKUP_EVENT_DEFINITION,
1285        "boost",
1286        "BoostNode",
1287        "BoostCalculator",
1288    ),
1289    produced_event(
1290        &BOOST_RESPAWN_EVENT_DEFINITION,
1291        "boost",
1292        "BoostNode",
1293        "BoostCalculator",
1294    ),
1295];
1296
1297pub(crate) const BUMP_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1298    &BUMP_EVENT_DEFINITION,
1299    "bump",
1300    "BumpNode",
1301    "BumpCalculator",
1302)];
1303
1304pub(crate) const POSSESSION_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1305    &POSSESSION_EVENT_DEFINITION,
1306    "possession",
1307    "PossessionNode",
1308    "PossessionCalculator",
1309)];
1310
1311pub(crate) const PLAYER_POSSESSION_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1312    &PLAYER_POSSESSION_EVENT_DEFINITION,
1313    "player_possession",
1314    "PlayerPossessionNode",
1315    "PlayerPossessionCalculator",
1316)];
1317
1318pub(crate) const LOOSE_POSSESSION_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1319    &LOOSE_POSSESSION_EVENT_DEFINITION,
1320    "loose_possession",
1321    "LoosePossessionNode",
1322    "LoosePossessionCalculator",
1323)];
1324
1325pub(crate) const BALL_HALF_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1326    &PRESSURE_EVENT_DEFINITION,
1327    "ball_half",
1328    "BallHalfNode",
1329    "BallHalfCalculator",
1330)];
1331
1332pub(crate) const BALL_THIRD_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1333    &BALL_THIRD_EVENT_DEFINITION,
1334    "ball_third",
1335    "BallThirdNode",
1336    "BallThirdCalculator",
1337)];
1338
1339pub(crate) const TERRITORIAL_BALL_HALF_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1340    &TERRITORIAL_PRESSURE_EVENT_DEFINITION,
1341    "territorial_pressure",
1342    "TerritorialPressureNode",
1343    "TerritorialPressureCalculator",
1344)];
1345
1346pub(crate) const MOVEMENT_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1347    &MOVEMENT_EVENT_DEFINITION,
1348    "movement",
1349    "MovementNode",
1350    "MovementCalculator",
1351)];
1352
1353pub(crate) const POSITIONING_EMITTED_EVENTS: &[EmittedEvent] = &[
1354    produced_event(
1355        &PLAYER_ACTIVITY_EVENT_DEFINITION,
1356        "positioning",
1357        "PositioningNode",
1358        "PositioningCalculator",
1359    ),
1360    produced_event(
1361        &FIELD_THIRD_EVENT_DEFINITION,
1362        "positioning",
1363        "PositioningNode",
1364        "PositioningCalculator",
1365    ),
1366    produced_event(
1367        &FIELD_HALF_EVENT_DEFINITION,
1368        "positioning",
1369        "PositioningNode",
1370        "PositioningCalculator",
1371    ),
1372    produced_event(
1373        &BALL_DEPTH_EVENT_DEFINITION,
1374        "positioning",
1375        "PositioningNode",
1376        "PositioningCalculator",
1377    ),
1378    produced_event(
1379        &DEPTH_ROLE_EVENT_DEFINITION,
1380        "positioning",
1381        "PositioningNode",
1382        "PositioningCalculator",
1383    ),
1384    produced_event(
1385        &BALL_PROXIMITY_EVENT_DEFINITION,
1386        "positioning",
1387        "PositioningNode",
1388        "PositioningCalculator",
1389    ),
1390];
1391
1392pub(crate) const ROTATION_EMITTED_EVENTS: &[EmittedEvent] = &[
1393    produced_event(
1394        &ROTATION_ROLE_EVENT_DEFINITION,
1395        "rotation",
1396        "RotationNode",
1397        "RotationCalculator",
1398    ),
1399    produced_event(
1400        &FIRST_MAN_CHANGE_EVENT_DEFINITION,
1401        "rotation",
1402        "RotationNode",
1403        "RotationCalculator",
1404    ),
1405];
1406
1407pub(crate) const STATS_TIMELINE_EVENTS_EMITTED_EVENTS: &[EmittedEvent] = &[produced_event(
1408    &TIMELINE_ENVELOPE_EVENT_DEFINITION,
1409    "stats_timeline_events",
1410    "StatsTimelineEventsNode",
1411    "StatsTimelineEventsState",
1412)];
1413
1414#[cfg(test)]
1415#[path = "event_definition_tests.rs"]
1416mod tests;