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 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}