Skip to main content

subtr_actor/stats/analysis_graph/nodes/
stats_timeline_events.rs

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