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