Skip to main content

subtr_actor/stats/analysis_graph/nodes/
stats_timeline_events.rs

1use super::*;
2use crate::stats::calculators::*;
3use crate::*;
4
5/// Holds the collected replay stats-timeline events.
6#[derive(Debug, Clone, Default)]
7pub struct StatsTimelineEventsState {
8    pub events: ReplayStatsTimelineEvents,
9}
10
11const MECHANIC_AIR_DRIBBLE: &str = "air_dribble";
12const MECHANIC_BALL_CARRY: &str = "ball_carry";
13const MECHANIC_CEILING_SHOT: &str = "ceiling_shot";
14const MECHANIC_CENTER: &str = "center";
15const MECHANIC_DOUBLE_TAP: &str = "double_tap";
16const MECHANIC_FLICK: &str = "flick";
17const MECHANIC_FLIP_RESET: &str = "flip_reset";
18const MECHANIC_HALF_FLIP: &str = "half_flip";
19const MECHANIC_HALF_VOLLEY: &str = "half_volley";
20const MECHANIC_ONE_TIMER: &str = "one_timer";
21const MECHANIC_PASS: &str = "pass";
22const MECHANIC_SPEED_FLIP: &str = "speed_flip";
23const MECHANIC_WALL_AERIAL: &str = "wall_aerial";
24const MECHANIC_WALL_AERIAL_SHOT: &str = "wall_aerial_shot";
25const MECHANIC_WAVEDASH: &str = "wavedash";
26
27/// List of mechanic kind identifiers emitted into the stats timeline.
28pub const STATS_TIMELINE_MECHANIC_KINDS: &[&str] = &[
29    MECHANIC_AIR_DRIBBLE,
30    MECHANIC_BALL_CARRY,
31    MECHANIC_CEILING_SHOT,
32    MECHANIC_CENTER,
33    MECHANIC_DOUBLE_TAP,
34    MECHANIC_FLICK,
35    MECHANIC_FLIP_RESET,
36    MECHANIC_HALF_FLIP,
37    MECHANIC_HALF_VOLLEY,
38    MECHANIC_ONE_TIMER,
39    MECHANIC_PASS,
40    MECHANIC_SPEED_FLIP,
41    MECHANIC_WALL_AERIAL,
42    MECHANIC_WALL_AERIAL_SHOT,
43    MECHANIC_WAVEDASH,
44];
45
46/// Collects mechanic/goal/state events from all calculators into a compact stats-timeline event stream.
47pub struct StatsTimelineEventsNode {
48    state: StatsTimelineEventsState,
49}
50
51impl StatsTimelineEventsNode {
52    pub fn new() -> Self {
53        Self {
54            state: StatsTimelineEventsState::default(),
55        }
56    }
57
58    fn dependencies() -> NodeDependencies {
59        vec![
60            frame_info_dependency(),
61            gameplay_state_dependency(),
62            live_play_dependency(),
63            match_stats_dependency(),
64            // Keep compact event transfer independent from full partial-sum projection.
65            backboard_dependency(),
66            ceiling_shot_dependency(),
67            wall_aerial_dependency(),
68            wall_aerial_shot_dependency(),
69            double_tap_dependency(),
70            one_timer_dependency(),
71            pass_dependency(),
72            controlled_play_dependency(),
73            fifty_fifty_dependency(),
74            kickoff_dependency(),
75            possession_dependency(),
76            player_possession_dependency(),
77            loose_possession_dependency(),
78            ball_half_dependency(),
79            ball_third_dependency(),
80            territorial_pressure_dependency(),
81            rotation_dependency(),
82            rush_dependency(),
83            touch_dependency(),
84            whiff_dependency(),
85            wavedash_dependency(),
86            flip_impulse_dependency(),
87            speed_flip_dependency(),
88            half_flip_dependency(),
89            flick_dependency(),
90            dodge_reset_dependency(),
91            ball_carry_dependency(),
92            air_dribble_dependency(),
93            boost_dependency(),
94            bump_dependency(),
95            half_volley_dependency(),
96            movement_dependency(),
97            positioning_dependency(),
98            powerslide_dependency(),
99            demo_dependency(),
100            center_dependency(),
101            aerial_goal_dependency(),
102            high_aerial_goal_dependency(),
103            long_distance_goal_dependency(),
104            own_half_goal_dependency(),
105            empty_net_goal_dependency(),
106            counter_attack_goal_dependency(),
107            sustained_pressure_goal_dependency(),
108            flick_goal_dependency(),
109            ceiling_shot_goal_dependency(),
110            double_tap_goal_dependency(),
111            one_timer_goal_dependency(),
112            passing_goal_dependency(),
113            air_dribble_goal_dependency(),
114            flip_reset_goal_dependency(),
115            flip_into_ball_goal_dependency(),
116            bump_goal_dependency(),
117            demo_goal_dependency(),
118            half_volley_goal_dependency(),
119            kickoff_goal_dependency(),
120        ]
121    }
122
123    fn capture_events(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
124        let match_stats = ctx.get::<MatchStatsCalculator>()?;
125        let possession = ctx.get::<PossessionCalculator>()?;
126        let player_possession = ctx.get::<PlayerPossessionCalculator>()?;
127        let loose_possession = ctx.get::<LoosePossessionCalculator>()?;
128        let ball_half = ctx.get::<BallHalfCalculator>()?;
129        let ball_third = ctx.get::<BallThirdCalculator>()?;
130        let territorial_pressure = ctx.get::<TerritorialPressureCalculator>()?;
131        let movement = ctx.get::<MovementCalculator>()?;
132        let positioning = ctx.get::<PositioningCalculator>()?;
133        let rotation = ctx.get::<RotationCalculator>()?;
134        let demo = ctx.get::<DemoCalculator>()?;
135        let backboard = ctx.get::<BackboardCalculator>()?;
136        let ball_carry = ctx.get::<BallCarryCalculator>()?;
137        let air_dribble = ctx.get::<AirDribbleCalculator>()?;
138        let ceiling_shot = ctx.get::<CeilingShotCalculator>()?;
139        let wall_aerial = ctx.get::<WallAerialCalculator>()?;
140        let wall_aerial_shot = ctx.get::<WallAerialShotCalculator>()?;
141        let center = ctx.get::<CenterCalculator>()?;
142        let dodge_reset = ctx.get::<DodgeResetCalculator>()?;
143        let double_tap = ctx.get::<DoubleTapCalculator>()?;
144        let one_timer = ctx.get::<OneTimerCalculator>()?;
145        let pass = ctx.get::<PassCalculator>()?;
146        let controlled_play = ctx.get::<ControlledPlayCalculator>()?;
147        let fifty_fifty = ctx.get::<FiftyFiftyCalculator>()?;
148        let kickoff = ctx.get::<KickoffCalculator>()?;
149        let flick = ctx.get::<FlickCalculator>()?;
150        let aerial_goal = ctx.get::<AerialGoalCalculator>()?;
151        let high_aerial_goal = ctx.get::<HighAerialGoalCalculator>()?;
152        let long_distance_goal = ctx.get::<LongDistanceGoalCalculator>()?;
153        let own_half_goal = ctx.get::<OwnHalfGoalCalculator>()?;
154        let empty_net_goal = ctx.get::<EmptyNetGoalCalculator>()?;
155        let counter_attack_goal = ctx.get::<CounterAttackGoalCalculator>()?;
156        let sustained_pressure_goal = ctx.get::<SustainedPressureGoalCalculator>()?;
157        let flick_goal = ctx.get::<FlickGoalCalculator>()?;
158        let ceiling_shot_goal = ctx.get::<CeilingShotGoalCalculator>()?;
159        let double_tap_goal = ctx.get::<DoubleTapGoalCalculator>()?;
160        let one_timer_goal = ctx.get::<OneTimerGoalCalculator>()?;
161        let passing_goal = ctx.get::<PassingGoalCalculator>()?;
162        let air_dribble_goal = ctx.get::<AirDribbleGoalCalculator>()?;
163        let flip_reset_goal = ctx.get::<FlipResetGoalCalculator>()?;
164        let flip_into_ball_goal = ctx.get::<FlipIntoBallGoalCalculator>()?;
165        let bump_goal = ctx.get::<BumpGoalCalculator>()?;
166        let demo_goal = ctx.get::<DemoGoalCalculator>()?;
167        let half_volley_goal = ctx.get::<HalfVolleyGoalCalculator>()?;
168        let kickoff_goal = ctx.get::<KickoffGoalCalculator>()?;
169        let rush = ctx.get::<RushCalculator>()?;
170        let flip_impulse = ctx.get::<FlipImpulseCalculator>()?;
171        let speed_flip = ctx.get::<SpeedFlipCalculator>()?;
172        let half_flip = ctx.get::<HalfFlipCalculator>()?;
173        let half_volley = ctx.get::<HalfVolleyCalculator>()?;
174        let wavedash = ctx.get::<WavedashCalculator>()?;
175        let whiff = ctx.get::<WhiffCalculator>()?;
176        let powerslide = ctx.get::<PowerslideCalculator>()?;
177        let touch = ctx.get::<TouchCalculator>()?;
178        let boost = ctx.get::<BoostCalculator>()?;
179        let bump = ctx.get::<BumpCalculator>()?;
180
181        let mut timeline = match_stats.timeline().to_vec();
182        timeline.sort_by(|left, right| left.time.total_cmp(&right.time));
183        let goal_tag_assignments = combined_goal_tag_assignments(&[
184            aerial_goal.events(),
185            high_aerial_goal.events(),
186            long_distance_goal.events(),
187            own_half_goal.events(),
188            empty_net_goal.events(),
189            counter_attack_goal.events(),
190            sustained_pressure_goal.events(),
191            flick_goal.events(),
192            ceiling_shot_goal.events(),
193            double_tap_goal.events(),
194            one_timer_goal.events(),
195            passing_goal.events(),
196            air_dribble_goal.events(),
197            flip_reset_goal.events(),
198            flip_into_ball_goal.events(),
199            bump_goal.events(),
200            demo_goal.events(),
201            half_volley_goal.events(),
202            kickoff_goal.events(),
203        ]);
204        let goal_context =
205            goal_context_events_with_tags(match_stats.goal_context_events(), &goal_tag_assignments);
206
207        self.state.events = ReplayStatsTimelineEvents {
208            events: build_replay_events(
209                &timeline,
210                match_stats,
211                possession,
212                player_possession,
213                loose_possession,
214                ball_half,
215                ball_third,
216                territorial_pressure,
217                movement,
218                positioning,
219                rotation,
220                &goal_context,
221                backboard,
222                ball_carry,
223                air_dribble,
224                ceiling_shot,
225                wall_aerial,
226                wall_aerial_shot,
227                center,
228                dodge_reset,
229                double_tap,
230                one_timer,
231                pass,
232                controlled_play,
233                fifty_fifty,
234                kickoff,
235                rush,
236                flip_impulse,
237                speed_flip,
238                half_flip,
239                half_volley,
240                wavedash,
241                whiff,
242                powerslide,
243                touch,
244                boost,
245                bump,
246                demo,
247                flick,
248            ),
249        };
250        Ok(())
251    }
252}
253
254impl Default for StatsTimelineEventsNode {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl AnalysisNode for StatsTimelineEventsNode {
261    type State = StatsTimelineEventsState;
262
263    fn name(&self) -> &'static str {
264        "stats_timeline_events"
265    }
266
267    fn emitted_events(&self) -> &'static [crate::stats::calculators::EmittedEvent] {
268        crate::stats::calculators::STATS_TIMELINE_EVENTS_EMITTED_EVENTS
269    }
270
271    fn dependencies(&self) -> Vec<AnalysisDependency> {
272        Self::dependencies()
273    }
274
275    fn evaluate(&mut self, _ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
276        Ok(())
277    }
278
279    fn finish(&mut self, ctx: &AnalysisStateContext<'_>) -> SubtrActorResult<()> {
280        self.capture_events(ctx)
281    }
282
283    fn state(&self) -> &Self::State {
284        &self.state
285    }
286}
287
288fn moment(frame: usize, time: f32) -> EventTiming {
289    EventTiming::Moment { frame, time }
290}
291
292fn span(start_frame: usize, end_frame: usize, start_time: f32, end_time: f32) -> EventTiming {
293    EventTiming::Span {
294        start_frame,
295        end_frame,
296        start_time,
297        end_time,
298    }
299}
300
301#[allow(clippy::too_many_arguments)]
302fn make_event(
303    stream: &str,
304    index: usize,
305    timing: EventTiming,
306    payload: EventPayload,
307    primary_player: Option<PlayerId>,
308    secondary_player: Option<PlayerId>,
309    team_is_team_0: Option<bool>,
310    player_position: Option<[f32; 3]>,
311    ball_position: Option<[f32; 3]>,
312    confidence: Option<f32>,
313) -> Event {
314    let frame_id = match timing {
315        EventTiming::Moment { frame, .. } => frame.to_string(),
316        EventTiming::Span {
317            start_frame,
318            end_frame,
319            ..
320        } => format!("{start_frame}:{end_frame}"),
321    };
322    let scope = payload.scope();
323    Event {
324        meta: EventMeta {
325            id: format!("{stream}:{frame_id}:{index}"),
326            stream: stream.to_owned(),
327            label: stats_timeline_event_label(stream),
328            scope,
329            timing,
330            primary_player,
331            secondary_player,
332            player_position,
333            ball_position,
334            team_is_team_0,
335            confidence,
336            properties: Vec::new(),
337        },
338        payload,
339    }
340}
341
342fn event_start_time(event: &Event) -> f32 {
343    match event.meta.timing {
344        EventTiming::Moment { time, .. } => time,
345        EventTiming::Span { start_time, .. } => start_time,
346    }
347}
348
349#[allow(clippy::too_many_arguments, clippy::cognitive_complexity)]
350fn build_replay_events(
351    timeline: &[TimelineEvent],
352    match_stats: &MatchStatsCalculator,
353    possession: &PossessionCalculator,
354    player_possession: &PlayerPossessionCalculator,
355    loose_possession: &LoosePossessionCalculator,
356    ball_half: &BallHalfCalculator,
357    ball_third: &BallThirdCalculator,
358    territorial_pressure: &TerritorialPressureCalculator,
359    movement: &MovementCalculator,
360    positioning: &PositioningCalculator,
361    rotation: &RotationCalculator,
362    goal_context: &[GoalContextEvent],
363    backboard: &BackboardCalculator,
364    ball_carry: &BallCarryCalculator,
365    air_dribble: &AirDribbleCalculator,
366    ceiling_shot: &CeilingShotCalculator,
367    wall_aerial: &WallAerialCalculator,
368    wall_aerial_shot: &WallAerialShotCalculator,
369    center: &CenterCalculator,
370    dodge_reset: &DodgeResetCalculator,
371    double_tap: &DoubleTapCalculator,
372    one_timer: &OneTimerCalculator,
373    pass: &PassCalculator,
374    controlled_play: &ControlledPlayCalculator,
375    fifty_fifty: &FiftyFiftyCalculator,
376    kickoff: &KickoffCalculator,
377    rush: &RushCalculator,
378    flip_impulse: &FlipImpulseCalculator,
379    speed_flip: &SpeedFlipCalculator,
380    half_flip: &HalfFlipCalculator,
381    half_volley: &HalfVolleyCalculator,
382    wavedash: &WavedashCalculator,
383    whiff: &WhiffCalculator,
384    powerslide: &PowerslideCalculator,
385    touch: &TouchCalculator,
386    boost: &BoostCalculator,
387    bump: &BumpCalculator,
388    demo: &DemoCalculator,
389    flick: &FlickCalculator,
390) -> Vec<Event> {
391    let mut events = Vec::new();
392
393    for (index, event) in timeline.iter().enumerate() {
394        events.push(make_event(
395            "timeline",
396            index,
397            moment(event.frame.unwrap_or_default(), event.time),
398            EventPayload::Timeline(event.clone()),
399            event.player_id.clone(),
400            None,
401            event.is_team_0,
402            event.player_position,
403            None,
404            None,
405        ));
406    }
407
408    for (index, event) in match_stats.core_player_events().iter().enumerate() {
409        events.push(make_event(
410            "core_player",
411            index,
412            moment(event.frame, event.time),
413            EventPayload::CorePlayer(event.clone()),
414            Some(event.player.clone()),
415            None,
416            Some(event.is_team_0),
417            event.player_position,
418            None,
419            None,
420        ));
421    }
422
423    for (index, event) in possession.events().iter().enumerate() {
424        events.push(make_event(
425            "possession",
426            index,
427            span(event.frame, event.end_frame, event.time, event.end_time),
428            EventPayload::Possession(event.clone()),
429            event.player_id.clone(),
430            None,
431            None,
432            None,
433            None,
434            None,
435        ));
436    }
437
438    for (index, event) in loose_possession.events().iter().enumerate() {
439        events.push(make_event(
440            "loose_possession",
441            index,
442            span(event.frame, event.end_frame, event.time, event.end_time),
443            EventPayload::LoosePossession(event.clone()),
444            event.player_id.clone(),
445            None,
446            None,
447            None,
448            None,
449            None,
450        ));
451    }
452
453    for (index, event) in player_possession.events().iter().enumerate() {
454        events.push(make_event(
455            "player_possession",
456            index,
457            span(
458                event.start_frame,
459                event.end_frame,
460                event.start_time,
461                event.end_time,
462            ),
463            EventPayload::PlayerPossession(event.clone()),
464            Some(event.player_id.clone()),
465            None,
466            Some(event.is_team_0),
467            None,
468            None,
469            None,
470        ));
471    }
472
473    for (index, event) in ball_half.events().iter().enumerate() {
474        events.push(make_event(
475            "ball_half",
476            index,
477            span(event.frame, event.end_frame, event.time, event.end_time),
478            EventPayload::BallHalf(event.clone()),
479            None,
480            None,
481            None,
482            None,
483            None,
484            None,
485        ));
486    }
487
488    for (index, event) in ball_third.events().iter().enumerate() {
489        events.push(make_event(
490            "ball_third",
491            index,
492            span(event.frame, event.end_frame, event.time, event.end_time),
493            EventPayload::BallThird(event.clone()),
494            None,
495            None,
496            None,
497            None,
498            None,
499            None,
500        ));
501    }
502
503    for (index, event) in territorial_pressure.events().iter().enumerate() {
504        events.push(make_event(
505            "territorial_pressure",
506            index,
507            span(
508                event.start_frame,
509                event.end_frame,
510                event.start_time,
511                event.end_time,
512            ),
513            EventPayload::TerritorialPressure(event.clone()),
514            None,
515            None,
516            Some(event.team_is_team_0),
517            None,
518            None,
519            None,
520        ));
521    }
522
523    for (index, event) in movement.events().iter().enumerate() {
524        events.push(make_event(
525            "movement",
526            index,
527            span(event.frame, event.end_frame, event.time, event.end_time),
528            EventPayload::Movement(event.clone()),
529            Some(event.player.clone()),
530            None,
531            Some(event.is_team_0),
532            event.player_position,
533            None,
534            None,
535        ));
536    }
537
538    for (index, event) in positioning.activity_events().iter().enumerate() {
539        events.push(make_event(
540            "player_activity",
541            index,
542            span(event.frame, event.end_frame, event.time, event.end_time),
543            EventPayload::PlayerActivity(event.clone()),
544            Some(event.player.clone()),
545            None,
546            Some(event.is_team_0),
547            event.player_position,
548            None,
549            None,
550        ));
551    }
552
553    for (index, event) in positioning.field_third_events().iter().enumerate() {
554        events.push(make_event(
555            "field_third",
556            index,
557            span(event.frame, event.end_frame, event.time, event.end_time),
558            EventPayload::FieldThird(event.clone()),
559            Some(event.player.clone()),
560            None,
561            Some(event.is_team_0),
562            event.player_position,
563            None,
564            None,
565        ));
566    }
567
568    for (index, event) in positioning.field_half_events().iter().enumerate() {
569        events.push(make_event(
570            "field_half",
571            index,
572            span(event.frame, event.end_frame, event.time, event.end_time),
573            EventPayload::FieldHalf(event.clone()),
574            Some(event.player.clone()),
575            None,
576            Some(event.is_team_0),
577            event.player_position,
578            None,
579            None,
580        ));
581    }
582
583    for (index, event) in positioning.ball_depth_events().iter().enumerate() {
584        events.push(make_event(
585            "ball_depth",
586            index,
587            span(event.frame, event.end_frame, event.time, event.end_time),
588            EventPayload::BallDepth(event.clone()),
589            Some(event.player.clone()),
590            None,
591            Some(event.is_team_0),
592            event.player_position,
593            None,
594            None,
595        ));
596    }
597
598    for (index, event) in positioning.depth_role_events().iter().enumerate() {
599        events.push(make_event(
600            "depth_role",
601            index,
602            span(event.frame, event.end_frame, event.time, event.end_time),
603            EventPayload::DepthRole(event.clone()),
604            Some(event.player.clone()),
605            None,
606            Some(event.is_team_0),
607            event.player_position,
608            None,
609            None,
610        ));
611    }
612
613    for (index, event) in positioning.ball_proximity_events().iter().enumerate() {
614        events.push(make_event(
615            "ball_proximity",
616            index,
617            span(event.frame, event.end_frame, event.time, event.end_time),
618            EventPayload::BallProximity(event.clone()),
619            Some(event.player.clone()),
620            None,
621            Some(event.is_team_0),
622            event.player_position,
623            None,
624            None,
625        ));
626    }
627
628    for (index, event) in positioning.shadow_defense_events().iter().enumerate() {
629        events.push(make_event(
630            "shadow_defense",
631            index,
632            span(event.frame, event.end_frame, event.time, event.end_time),
633            EventPayload::ShadowDefense(event.clone()),
634            Some(event.player.clone()),
635            None,
636            Some(event.is_team_0),
637            event.player_position,
638            None,
639            None,
640        ));
641    }
642
643    for (index, event) in rotation.role_events().iter().enumerate() {
644        events.push(make_event(
645            "rotation_role",
646            index,
647            span(event.frame, event.end_frame, event.time, event.end_time),
648            EventPayload::RotationRole(event.clone()),
649            Some(event.player.clone()),
650            None,
651            Some(event.is_team_0),
652            event.player_position,
653            None,
654            None,
655        ));
656    }
657
658    for (index, event) in rotation.first_man_change_events().iter().enumerate() {
659        events.push(make_event(
660            "first_man_change",
661            index,
662            moment(event.frame, event.time),
663            EventPayload::FirstManChange(event.clone()),
664            Some(event.next_first_man.clone()),
665            Some(event.previous_first_man.clone()),
666            Some(event.is_team_0),
667            None,
668            None,
669            None,
670        ));
671    }
672
673    for (index, event) in goal_context.iter().enumerate() {
674        events.push(make_event(
675            "goal_context",
676            index,
677            moment(event.frame, event.time),
678            EventPayload::GoalContext(event.clone()),
679            event.scorer.clone(),
680            None,
681            Some(event.scoring_team_is_team_0),
682            None,
683            event
684                .ball_position
685                .map(|position| [position.x, position.y, position.z]),
686            None,
687        ));
688    }
689
690    for (index, event) in backboard.events().iter().enumerate() {
691        events.push(make_event(
692            "backboard",
693            index,
694            moment(event.frame, event.time),
695            EventPayload::Backboard(event.clone()),
696            Some(event.player.clone()),
697            None,
698            Some(event.is_team_0),
699            event.player_position,
700            None,
701            None,
702        ));
703    }
704
705    for (index, event) in ball_carry.carry_events().iter().enumerate() {
706        events.push(make_event(
707            MECHANIC_BALL_CARRY,
708            index,
709            span(
710                event.start_frame,
711                event.end_frame,
712                event.start_time,
713                event.end_time,
714            ),
715            EventPayload::BallCarry(event.clone()),
716            Some(event.player_id.clone()),
717            None,
718            Some(event.is_team_0),
719            Some(event.end_position),
720            Some(event.end_position),
721            None,
722        ));
723    }
724
725    for (index, event) in air_dribble.events().iter().enumerate() {
726        events.push(make_event(
727            MECHANIC_AIR_DRIBBLE,
728            index,
729            span(
730                event.start_frame,
731                event.end_frame,
732                event.start_time,
733                event.end_time,
734            ),
735            EventPayload::BallCarry(event.clone()),
736            Some(event.player_id.clone()),
737            None,
738            Some(event.is_team_0),
739            Some(event.end_position),
740            Some(event.end_position),
741            None,
742        ));
743    }
744
745    for (index, event) in ceiling_shot.events().iter().enumerate() {
746        events.push(make_event(
747            MECHANIC_CEILING_SHOT,
748            index,
749            span(
750                event.ceiling_contact_frame,
751                event.frame,
752                event.ceiling_contact_time,
753                event.time,
754            ),
755            EventPayload::CeilingShot(event.clone()),
756            Some(event.player.clone()),
757            None,
758            Some(event.is_team_0),
759            event.player_position,
760            Some(event.touch_position),
761            Some(event.confidence),
762        ));
763    }
764
765    for (index, event) in wall_aerial.events().iter().enumerate() {
766        events.push(make_event(
767            MECHANIC_WALL_AERIAL,
768            index,
769            span(
770                event.wall_contact_frame,
771                event.frame,
772                event.wall_contact_time,
773                event.time,
774            ),
775            EventPayload::WallAerial(event.clone()),
776            Some(event.player.clone()),
777            None,
778            Some(event.is_team_0),
779            Some(event.player_position),
780            Some(event.ball_position),
781            Some(event.confidence),
782        ));
783    }
784
785    for (index, event) in wall_aerial_shot.events().iter().enumerate() {
786        events.push(make_event(
787            MECHANIC_WALL_AERIAL_SHOT,
788            index,
789            span(
790                event.takeoff_frame,
791                event.frame,
792                event.takeoff_time,
793                event.time,
794            ),
795            EventPayload::WallAerialShot(event.clone()),
796            Some(event.player.clone()),
797            None,
798            Some(event.is_team_0),
799            Some(event.player_position),
800            Some(event.ball_position),
801            Some(event.confidence),
802        ));
803    }
804
805    for (index, event) in center.events().iter().enumerate() {
806        events.push(make_event(
807            MECHANIC_CENTER,
808            index,
809            span(event.start_frame, event.frame, event.start_time, event.time),
810            EventPayload::Center(event.clone()),
811            Some(event.player.clone()),
812            None,
813            Some(event.is_team_0),
814            event.player_position,
815            Some(event.end_ball_position),
816            None,
817        ));
818    }
819
820    for (index, event) in dodge_reset.events().iter().enumerate() {
821        events.push(make_event(
822            "dodge_reset",
823            index,
824            moment(event.frame, event.time),
825            EventPayload::DodgeReset(event.clone()),
826            Some(event.player.clone()),
827            None,
828            Some(event.is_team_0),
829            event.player_position,
830            None,
831            None,
832        ));
833    }
834
835    for (index, event) in dodge_reset.confirmed_flip_reset_events().iter().enumerate() {
836        events.push(make_event(
837            MECHANIC_FLIP_RESET,
838            index,
839            span(event.reset_frame, event.frame, event.reset_time, event.time),
840            EventPayload::FlipReset(event.clone()),
841            Some(event.player.clone()),
842            None,
843            Some(event.is_team_0),
844            event.player_position,
845            None,
846            None,
847        ));
848    }
849
850    for (index, event) in double_tap.events().iter().enumerate() {
851        events.push(make_event(
852            MECHANIC_DOUBLE_TAP,
853            index,
854            span(
855                event.backboard_frame,
856                event.frame,
857                event.backboard_time,
858                event.time,
859            ),
860            EventPayload::DoubleTap(event.clone()),
861            Some(event.player.clone()),
862            None,
863            Some(event.is_team_0),
864            event.player_position,
865            None,
866            None,
867        ));
868    }
869
870    for (index, event) in one_timer.events().iter().enumerate() {
871        events.push(make_event(
872            MECHANIC_ONE_TIMER,
873            index,
874            span(
875                event.pass_start_frame,
876                event.frame,
877                event.pass_start_time,
878                event.time,
879            ),
880            EventPayload::OneTimer(event.clone()),
881            Some(event.player.clone()),
882            Some(event.passer.clone()),
883            Some(event.is_team_0),
884            event.player_position,
885            None,
886            None,
887        ));
888    }
889
890    for (index, event) in pass.events().iter().enumerate() {
891        events.push(make_event(
892            MECHANIC_PASS,
893            index,
894            span(event.start_frame, event.frame, event.start_time, event.time),
895            EventPayload::Pass(event.clone()),
896            Some(event.passer.clone()),
897            Some(event.receiver.clone()),
898            Some(event.is_team_0),
899            event.passer_position,
900            None,
901            None,
902        ));
903    }
904
905    for (index, event) in controlled_play.events().iter().enumerate() {
906        events.push(make_event(
907            "controlled_play",
908            index,
909            span(
910                event.start_frame,
911                event.end_frame,
912                event.start_time,
913                event.end_time,
914            ),
915            EventPayload::ControlledPlay(event.clone()),
916            Some(event.player_id.clone()),
917            None,
918            Some(event.is_team_0),
919            None,
920            None,
921            None,
922        ));
923    }
924
925    for (index, event) in fifty_fifty.events().iter().enumerate() {
926        events.push(make_event(
927            "fifty_fifty",
928            index,
929            span(
930                event.start_frame,
931                event.resolve_frame,
932                event.start_time,
933                event.resolve_time,
934            ),
935            EventPayload::FiftyFifty(event.clone()),
936            event
937                .team_zero_player
938                .clone()
939                .or_else(|| event.team_one_player.clone()),
940            None,
941            event.winning_team_is_team_0,
942            None,
943            Some(event.midpoint),
944            None,
945        ));
946    }
947
948    for (index, event) in kickoff.events().iter().enumerate() {
949        events.push(make_event(
950            "kickoff",
951            index,
952            span(
953                event.start_frame,
954                event.end_frame,
955                event.start_time,
956                event.end_time,
957            ),
958            EventPayload::Kickoff(Box::new(event.clone())),
959            event.first_touch_player.clone(),
960            None,
961            event.first_touch_team_is_team_0,
962            None,
963            None,
964            None,
965        ));
966    }
967
968    for (index, event) in rush.events().iter().enumerate() {
969        events.push(make_event(
970            "rush",
971            index,
972            span(
973                event.start_frame,
974                event.end_frame,
975                event.start_time,
976                event.end_time,
977            ),
978            EventPayload::Rush(event.clone()),
979            None,
980            None,
981            Some(event.is_team_0),
982            None,
983            None,
984            None,
985        ));
986    }
987
988    for (index, event) in flip_impulse.events().iter().enumerate() {
989        events.push(make_event(
990            "dodge",
991            index,
992            span(
993                event.frame,
994                event.resolved_frame,
995                event.time,
996                event.resolved_time,
997            ),
998            EventPayload::Dodge(event.clone()),
999            Some(event.player.clone()),
1000            None,
1001            Some(event.is_team_0),
1002            event
1003                .dodge_impulse
1004                .as_ref()
1005                .map(|dodge_impulse| dodge_impulse.end_position),
1006            None,
1007            event
1008                .dodge_impulse
1009                .as_ref()
1010                .map(|dodge_impulse| dodge_impulse.confidence),
1011        ));
1012    }
1013
1014    for (index, event) in speed_flip.events().iter().enumerate() {
1015        events.push(make_event(
1016            MECHANIC_SPEED_FLIP,
1017            index,
1018            span(
1019                event.frame,
1020                event.resolved_frame,
1021                event.time,
1022                event.resolved_time,
1023            ),
1024            EventPayload::SpeedFlip(event.clone()),
1025            Some(event.player.clone()),
1026            None,
1027            Some(event.is_team_0),
1028            Some(event.end_position),
1029            None,
1030            Some(event.confidence),
1031        ));
1032    }
1033
1034    for (index, event) in half_flip.events().iter().enumerate() {
1035        events.push(make_event(
1036            MECHANIC_HALF_FLIP,
1037            index,
1038            moment(event.frame, event.time),
1039            EventPayload::HalfFlip(event.clone()),
1040            Some(event.player.clone()),
1041            None,
1042            Some(event.is_team_0),
1043            Some(event.end_position),
1044            None,
1045            Some(event.confidence),
1046        ));
1047    }
1048
1049    for (index, event) in half_volley.events().iter().enumerate() {
1050        events.push(make_event(
1051            MECHANIC_HALF_VOLLEY,
1052            index,
1053            moment(event.frame, event.time),
1054            EventPayload::HalfVolley(event.clone()),
1055            Some(event.player.clone()),
1056            None,
1057            Some(event.is_team_0),
1058            event.player_position,
1059            None,
1060            None,
1061        ));
1062    }
1063
1064    for (index, event) in wavedash.events().iter().enumerate() {
1065        events.push(make_event(
1066            MECHANIC_WAVEDASH,
1067            index,
1068            span(event.dodge_frame, event.frame, event.dodge_time, event.time),
1069            EventPayload::Wavedash(event.clone()),
1070            Some(event.player.clone()),
1071            None,
1072            Some(event.is_team_0),
1073            Some(event.landing_position),
1074            None,
1075            Some(event.confidence),
1076        ));
1077    }
1078
1079    for (index, event) in whiff.events().iter().enumerate() {
1080        events.push(make_event(
1081            "whiff",
1082            index,
1083            span(
1084                event.frame,
1085                event.resolved_frame,
1086                event.time,
1087                event.resolved_time,
1088            ),
1089            EventPayload::Whiff(event.clone()),
1090            Some(event.player.clone()),
1091            None,
1092            Some(event.is_team_0),
1093            event.player_position,
1094            None,
1095            None,
1096        ));
1097    }
1098
1099    for (index, event) in powerslide.events().iter().enumerate() {
1100        events.push(make_event(
1101            "powerslide",
1102            index,
1103            moment(event.frame, event.time),
1104            EventPayload::Powerslide(event.clone()),
1105            Some(event.player.clone()),
1106            None,
1107            Some(event.is_team_0),
1108            event.player_position,
1109            None,
1110            None,
1111        ));
1112    }
1113
1114    for (index, event) in touch.events().iter().enumerate() {
1115        let timing =
1116            event
1117                .ball_movement
1118                .as_ref()
1119                .map_or(moment(event.frame, event.time), |movement| {
1120                    span(
1121                        movement.start_frame,
1122                        movement.end_frame,
1123                        movement.start_time,
1124                        movement.end_time,
1125                    )
1126                });
1127        events.push(make_event(
1128            "touch",
1129            index,
1130            timing,
1131            EventPayload::Touch(event.clone()),
1132            Some(event.player.clone()),
1133            None,
1134            Some(event.is_team_0),
1135            event.player_position,
1136            None,
1137            None,
1138        ));
1139    }
1140
1141    for (index, event) in boost.pickup_events().iter().enumerate() {
1142        events.push(make_event(
1143            "boost_pickups",
1144            index,
1145            moment(event.frame, event.time),
1146            EventPayload::BoostPickup(event.clone()),
1147            Some(event.player_id.clone()),
1148            None,
1149            Some(event.is_team_0),
1150            event.player_position,
1151            None,
1152            None,
1153        ));
1154    }
1155
1156    for (index, event) in boost.respawn_events().iter().enumerate() {
1157        events.push(make_event(
1158            "boost_respawn",
1159            index,
1160            moment(event.frame, event.time),
1161            EventPayload::Respawn(event.clone()),
1162            Some(event.player_id.clone()),
1163            None,
1164            Some(event.is_team_0),
1165            event.player_position,
1166            None,
1167            None,
1168        ));
1169    }
1170
1171    for (index, event) in bump.events().iter().enumerate() {
1172        events.push(make_event(
1173            "bump",
1174            index,
1175            moment(event.frame, event.time),
1176            EventPayload::Bump(event.clone()),
1177            Some(event.initiator.clone()),
1178            Some(event.victim.clone()),
1179            Some(event.initiator_is_team_0),
1180            Some(event.initiator_position),
1181            None,
1182            Some(event.confidence),
1183        ));
1184    }
1185
1186    for (index, event) in demo.events().iter().enumerate() {
1187        events.push(make_event(
1188            "demolition",
1189            index,
1190            moment(event.frame, event.time),
1191            EventPayload::Demolition(event.clone()),
1192            Some(event.attacker.clone()),
1193            Some(event.victim.clone()),
1194            event.attacker_is_team_0,
1195            event.attacker_position,
1196            None,
1197            None,
1198        ));
1199    }
1200
1201    for (index, event) in flick.events().iter().enumerate() {
1202        events.push(make_event(
1203            MECHANIC_FLICK,
1204            index,
1205            span(
1206                event.setup_start_frame,
1207                event.frame,
1208                event.setup_start_time,
1209                event.time,
1210            ),
1211            EventPayload::Flick(event.clone()),
1212            Some(event.player.clone()),
1213            None,
1214            Some(event.is_team_0),
1215            event.player_position,
1216            None,
1217            Some(event.confidence),
1218        ));
1219    }
1220
1221    events.sort_by(|left, right| {
1222        event_start_time(left)
1223            .total_cmp(&event_start_time(right))
1224            .then_with(|| left.meta.stream.cmp(&right.meta.stream))
1225            .then_with(|| left.meta.id.cmp(&right.meta.id))
1226    });
1227    events
1228}