Skip to main content

subtr_actor/collector/stats/
playback.rs

1use boxcars::{Ps4Id, PsyNetId, RemoteId, SwitchId};
2use serde::de::DeserializeOwned;
3use serde::Serialize;
4use serde_json::{Map, Value};
5
6use crate::*;
7
8use super::types::serialize_to_json_value;
9
10#[derive(Debug, Clone, PartialEq, Serialize)]
11pub struct CapturedStatsFrame<Modules> {
12    pub frame_number: usize,
13    pub time: f32,
14    pub dt: f32,
15    pub seconds_remaining: Option<i32>,
16    pub game_state: Option<i32>,
17    pub gameplay_phase: GameplayPhase,
18    pub is_live_play: bool,
19    pub modules: Modules,
20}
21
22pub type StatsSnapshotFrame = CapturedStatsFrame<Map<String, Value>>;
23
24#[derive(Debug, Clone, PartialEq, Serialize)]
25pub struct CapturedStatsData<Frame> {
26    pub replay_meta: ReplayMeta,
27    pub config: Map<String, Value>,
28    pub modules: Map<String, Value>,
29    pub frames: Vec<Frame>,
30}
31
32pub type StatsSnapshotData = CapturedStatsData<StatsSnapshotFrame>;
33
34impl<Modules> CapturedStatsFrame<Modules> {
35    pub fn map_modules<Mapped, F>(
36        self,
37        transform: F,
38    ) -> SubtrActorResult<CapturedStatsFrame<Mapped>>
39    where
40        F: FnOnce(Modules) -> SubtrActorResult<Mapped>,
41    {
42        Ok(CapturedStatsFrame {
43            frame_number: self.frame_number,
44            time: self.time,
45            dt: self.dt,
46            seconds_remaining: self.seconds_remaining,
47            game_state: self.game_state,
48            gameplay_phase: self.gameplay_phase,
49            is_live_play: self.is_live_play,
50            modules: transform(self.modules)?,
51        })
52    }
53}
54
55impl CapturedStatsData<StatsSnapshotFrame> {
56    pub fn into_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
57        self.to_stats_timeline()
58    }
59
60    pub fn into_stats_timeline_with_progress<F>(
61        self,
62        frame_interval: usize,
63        mut on_progress: F,
64    ) -> SubtrActorResult<ReplayStatsTimeline>
65    where
66        F: FnMut(usize, usize) -> SubtrActorResult<()>,
67    {
68        let frame_interval = frame_interval.max(1);
69        let total_frames = self.frames.len();
70        on_progress(0, total_frames)?;
71        let frames = self
72            .frames
73            .iter()
74            .enumerate()
75            .map(|(frame_index, frame)| {
76                let replay_frame = self.replay_stats_frame(frame)?;
77                let processed_frames = frame_index + 1;
78                if processed_frames == total_frames
79                    || processed_frames.is_multiple_of(frame_interval)
80                {
81                    on_progress(processed_frames, total_frames)?;
82                }
83                Ok(replay_frame)
84            })
85            .collect::<SubtrActorResult<Vec<_>>>()?;
86        self.to_replay_stats_timeline_with_frames(frames)
87    }
88
89    pub fn to_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
90        self.to_replay_stats_timeline_with_frames(
91            self.frames
92                .iter()
93                .map(|frame| self.replay_stats_frame(frame))
94                .collect::<SubtrActorResult<Vec<_>>>()?,
95        )
96    }
97
98    pub(crate) fn into_replay_stats_timeline_with_frames(
99        self,
100        frames: Vec<ReplayStatsFrame>,
101    ) -> SubtrActorResult<ReplayStatsTimeline> {
102        self.to_replay_stats_timeline_with_frames(frames)
103    }
104
105    fn to_replay_stats_timeline_with_frames(
106        &self,
107        frames: Vec<ReplayStatsFrame>,
108    ) -> SubtrActorResult<ReplayStatsTimeline> {
109        Ok(ReplayStatsTimeline {
110            config: self.timeline_config(),
111            replay_meta: self.replay_meta.clone(),
112            events: self.timeline_event_sets_typed()?,
113            frames,
114        })
115    }
116
117    pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
118        self.to_stats_timeline_value()
119    }
120
121    pub fn to_stats_timeline_value(&self) -> SubtrActorResult<Value> {
122        let mut timeline = Map::new();
123        timeline.insert("config".to_owned(), self.timeline_config_value()?);
124        timeline.insert(
125            "replay_meta".to_owned(),
126            serialize_to_json_value(&self.replay_meta)?,
127        );
128        timeline.insert("events".to_owned(), self.timeline_event_sets_value());
129        timeline.insert(
130            "frames".to_owned(),
131            Value::Array(
132                self.frames
133                    .iter()
134                    .map(|frame| self.timeline_frame_value(frame))
135                    .collect::<SubtrActorResult<Vec<_>>>()?,
136            ),
137        );
138        Ok(Value::Object(timeline))
139    }
140
141    fn timeline_events(&self) -> Vec<Value> {
142        let mut events = self.module_array("core", "timeline");
143        events.extend(self.module_array("demo", "timeline"));
144        events.sort_by(|left, right| {
145            let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
146            let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
147            left_time.total_cmp(&right_time)
148        });
149        events
150    }
151
152    fn timeline_events_typed(&self) -> SubtrActorResult<Vec<TimelineEvent>> {
153        self.timeline_events()
154            .iter()
155            .map(parse_timeline_event)
156            .collect()
157    }
158
159    fn goal_tag_events_typed(&self) -> SubtrActorResult<Vec<GoalTagEvent>> {
160        let mut events = Vec::new();
161        for module_name in [
162            "aerial_goal",
163            "high_aerial_goal",
164            "long_distance_goal",
165            "own_half_goal",
166            "empty_net_goal",
167            "flick_goal",
168            "one_timer_goal",
169            "air_dribble_goal",
170            "flip_reset_goal",
171        ] {
172            events.extend(self.module_player_events(
173                module_name,
174                "events",
175                parse_goal_tag_event,
176            )?);
177        }
178        events.sort_by(|left, right| {
179            left.time
180                .total_cmp(&right.time)
181                .then_with(|| left.frame.cmp(&right.frame))
182                .then_with(|| left.goal_index.cmp(&right.goal_index))
183                .then_with(|| format!("{:?}", left.kind).cmp(&format!("{:?}", right.kind)))
184        });
185        Ok(events)
186    }
187
188    fn mechanic_events_typed(&self) -> SubtrActorResult<Vec<MechanicEvent>> {
189        let mut events = Vec::new();
190
191        for (index, value) in self.module_array("ball_carry", "events").iter().enumerate() {
192            events.push(parse_ball_carry_mechanic_event(value, index)?);
193        }
194        for (index, value) in self
195            .module_array("ceiling_shot", "events")
196            .iter()
197            .enumerate()
198        {
199            let event = parse_ceiling_shot_event(value)?;
200            events.push(span_mechanic_event(
201                "ceiling_shot",
202                index,
203                event.ceiling_contact_frame,
204                event.frame,
205                event.ceiling_contact_time,
206                event.time,
207                event.player,
208                event.is_team_0,
209            ));
210        }
211        for (index, value) in self
212            .module_array("dodge_reset", "events")
213            .iter()
214            .enumerate()
215        {
216            events.push(parse_dodge_reset_mechanic_event(value, index)?);
217        }
218        for (index, value) in self.module_array("double_tap", "events").iter().enumerate() {
219            let event = parse_double_tap_event(value)?;
220            events.push(span_mechanic_event(
221                "double_tap",
222                index,
223                event.backboard_frame,
224                event.frame,
225                event.backboard_time,
226                event.time,
227                event.player,
228                event.is_team_0,
229            ));
230        }
231        for (index, value) in self.module_array("flick", "events").iter().enumerate() {
232            events.push(parse_flick_mechanic_event(value, index)?);
233        }
234        for (index, value) in self
235            .module_array("musty_flick", "events")
236            .iter()
237            .enumerate()
238        {
239            events.push(parse_musty_flick_mechanic_event(value, index)?);
240        }
241        for (index, value) in self.module_array("one_timer", "events").iter().enumerate() {
242            let event = parse_one_timer_event(value)?;
243            events.push(span_mechanic_event(
244                "one_timer",
245                index,
246                event.pass_start_frame,
247                event.frame,
248                event.pass_start_time,
249                event.time,
250                event.player,
251                event.is_team_0,
252            ));
253        }
254        for (index, value) in self.module_array("pass", "events").iter().enumerate() {
255            let event = parse_pass_event(value)?;
256            events.push(span_mechanic_event(
257                "pass",
258                index,
259                event.start_frame,
260                event.frame,
261                event.start_time,
262                event.time,
263                event.passer,
264                event.is_team_0,
265            ));
266        }
267        for (index, value) in self.module_array("speed_flip", "events").iter().enumerate() {
268            let event = parse_speed_flip_event(value)?;
269            events.push(moment_mechanic_event(
270                "speed_flip",
271                index,
272                event.frame,
273                event.time,
274                event.player,
275                event.is_team_0,
276            ));
277        }
278        for (index, value) in self.module_array("half_flip", "events").iter().enumerate() {
279            let event = parse_half_flip_event(value)?;
280            events.push(moment_mechanic_event(
281                "half_flip",
282                index,
283                event.frame,
284                event.time,
285                event.player,
286                event.is_team_0,
287            ));
288        }
289        for (index, value) in self.module_array("wavedash", "events").iter().enumerate() {
290            let event = parse_wavedash_event(value)?;
291            events.push(span_mechanic_event(
292                "wavedash",
293                index,
294                event.dodge_frame,
295                event.frame,
296                event.dodge_time,
297                event.time,
298                event.player,
299                event.is_team_0,
300            ));
301        }
302        for (index, value) in self.module_array("whiff", "events").iter().enumerate() {
303            let event = parse_whiff_event(value)?;
304            events.push(moment_mechanic_event(
305                "whiff",
306                index,
307                event.frame,
308                event.time,
309                event.player,
310                event.is_team_0,
311            ));
312        }
313
314        events.sort_by(|left, right| {
315            let left_time = mechanic_event_start_time(left);
316            let right_time = mechanic_event_start_time(right);
317            left_time
318                .total_cmp(&right_time)
319                .then_with(|| left.kind.cmp(&right.kind))
320                .then_with(|| left.id.cmp(&right.id))
321        });
322        Ok(events)
323    }
324
325    fn goal_tag_events_value(&self) -> Vec<Value> {
326        let mut events = Vec::new();
327        for module_name in [
328            "aerial_goal",
329            "high_aerial_goal",
330            "long_distance_goal",
331            "own_half_goal",
332            "empty_net_goal",
333            "flick_goal",
334            "one_timer_goal",
335            "air_dribble_goal",
336            "flip_reset_goal",
337        ] {
338            events.extend(self.module_array(module_name, "events"));
339        }
340        events.sort_by(|left, right| {
341            let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
342            let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
343            left_time.total_cmp(&right_time)
344        });
345        events
346    }
347
348    fn timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
349        Ok(ReplayStatsTimelineEvents {
350            timeline: self.timeline_events_typed()?,
351            mechanics: self.mechanic_events_typed()?,
352            goal_context: self.module_player_events(
353                "core",
354                "goal_context",
355                parse_goal_context_event,
356            )?,
357            backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
358            ceiling_shot: self.module_player_events(
359                "ceiling_shot",
360                "events",
361                parse_ceiling_shot_event,
362            )?,
363            double_tap: self.module_player_events(
364                "double_tap",
365                "events",
366                parse_double_tap_event,
367            )?,
368            one_timer: self.module_player_events("one_timer", "events", parse_one_timer_event)?,
369            fifty_fifty: self.module_player_events(
370                "fifty_fifty",
371                "events",
372                parse_fifty_fifty_event,
373            )?,
374            pass: self.module_player_events("pass", "events", parse_pass_event)?,
375            goal_tags: self.goal_tag_events_typed()?,
376            rush: self.module_typed_array("rush", "events")?,
377            speed_flip: self.module_player_events(
378                "speed_flip",
379                "events",
380                parse_speed_flip_event,
381            )?,
382            half_flip: self.module_player_events("half_flip", "events", parse_half_flip_event)?,
383            wavedash: self.module_player_events("wavedash", "events", parse_wavedash_event)?,
384            whiff: self.module_player_events("whiff", "events", parse_whiff_event)?,
385            boost_pickups: self.module_player_events(
386                "boost",
387                "events",
388                parse_boost_pickup_comparison_event,
389            )?,
390        })
391    }
392
393    fn timeline_event_sets_value(&self) -> Value {
394        let mut events = Map::new();
395        events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
396        events.insert("mechanics".to_owned(), Value::Array(Vec::new()));
397        events.insert(
398            "backboard".to_owned(),
399            Value::Array(self.module_array("backboard", "events")),
400        );
401        events.insert(
402            "ceiling_shot".to_owned(),
403            Value::Array(self.module_array("ceiling_shot", "events")),
404        );
405        events.insert(
406            "double_tap".to_owned(),
407            Value::Array(self.module_array("double_tap", "events")),
408        );
409        events.insert(
410            "one_timer".to_owned(),
411            Value::Array(self.module_array("one_timer", "events")),
412        );
413        events.insert(
414            "pass".to_owned(),
415            Value::Array(self.module_array("pass", "events")),
416        );
417        events.insert(
418            "goal_tags".to_owned(),
419            Value::Array(self.goal_tag_events_value()),
420        );
421        events.insert(
422            "fifty_fifty".to_owned(),
423            Value::Array(self.module_array("fifty_fifty", "events")),
424        );
425        events.insert(
426            "rush".to_owned(),
427            Value::Array(self.module_array("rush", "events")),
428        );
429        events.insert(
430            "speed_flip".to_owned(),
431            Value::Array(self.module_array("speed_flip", "events")),
432        );
433        events.insert(
434            "half_flip".to_owned(),
435            Value::Array(self.module_array("half_flip", "events")),
436        );
437        events.insert(
438            "wavedash".to_owned(),
439            Value::Array(self.module_array("wavedash", "events")),
440        );
441        events.insert(
442            "whiff".to_owned(),
443            Value::Array(self.module_array("whiff", "events")),
444        );
445        events.insert(
446            "boost_pickups".to_owned(),
447            Value::Array(self.module_array("boost", "events")),
448        );
449        Value::Object(events)
450    }
451
452    fn timeline_config(&self) -> StatsTimelineConfig {
453        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
454        let pressure_config = self.config.get("pressure").and_then(Value::as_object);
455        let rush_config = self.config.get("rush").and_then(Value::as_object);
456        let rush_defaults = RushCalculatorConfig::default();
457        let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
458        let high_aerial_goal_config = self
459            .config
460            .get("high_aerial_goal")
461            .and_then(Value::as_object);
462        let long_distance_goal_config = self
463            .config
464            .get("long_distance_goal")
465            .and_then(Value::as_object);
466        let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
467        let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
468        let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
469        let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
470        let air_dribble_goal_config = self
471            .config
472            .get("air_dribble_goal")
473            .and_then(Value::as_object);
474        let flip_reset_goal_config = self
475            .config
476            .get("flip_reset_goal")
477            .and_then(Value::as_object);
478
479        StatsTimelineConfig {
480            most_back_forward_threshold_y: positioning_config
481                .and_then(|config| config.get("most_back_forward_threshold_y"))
482                .and_then(json_f32)
483                .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
484            level_ball_depth_margin: positioning_config
485                .and_then(|config| config.get("level_ball_depth_margin"))
486                .and_then(json_f32)
487                .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
488            pressure_neutral_zone_half_width_y: pressure_config
489                .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
490                .and_then(json_f32)
491                .unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
492            rush_max_start_y: rush_config
493                .and_then(|config| config.get("rush_max_start_y"))
494                .and_then(json_f32)
495                .unwrap_or(rush_defaults.max_start_y),
496            rush_attack_support_distance_y: rush_config
497                .and_then(|config| config.get("rush_attack_support_distance_y"))
498                .and_then(json_f32)
499                .unwrap_or(rush_defaults.attack_support_distance_y),
500            rush_defender_distance_y: rush_config
501                .and_then(|config| config.get("rush_defender_distance_y"))
502                .and_then(json_f32)
503                .unwrap_or(rush_defaults.defender_distance_y),
504            rush_min_possession_retained_seconds: rush_config
505                .and_then(|config| config.get("rush_min_possession_retained_seconds"))
506                .and_then(json_f32)
507                .unwrap_or(rush_defaults.min_possession_retained_seconds),
508            aerial_goal_min_ball_z: aerial_goal_config
509                .and_then(|config| config.get("aerial_goal_min_ball_z"))
510                .and_then(json_f32)
511                .unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
512            high_aerial_goal_min_ball_z: high_aerial_goal_config
513                .and_then(|config| config.get("high_aerial_goal_min_ball_z"))
514                .and_then(json_f32)
515                .unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
516            long_distance_goal_max_attacking_y: long_distance_goal_config
517                .and_then(|config| config.get("long_distance_goal_max_attacking_y"))
518                .and_then(json_f32)
519                .unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
520            own_half_goal_max_attacking_y: own_half_goal_config
521                .and_then(|config| config.get("own_half_goal_max_attacking_y"))
522                .and_then(json_f32)
523                .unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
524            empty_net_min_defender_y_margin: empty_net_goal_config
525                .and_then(|config| config.get("empty_net_min_defender_y_margin"))
526                .and_then(json_f32)
527                .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
528            empty_net_min_defender_distance: empty_net_goal_config
529                .and_then(|config| config.get("empty_net_min_defender_distance"))
530                .and_then(json_f32)
531                .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
532            empty_net_max_touch_attacking_y: empty_net_goal_config
533                .and_then(|config| config.get("empty_net_max_touch_attacking_y"))
534                .and_then(json_f32)
535                .unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
536            flick_goal_max_event_to_goal_seconds: json_config_f32(
537                flick_goal_config,
538                "flick_goal_max_event_to_goal_seconds",
539                "flick_goal_max_event_to_touch_seconds",
540            )
541            .unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
542            one_timer_goal_max_event_to_goal_seconds: json_config_f32(
543                one_timer_goal_config,
544                "one_timer_goal_max_event_to_goal_seconds",
545                "one_timer_goal_max_event_to_touch_seconds",
546            )
547            .unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
548            air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
549                air_dribble_goal_config,
550                "air_dribble_goal_max_end_to_goal_seconds",
551                "air_dribble_goal_max_end_to_touch_seconds",
552            )
553            .unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
554            flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
555                flip_reset_goal_config,
556                "flip_reset_goal_max_event_to_goal_seconds",
557                "flip_reset_goal_max_event_to_touch_seconds",
558            )
559            .unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
560        }
561    }
562
563    fn timeline_config_value(&self) -> SubtrActorResult<Value> {
564        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
565        let pressure_config = self.config.get("pressure").and_then(Value::as_object);
566        let rush_config = self.config.get("rush").and_then(Value::as_object);
567        let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
568        let high_aerial_goal_config = self
569            .config
570            .get("high_aerial_goal")
571            .and_then(Value::as_object);
572        let long_distance_goal_config = self
573            .config
574            .get("long_distance_goal")
575            .and_then(Value::as_object);
576        let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
577        let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
578        let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
579        let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
580        let air_dribble_goal_config = self
581            .config
582            .get("air_dribble_goal")
583            .and_then(Value::as_object);
584        let flip_reset_goal_config = self
585            .config
586            .get("flip_reset_goal")
587            .and_then(Value::as_object);
588
589        let mut config = Map::new();
590        config.insert(
591            "most_back_forward_threshold_y".to_owned(),
592            serialize_to_json_value(
593                &positioning_config
594                    .and_then(|config| config.get("most_back_forward_threshold_y"))
595                    .and_then(Value::as_f64)
596                    .unwrap_or(
597                        PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
598                    ),
599            )?,
600        );
601        config.insert(
602            "level_ball_depth_margin".to_owned(),
603            serialize_to_json_value(
604                &positioning_config
605                    .and_then(|config| config.get("level_ball_depth_margin"))
606                    .and_then(Value::as_f64)
607                    .unwrap_or(
608                        PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
609                    ),
610            )?,
611        );
612        config.insert(
613            "pressure_neutral_zone_half_width_y".to_owned(),
614            serialize_to_json_value(
615                &pressure_config
616                    .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
617                    .and_then(Value::as_f64)
618                    .unwrap_or(
619                        PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
620                    ),
621            )?,
622        );
623        let rush_defaults = RushCalculatorConfig::default();
624        config.insert(
625            "rush_max_start_y".to_owned(),
626            serialize_to_json_value(
627                &rush_config
628                    .and_then(|config| config.get("rush_max_start_y"))
629                    .and_then(Value::as_f64)
630                    .unwrap_or(rush_defaults.max_start_y as f64),
631            )?,
632        );
633        config.insert(
634            "rush_attack_support_distance_y".to_owned(),
635            serialize_to_json_value(
636                &rush_config
637                    .and_then(|config| config.get("rush_attack_support_distance_y"))
638                    .and_then(Value::as_f64)
639                    .unwrap_or(rush_defaults.attack_support_distance_y as f64),
640            )?,
641        );
642        config.insert(
643            "rush_defender_distance_y".to_owned(),
644            serialize_to_json_value(
645                &rush_config
646                    .and_then(|config| config.get("rush_defender_distance_y"))
647                    .and_then(Value::as_f64)
648                    .unwrap_or(rush_defaults.defender_distance_y as f64),
649            )?,
650        );
651        config.insert(
652            "rush_min_possession_retained_seconds".to_owned(),
653            serialize_to_json_value(
654                &rush_config
655                    .and_then(|config| config.get("rush_min_possession_retained_seconds"))
656                    .and_then(Value::as_f64)
657                    .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
658            )?,
659        );
660        for (module_config, key, default_value) in [
661            (
662                aerial_goal_config,
663                "aerial_goal_min_ball_z",
664                AerialGoalCalculatorConfig::default().min_ball_z,
665            ),
666            (
667                high_aerial_goal_config,
668                "high_aerial_goal_min_ball_z",
669                HighAerialGoalCalculatorConfig::default().min_ball_z,
670            ),
671            (
672                long_distance_goal_config,
673                "long_distance_goal_max_attacking_y",
674                LongDistanceGoalCalculatorConfig::default().max_attacking_y,
675            ),
676            (
677                own_half_goal_config,
678                "own_half_goal_max_attacking_y",
679                OwnHalfGoalCalculatorConfig::default().max_attacking_y,
680            ),
681            (
682                empty_net_goal_config,
683                "empty_net_min_defender_y_margin",
684                EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
685            ),
686            (
687                empty_net_goal_config,
688                "empty_net_min_defender_distance",
689                EmptyNetGoalCalculatorConfig::default().min_defender_distance,
690            ),
691            (
692                empty_net_goal_config,
693                "empty_net_max_touch_attacking_y",
694                EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
695            ),
696            (
697                flick_goal_config,
698                "flick_goal_max_event_to_goal_seconds",
699                FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
700            ),
701            (
702                one_timer_goal_config,
703                "one_timer_goal_max_event_to_goal_seconds",
704                OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
705            ),
706            (
707                air_dribble_goal_config,
708                "air_dribble_goal_max_end_to_goal_seconds",
709                AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
710            ),
711            (
712                flip_reset_goal_config,
713                "flip_reset_goal_max_event_to_goal_seconds",
714                FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
715            ),
716        ] {
717            config.insert(
718                key.to_owned(),
719                serialize_to_json_value(
720                    &module_config
721                        .and_then(|config| config.get(key))
722                        .and_then(Value::as_f64)
723                        .unwrap_or(default_value as f64),
724                )?,
725            );
726        }
727        Ok(Value::Object(config))
728    }
729
730    fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
731        let mut timeline = Map::new();
732        timeline.insert(
733            "frame_number".to_owned(),
734            serialize_to_json_value(&frame.frame_number)?,
735        );
736        timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
737        timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
738        timeline.insert(
739            "seconds_remaining".to_owned(),
740            serialize_to_json_value(&frame.seconds_remaining)?,
741        );
742        timeline.insert(
743            "game_state".to_owned(),
744            serialize_to_json_value(&frame.game_state)?,
745        );
746        timeline.insert(
747            "gameplay_phase".to_owned(),
748            serialize_to_json_value(&frame.gameplay_phase)?,
749        );
750        timeline.insert(
751            "is_live_play".to_owned(),
752            serialize_to_json_value(&frame.is_live_play)?,
753        );
754        timeline.insert(
755            "fifty_fifty".to_owned(),
756            self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
757        );
758        timeline.insert(
759            "possession".to_owned(),
760            self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
761        );
762        timeline.insert(
763            "pressure".to_owned(),
764            self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
765        );
766        timeline.insert(
767            "rush".to_owned(),
768            self.frame_stats_or_default::<RushStats>(frame, "rush"),
769        );
770        timeline.insert(
771            "team_zero".to_owned(),
772            self.timeline_team_value(frame, "team_zero")?,
773        );
774        timeline.insert(
775            "team_one".to_owned(),
776            self.timeline_team_value(frame, "team_one")?,
777        );
778        timeline.insert(
779            "players".to_owned(),
780            Value::Array(
781                self.replay_meta
782                    .player_order()
783                    .map(|player| self.timeline_player_value(frame, player))
784                    .collect::<SubtrActorResult<Vec<_>>>()?,
785            ),
786        );
787        Ok(Value::Object(timeline))
788    }
789
790    pub(crate) fn replay_stats_frame(
791        &self,
792        frame: &StatsSnapshotFrame,
793    ) -> SubtrActorResult<ReplayStatsFrame> {
794        Ok(ReplayStatsFrame {
795            frame_number: frame.frame_number,
796            time: frame.time,
797            dt: frame.dt,
798            seconds_remaining: frame.seconds_remaining,
799            game_state: frame.game_state,
800            gameplay_phase: frame.gameplay_phase,
801            is_live_play: frame.is_live_play,
802            team_zero: self.replay_team_stats(frame, "team_zero")?,
803            team_one: self.replay_team_stats(frame, "team_one")?,
804            players: self
805                .replay_meta
806                .player_order()
807                .map(|player| self.replay_player_stats(frame, player))
808                .collect::<SubtrActorResult<Vec<_>>>()?,
809        })
810    }
811
812    fn replay_team_stats(
813        &self,
814        frame: &StatsSnapshotFrame,
815        team_key: &str,
816    ) -> SubtrActorResult<TeamStatsSnapshot> {
817        let is_team_zero = team_key == "team_zero";
818        Ok(TeamStatsSnapshot {
819            fifty_fifty: self
820                .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
821                .for_team(is_team_zero),
822            possession: self
823                .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
824                .for_team(is_team_zero),
825            pressure: self
826                .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
827                .for_team(is_team_zero),
828            rush: self
829                .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
830                .for_team(is_team_zero),
831            core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
832            backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
833            double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
834            one_timer: self.frame_team_stat_or_default_typed(frame, "one_timer", team_key)?,
835            pass: self.frame_team_stat_or_default_typed(frame, "pass", team_key)?,
836            ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
837            air_dribble: self.frame_team_stat_or_default_typed(frame, "air_dribble", team_key)?,
838            boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
839            movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
840            powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
841            demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
842        })
843    }
844
845    fn replay_player_stats(
846        &self,
847        frame: &StatsSnapshotFrame,
848        player: &PlayerInfo,
849    ) -> SubtrActorResult<PlayerStatsSnapshot> {
850        let player_key = player_info_key(player)?;
851        Ok(PlayerStatsSnapshot {
852            player_id: player.remote_id.clone(),
853            name: player.name.clone(),
854            is_team_0: self.is_team_zero_player(player),
855            core: self.frame_core_player_stat_or_default_by_key(frame, &player_key)?,
856            backboard: self.frame_player_stat_or_default_typed_by_key(
857                frame,
858                "backboard",
859                &player_key,
860            )?,
861            ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
862                frame,
863                "ceiling_shot",
864                &player_key,
865            )?,
866            double_tap: self.frame_player_stat_or_default_typed_by_key(
867                frame,
868                "double_tap",
869                &player_key,
870            )?,
871            one_timer: self.frame_player_stat_or_default_typed_by_key(
872                frame,
873                "one_timer",
874                &player_key,
875            )?,
876            pass: self.frame_player_stat_or_default_typed_by_key(frame, "pass", &player_key)?,
877            fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
878                frame,
879                "fifty_fifty",
880                &player_key,
881            )?,
882            speed_flip: self.frame_player_stat_or_default_typed_by_key(
883                frame,
884                "speed_flip",
885                &player_key,
886            )?,
887            half_flip: self.frame_player_stat_or_default_typed_by_key(
888                frame,
889                "half_flip",
890                &player_key,
891            )?,
892            wavedash: self.frame_player_stat_or_default_typed_by_key(
893                frame,
894                "wavedash",
895                &player_key,
896            )?,
897            touch: if frame.modules.contains_key("touch") {
898                self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
899                    TouchStats::default().with_complete_labeled_touch_counts()
900                })?
901            } else {
902                self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
903            },
904            whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
905            flick: self.frame_player_stat_or_default_typed_by_key(frame, "flick", &player_key)?,
906            musty_flick: self.frame_player_stat_or_default_typed_by_key(
907                frame,
908                "musty_flick",
909                &player_key,
910            )?,
911            dodge_reset: self.frame_player_stat_or_default_typed_by_key(
912                frame,
913                "dodge_reset",
914                &player_key,
915            )?,
916            ball_carry: self.frame_player_stat_or_default_typed_by_key(
917                frame,
918                "ball_carry",
919                &player_key,
920            )?,
921            air_dribble: self.frame_player_stat_or_default_typed_by_key(
922                frame,
923                "air_dribble",
924                &player_key,
925            )?,
926            boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
927            movement: self.frame_player_stat_or_default_with_by_key(
928                frame,
929                "movement",
930                &player_key,
931                || MovementStats::default().with_complete_labeled_tracked_time(),
932            )?,
933            positioning: self.frame_player_stat_or_default_typed_by_key(
934                frame,
935                "positioning",
936                &player_key,
937            )?,
938            powerslide: self.frame_player_stat_or_default_typed_by_key(
939                frame,
940                "powerslide",
941                &player_key,
942            )?,
943            demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
944        })
945    }
946
947    fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
948        self.replay_meta
949            .team_zero
950            .iter()
951            .any(|team_player| team_player.remote_id == player.remote_id)
952    }
953
954    fn timeline_team_value(
955        &self,
956        frame: &StatsSnapshotFrame,
957        team_key: &str,
958    ) -> SubtrActorResult<Value> {
959        let is_team_zero = team_key == "team_zero";
960        let mut team = Map::new();
961        team.insert(
962            "fifty_fifty".to_owned(),
963            serialize_to_json_value(
964                &self
965                    .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
966                    .for_team(is_team_zero),
967            )?,
968        );
969        team.insert(
970            "possession".to_owned(),
971            serialize_to_json_value(
972                &self
973                    .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
974                    .for_team(is_team_zero),
975            )?,
976        );
977        team.insert(
978            "pressure".to_owned(),
979            serialize_to_json_value(
980                &self
981                    .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
982                    .for_team(is_team_zero),
983            )?,
984        );
985        team.insert(
986            "rush".to_owned(),
987            serialize_to_json_value(
988                &self
989                    .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
990                    .for_team(is_team_zero),
991            )?,
992        );
993        team.insert(
994            "core".to_owned(),
995            self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
996        );
997        team.insert(
998            "backboard".to_owned(),
999            self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
1000        );
1001        team.insert(
1002            "double_tap".to_owned(),
1003            self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
1004        );
1005        team.insert(
1006            "one_timer".to_owned(),
1007            self.frame_team_stat_or_default::<OneTimerTeamStats>(frame, "one_timer", team_key),
1008        );
1009        team.insert(
1010            "pass".to_owned(),
1011            self.frame_team_stat_or_default::<PassTeamStats>(frame, "pass", team_key),
1012        );
1013        team.insert(
1014            "ball_carry".to_owned(),
1015            self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
1016        );
1017        team.insert(
1018            "air_dribble".to_owned(),
1019            self.frame_team_stat_or_default::<AirDribbleStats>(frame, "air_dribble", team_key),
1020        );
1021        team.insert(
1022            "boost".to_owned(),
1023            self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
1024        );
1025        team.insert(
1026            "movement".to_owned(),
1027            self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
1028        );
1029        team.insert(
1030            "powerslide".to_owned(),
1031            self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
1032        );
1033        team.insert(
1034            "demo".to_owned(),
1035            self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
1036        );
1037        Ok(Value::Object(team))
1038    }
1039
1040    fn timeline_player_value(
1041        &self,
1042        frame: &StatsSnapshotFrame,
1043        player: &PlayerInfo,
1044    ) -> SubtrActorResult<Value> {
1045        let player_key = player_info_key(player)?;
1046        let mut player_value = Map::new();
1047        player_value.insert(
1048            "player_id".to_owned(),
1049            serialize_to_json_value(&player.remote_id)?,
1050        );
1051        player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
1052        player_value.insert(
1053            "is_team_0".to_owned(),
1054            serialize_to_json_value(
1055                &self
1056                    .replay_meta
1057                    .team_zero
1058                    .iter()
1059                    .any(|team_player| team_player.remote_id == player.remote_id),
1060            )?,
1061        );
1062        player_value.insert(
1063            "core".to_owned(),
1064            self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
1065                frame,
1066                "core",
1067                &player_key,
1068            )?,
1069        );
1070        player_value.insert(
1071            "backboard".to_owned(),
1072            self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
1073                frame,
1074                "backboard",
1075                &player_key,
1076            )?,
1077        );
1078        player_value.insert(
1079            "ceiling_shot".to_owned(),
1080            self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
1081                frame,
1082                "ceiling_shot",
1083                &player_key,
1084            )?,
1085        );
1086        player_value.insert(
1087            "double_tap".to_owned(),
1088            self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
1089                frame,
1090                "double_tap",
1091                &player_key,
1092            )?,
1093        );
1094        player_value.insert(
1095            "one_timer".to_owned(),
1096            self.frame_player_stat_or_default_by_key::<OneTimerPlayerStats>(
1097                frame,
1098                "one_timer",
1099                &player_key,
1100            )?,
1101        );
1102        player_value.insert(
1103            "pass".to_owned(),
1104            self.frame_player_stat_or_default_by_key::<PassPlayerStats>(
1105                frame,
1106                "pass",
1107                &player_key,
1108            )?,
1109        );
1110        player_value.insert(
1111            "fifty_fifty".to_owned(),
1112            self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
1113                frame,
1114                "fifty_fifty",
1115                &player_key,
1116            )?,
1117        );
1118        player_value.insert(
1119            "speed_flip".to_owned(),
1120            self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
1121                frame,
1122                "speed_flip",
1123                &player_key,
1124            )?,
1125        );
1126        player_value.insert(
1127            "half_flip".to_owned(),
1128            self.frame_player_stat_or_default_by_key::<HalfFlipStats>(
1129                frame,
1130                "half_flip",
1131                &player_key,
1132            )?,
1133        );
1134        player_value.insert(
1135            "wavedash".to_owned(),
1136            self.frame_player_stat_or_default_by_key::<WavedashStats>(
1137                frame,
1138                "wavedash",
1139                &player_key,
1140            )?,
1141        );
1142        player_value.insert(
1143            "touch".to_owned(),
1144            self.frame_player_stat_or_value_by_key(
1145                frame,
1146                "touch",
1147                &player_key,
1148                if frame.modules.contains_key("touch") {
1149                    serialize_to_json_value(
1150                        &TouchStats::default().with_complete_labeled_touch_counts(),
1151                    )?
1152                } else {
1153                    default_json_value::<TouchStats>()
1154                },
1155            )?,
1156        );
1157        player_value.insert(
1158            "whiff".to_owned(),
1159            self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
1160        );
1161        player_value.insert(
1162            "flick".to_owned(),
1163            self.frame_player_stat_or_default_by_key::<FlickStats>(frame, "flick", &player_key)?,
1164        );
1165        player_value.insert(
1166            "musty_flick".to_owned(),
1167            self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
1168                frame,
1169                "musty_flick",
1170                &player_key,
1171            )?,
1172        );
1173        player_value.insert(
1174            "dodge_reset".to_owned(),
1175            self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
1176                frame,
1177                "dodge_reset",
1178                &player_key,
1179            )?,
1180        );
1181        player_value.insert(
1182            "ball_carry".to_owned(),
1183            self.frame_player_stat_or_default_by_key::<BallCarryStats>(
1184                frame,
1185                "ball_carry",
1186                &player_key,
1187            )?,
1188        );
1189        player_value.insert(
1190            "air_dribble".to_owned(),
1191            self.frame_player_stat_or_default_by_key::<AirDribbleStats>(
1192                frame,
1193                "air_dribble",
1194                &player_key,
1195            )?,
1196        );
1197        player_value.insert(
1198            "boost".to_owned(),
1199            self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
1200        );
1201        player_value.insert(
1202            "movement".to_owned(),
1203            self.frame_player_stat_or_value_by_key(
1204                frame,
1205                "movement",
1206                &player_key,
1207                if frame.modules.contains_key("movement") {
1208                    serialize_to_json_value(
1209                        &MovementStats::default().with_complete_labeled_tracked_time(),
1210                    )?
1211                } else {
1212                    default_json_value::<MovementStats>()
1213                },
1214            )?,
1215        );
1216        player_value.insert(
1217            "positioning".to_owned(),
1218            self.frame_player_stat_or_default_by_key::<PositioningStats>(
1219                frame,
1220                "positioning",
1221                &player_key,
1222            )?,
1223        );
1224        player_value.insert(
1225            "powerslide".to_owned(),
1226            self.frame_player_stat_or_default_by_key::<PowerslideStats>(
1227                frame,
1228                "powerslide",
1229                &player_key,
1230            )?,
1231        );
1232        player_value.insert(
1233            "demo".to_owned(),
1234            self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
1235                frame,
1236                "demo",
1237                &player_key,
1238            )?,
1239        );
1240        Ok(Value::Object(player_value))
1241    }
1242
1243    fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
1244    where
1245        T: Default + Serialize,
1246    {
1247        frame
1248            .modules
1249            .get(module_name)
1250            .and_then(Value::as_object)
1251            .and_then(|module| module.get("stats"))
1252            .cloned()
1253            .unwrap_or_else(|| default_json_value::<T>())
1254    }
1255
1256    fn frame_team_stat_or_default<T>(
1257        &self,
1258        frame: &StatsSnapshotFrame,
1259        module_name: &str,
1260        team_key: &str,
1261    ) -> Value
1262    where
1263        T: Default + Serialize,
1264    {
1265        frame
1266            .modules
1267            .get(module_name)
1268            .and_then(Value::as_object)
1269            .and_then(|module| module.get(team_key))
1270            .cloned()
1271            .unwrap_or_else(|| default_json_value::<T>())
1272    }
1273
1274    fn frame_player_stat_or_default_by_key<T>(
1275        &self,
1276        frame: &StatsSnapshotFrame,
1277        module_name: &str,
1278        player_key: &str,
1279    ) -> SubtrActorResult<Value>
1280    where
1281        T: Default + Serialize,
1282    {
1283        self.frame_player_stat_or_value_by_key(
1284            frame,
1285            module_name,
1286            player_key,
1287            default_json_value::<T>(),
1288        )
1289    }
1290
1291    fn frame_player_stat_or_value_by_key(
1292        &self,
1293        frame: &StatsSnapshotFrame,
1294        module_name: &str,
1295        player_key: &str,
1296        default_value: Value,
1297    ) -> SubtrActorResult<Value> {
1298        Ok(
1299            player_stats_value_for_key(frame.modules.get(module_name), player_key)?
1300                .cloned()
1301                .unwrap_or(default_value),
1302        )
1303    }
1304
1305    fn frame_stats_or_default_typed<T>(
1306        &self,
1307        frame: &StatsSnapshotFrame,
1308        module_name: &str,
1309    ) -> SubtrActorResult<T>
1310    where
1311        T: Default + DeserializeOwned + Serialize,
1312    {
1313        decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
1314    }
1315
1316    fn frame_team_stat_or_default_typed<T>(
1317        &self,
1318        frame: &StatsSnapshotFrame,
1319        module_name: &str,
1320        team_key: &str,
1321    ) -> SubtrActorResult<T>
1322    where
1323        T: Default + DeserializeOwned + Serialize,
1324    {
1325        decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
1326    }
1327
1328    fn frame_player_stat_or_default_typed_by_key<T>(
1329        &self,
1330        frame: &StatsSnapshotFrame,
1331        module_name: &str,
1332        player_key: &str,
1333    ) -> SubtrActorResult<T>
1334    where
1335        T: Default + DeserializeOwned + Serialize,
1336    {
1337        self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
1338    }
1339
1340    fn frame_core_player_stat_or_default_by_key(
1341        &self,
1342        frame: &StatsSnapshotFrame,
1343        player_key: &str,
1344    ) -> SubtrActorResult<CorePlayerStats> {
1345        decode_core_player_stats_value(self.frame_player_stat_or_value_by_key(
1346            frame,
1347            "core",
1348            player_key,
1349            default_json_value::<CorePlayerStats>(),
1350        )?)
1351    }
1352
1353    fn frame_player_stat_or_default_with_by_key<T, F>(
1354        &self,
1355        frame: &StatsSnapshotFrame,
1356        module_name: &str,
1357        player_key: &str,
1358        default: F,
1359    ) -> SubtrActorResult<T>
1360    where
1361        T: DeserializeOwned + Serialize,
1362        F: FnOnce() -> T,
1363    {
1364        decode_json_value(self.frame_player_stat_or_value_by_key(
1365            frame,
1366            module_name,
1367            player_key,
1368            serialize_to_json_value(&default())?,
1369        )?)
1370    }
1371
1372    fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
1373    where
1374        T: DeserializeOwned,
1375    {
1376        decode_json_value(Value::Array(self.module_array(module_name, field)))
1377    }
1378
1379    fn module_player_events<T, F>(
1380        &self,
1381        module_name: &str,
1382        field: &str,
1383        parse: F,
1384    ) -> SubtrActorResult<Vec<T>>
1385    where
1386        F: Fn(&Value) -> SubtrActorResult<T>,
1387    {
1388        self.module_array(module_name, field)
1389            .iter()
1390            .map(parse)
1391            .collect()
1392    }
1393
1394    fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
1395        self.modules
1396            .get(module_name)
1397            .and_then(Value::as_object)
1398            .and_then(|module| module.get(field))
1399            .and_then(Value::as_array)
1400            .cloned()
1401            .unwrap_or_default()
1402    }
1403}
1404
1405impl CapturedStatsData<ReplayStatsFrame> {
1406    pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
1407        let CapturedStatsData {
1408            replay_meta,
1409            config,
1410            modules,
1411            frames,
1412        } = self;
1413        CapturedStatsData::<StatsSnapshotFrame> {
1414            replay_meta,
1415            config,
1416            modules,
1417            frames: Vec::new(),
1418        }
1419        .into_replay_stats_timeline_with_frames(frames)
1420    }
1421}
1422
1423fn player_stats_value_for_key<'a>(
1424    module: Option<&'a Value>,
1425    player_key: &str,
1426) -> SubtrActorResult<Option<&'a Value>> {
1427    let Some(entries) = module
1428        .and_then(Value::as_object)
1429        .and_then(|module| module.get("player_stats"))
1430        .and_then(Value::as_array)
1431    else {
1432        return Ok(None);
1433    };
1434
1435    for entry in entries {
1436        let Some(entry_object) = entry.as_object() else {
1437            continue;
1438        };
1439        let Some(player_id) = entry_object.get("player_id") else {
1440            continue;
1441        };
1442        let Some(player_stats) = entry_object.get("stats") else {
1443            continue;
1444        };
1445        if player_id_key(player_id)? == player_key {
1446            return Ok(Some(player_stats));
1447        }
1448    }
1449
1450    Ok(None)
1451}
1452
1453fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
1454    player_id_key(&serialize_to_json_value(&player.remote_id)?)
1455}
1456
1457fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
1458    serde_json::to_string(player_id).map_err(|error| {
1459        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1460            error.to_string(),
1461        ))
1462    })
1463}
1464
1465fn default_json_value<T>() -> Value
1466where
1467    T: Default + Serialize,
1468{
1469    serde_json::to_value(T::default()).expect("default stats should serialize to json")
1470}
1471
1472fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
1473where
1474    T: DeserializeOwned,
1475{
1476    serde_json::from_value(value).map_err(|error| {
1477        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1478            error.to_string(),
1479        ))
1480    })
1481}
1482
1483fn decode_core_player_stats_value(mut value: Value) -> SubtrActorResult<CorePlayerStats> {
1484    normalize_core_player_stats_snapshot(&mut value)?;
1485    decode_json_value(value)
1486}
1487
1488fn normalize_core_player_stats_snapshot(value: &mut Value) -> SubtrActorResult<()> {
1489    let Some(object) = value.as_object_mut() else {
1490        return Ok(());
1491    };
1492
1493    insert_cumulative_from_average(
1494        object,
1495        "cumulative_boost_on_goals_against",
1496        "average_boost_on_goals_against",
1497        "goal_against_boost_sample_count",
1498    )?;
1499    insert_cumulative_from_average(
1500        object,
1501        "cumulative_average_boost_in_goal_against_leadup",
1502        "average_boost_in_goal_against_leadup",
1503        "goal_against_boost_leadup_sample_count",
1504    )?;
1505    insert_cumulative_from_average(
1506        object,
1507        "cumulative_min_boost_in_goal_against_leadup",
1508        "average_min_boost_in_goal_against_leadup",
1509        "goal_against_boost_leadup_sample_count",
1510    )?;
1511    insert_cumulative_from_average(
1512        object,
1513        "cumulative_goal_against_position_x",
1514        "average_goal_against_position_x",
1515        "goal_against_position_sample_count",
1516    )?;
1517    insert_cumulative_from_average(
1518        object,
1519        "cumulative_goal_against_position_y",
1520        "average_goal_against_position_y",
1521        "goal_against_position_sample_count",
1522    )?;
1523    insert_cumulative_from_average(
1524        object,
1525        "cumulative_goal_against_position_z",
1526        "average_goal_against_position_z",
1527        "goal_against_position_sample_count",
1528    )?;
1529    insert_cumulative_from_average(
1530        object,
1531        "cumulative_scoring_goal_last_touch_position_x",
1532        "average_scoring_goal_last_touch_position_x",
1533        "scoring_goal_last_touch_position_sample_count",
1534    )?;
1535    insert_cumulative_from_average(
1536        object,
1537        "cumulative_scoring_goal_last_touch_position_y",
1538        "average_scoring_goal_last_touch_position_y",
1539        "scoring_goal_last_touch_position_sample_count",
1540    )?;
1541    insert_cumulative_from_average(
1542        object,
1543        "cumulative_scoring_goal_last_touch_position_z",
1544        "average_scoring_goal_last_touch_position_z",
1545        "scoring_goal_last_touch_position_sample_count",
1546    )?;
1547
1548    if let Value::Object(defaults) = default_json_value::<CorePlayerStats>() {
1549        for (field, default_value) in defaults {
1550            object.entry(field).or_insert(default_value);
1551        }
1552    }
1553
1554    Ok(())
1555}
1556
1557fn insert_cumulative_from_average(
1558    object: &mut Map<String, Value>,
1559    cumulative_field: &str,
1560    average_field: &str,
1561    sample_count_field: &str,
1562) -> SubtrActorResult<()> {
1563    if object.contains_key(cumulative_field) {
1564        return Ok(());
1565    }
1566
1567    let average = object
1568        .get(average_field)
1569        .and_then(Value::as_f64)
1570        .unwrap_or(0.0) as f32;
1571    let sample_count = object
1572        .get(sample_count_field)
1573        .and_then(Value::as_u64)
1574        .unwrap_or(0) as f32;
1575    object.insert(
1576        cumulative_field.to_owned(),
1577        serialize_to_json_value(&(average * sample_count))?,
1578    );
1579
1580    Ok(())
1581}
1582
1583fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
1584    let object = json_object(value, "timeline event")?;
1585    Ok(TimelineEvent {
1586        time: json_required_f32(object, "time")?,
1587        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1588        player_id: json_optional_remote_id(object.get("player_id"))?,
1589        is_team_0: json_optional_bool(object.get("is_team_0")),
1590    })
1591}
1592
1593fn moment_mechanic_event(
1594    kind: &str,
1595    index: usize,
1596    frame: usize,
1597    time: f32,
1598    player_id: PlayerId,
1599    is_team_0: bool,
1600) -> MechanicEvent {
1601    MechanicEvent {
1602        id: format!("{kind}:{frame}:{index}"),
1603        kind: kind.to_owned(),
1604        player_id,
1605        is_team_0,
1606        timing: MechanicTiming::Moment { frame, time },
1607    }
1608}
1609
1610fn span_mechanic_event(
1611    kind: &str,
1612    index: usize,
1613    start_frame: usize,
1614    end_frame: usize,
1615    start_time: f32,
1616    end_time: f32,
1617    player_id: PlayerId,
1618    is_team_0: bool,
1619) -> MechanicEvent {
1620    MechanicEvent {
1621        id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
1622        kind: kind.to_owned(),
1623        player_id,
1624        is_team_0,
1625        timing: MechanicTiming::Span {
1626            start_frame,
1627            end_frame,
1628            start_time,
1629            end_time,
1630        },
1631    }
1632}
1633
1634fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
1635    match event.timing {
1636        MechanicTiming::Moment { time, .. } => time,
1637        MechanicTiming::Span { start_time, .. } => start_time,
1638    }
1639}
1640
1641fn parse_ball_carry_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
1642    let object = json_object(value, "ball carry mechanic event")?;
1643    let serialized_kind = json_required_str(object, "kind")?;
1644    let kind = match serialized_kind {
1645        "carry" => "ball_carry",
1646        "air_dribble" => "air_dribble",
1647        other => other,
1648    };
1649    Ok(span_mechanic_event(
1650        kind,
1651        index,
1652        json_required_usize(object, "start_frame")?,
1653        json_required_usize(object, "end_frame")?,
1654        json_required_f32(object, "start_time")?,
1655        json_required_f32(object, "end_time")?,
1656        json_required_remote_id(object, "player_id")?,
1657        json_required_bool(object, "is_team_0")?,
1658    ))
1659}
1660
1661fn parse_dodge_reset_mechanic_event(
1662    value: &Value,
1663    index: usize,
1664) -> SubtrActorResult<MechanicEvent> {
1665    let object = json_object(value, "dodge reset mechanic event")?;
1666    Ok(moment_mechanic_event(
1667        "flip_reset",
1668        index,
1669        json_required_usize(object, "frame")?,
1670        json_required_f32(object, "time")?,
1671        json_required_remote_id(object, "player")?,
1672        json_required_bool(object, "is_team_0")?,
1673    ))
1674}
1675
1676fn parse_flick_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
1677    let object = json_object(value, "flick mechanic event")?;
1678    Ok(span_mechanic_event(
1679        "flick",
1680        index,
1681        json_required_usize(object, "setup_start_frame")?,
1682        json_required_usize(object, "frame")?,
1683        json_required_f32(object, "setup_start_time")?,
1684        json_required_f32(object, "time")?,
1685        json_required_remote_id(object, "player")?,
1686        json_required_bool(object, "is_team_0")?,
1687    ))
1688}
1689
1690fn parse_musty_flick_mechanic_event(
1691    value: &Value,
1692    index: usize,
1693) -> SubtrActorResult<MechanicEvent> {
1694    let object = json_object(value, "musty flick mechanic event")?;
1695    Ok(span_mechanic_event(
1696        "musty_flick",
1697        index,
1698        json_required_usize(object, "dodge_frame")?,
1699        json_required_usize(object, "frame")?,
1700        json_required_f32(object, "dodge_time")?,
1701        json_required_f32(object, "time")?,
1702        json_required_remote_id(object, "player")?,
1703        json_required_bool(object, "is_team_0")?,
1704    ))
1705}
1706
1707fn parse_goal_context_event(value: &Value) -> SubtrActorResult<GoalContextEvent> {
1708    let object = json_object(value, "goal context event")?;
1709    Ok(GoalContextEvent {
1710        time: json_required_f32(object, "time")?,
1711        frame: json_required_usize(object, "frame")?,
1712        scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
1713        scorer: json_optional_remote_id(object.get("scorer"))?,
1714        scoring_team_most_back_player: json_optional_remote_id(
1715            object.get("scoring_team_most_back_player"),
1716        )?,
1717        defending_team_most_back_player: json_optional_remote_id(
1718            object.get("defending_team_most_back_player"),
1719        )?,
1720        ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
1721        scorer_last_touch: match object.get("scorer_last_touch") {
1722            None | Some(Value::Null) => None,
1723            Some(value) => Some(parse_goal_touch_context(value)?),
1724        },
1725        players: json_required_array(object, "players")?
1726            .iter()
1727            .map(parse_goal_player_context)
1728            .collect::<SubtrActorResult<Vec<_>>>()?,
1729    })
1730}
1731
1732fn parse_goal_player_context(value: &Value) -> SubtrActorResult<GoalPlayerContext> {
1733    let object = json_object(value, "goal player context")?;
1734    Ok(GoalPlayerContext {
1735        player: json_required_remote_id(object, "player")?,
1736        is_team_0: json_required_bool(object, "is_team_0")?,
1737        position: json_optional_goal_context_position(object.get("position"))?,
1738        boost_amount: json_optional_f32(object.get("boost_amount"))?,
1739        average_boost_in_leadup: json_optional_f32(object.get("average_boost_in_leadup"))?,
1740        min_boost_in_leadup: json_optional_f32(object.get("min_boost_in_leadup"))?,
1741        is_most_back: json_required_bool(object, "is_most_back")?,
1742    })
1743}
1744
1745fn parse_goal_touch_context(value: &Value) -> SubtrActorResult<GoalTouchContext> {
1746    let object = json_object(value, "goal touch context")?;
1747    Ok(GoalTouchContext {
1748        time: json_required_f32(object, "time")?,
1749        frame: json_required_usize(object, "frame")?,
1750        player: json_required_remote_id(object, "player")?,
1751        is_team_0: json_required_bool(object, "is_team_0")?,
1752        ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
1753        player_position: json_optional_goal_context_position(object.get("player_position"))?,
1754        players: match object.get("players").and_then(Value::as_array) {
1755            Some(players) => players
1756                .iter()
1757                .map(parse_goal_player_context)
1758                .collect::<SubtrActorResult<Vec<_>>>()?,
1759            None => Vec::new(),
1760        },
1761    })
1762}
1763
1764fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
1765    let object = json_object(value, "backboard event")?;
1766    Ok(BackboardBounceEvent {
1767        time: json_required_f32(object, "time")?,
1768        frame: json_required_usize(object, "frame")?,
1769        player: json_required_remote_id(object, "player")?,
1770        is_team_0: json_required_bool(object, "is_team_0")?,
1771    })
1772}
1773
1774fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
1775    let object = json_object(value, "ceiling shot event")?;
1776    Ok(CeilingShotEvent {
1777        time: json_required_f32(object, "time")?,
1778        frame: json_required_usize(object, "frame")?,
1779        player: json_required_remote_id(object, "player")?,
1780        is_team_0: json_required_bool(object, "is_team_0")?,
1781        ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
1782        ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
1783        time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
1784        ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
1785        touch_position: json_required_vec3(object, "touch_position")?,
1786        local_ball_position: json_required_vec3(object, "local_ball_position")?,
1787        separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
1788        roof_alignment: json_required_f32(object, "roof_alignment")?,
1789        forward_alignment: json_required_f32(object, "forward_alignment")?,
1790        forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
1791        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
1792        confidence: json_required_f32(object, "confidence")?,
1793    })
1794}
1795
1796fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
1797    let object = json_object(value, "double tap event")?;
1798    Ok(DoubleTapEvent {
1799        time: json_required_f32(object, "time")?,
1800        frame: json_required_usize(object, "frame")?,
1801        player: json_required_remote_id(object, "player")?,
1802        is_team_0: json_required_bool(object, "is_team_0")?,
1803        backboard_time: json_required_f32(object, "backboard_time")?,
1804        backboard_frame: json_required_usize(object, "backboard_frame")?,
1805    })
1806}
1807
1808fn parse_pass_event(value: &Value) -> SubtrActorResult<PassEvent> {
1809    let object = json_object(value, "pass event")?;
1810    Ok(PassEvent {
1811        time: json_required_f32(object, "time")?,
1812        frame: json_required_usize(object, "frame")?,
1813        passer: json_required_remote_id(object, "passer")?,
1814        receiver: json_required_remote_id(object, "receiver")?,
1815        is_team_0: json_required_bool(object, "is_team_0")?,
1816        start_time: json_required_f32(object, "start_time")?,
1817        start_frame: json_required_usize(object, "start_frame")?,
1818        duration: json_required_f32(object, "duration")?,
1819        ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
1820        ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
1821    })
1822}
1823
1824fn parse_one_timer_event(value: &Value) -> SubtrActorResult<OneTimerEvent> {
1825    let object = json_object(value, "one timer event")?;
1826    Ok(OneTimerEvent {
1827        time: json_required_f32(object, "time")?,
1828        frame: json_required_usize(object, "frame")?,
1829        player: json_required_remote_id(object, "player")?,
1830        passer: json_required_remote_id(object, "passer")?,
1831        is_team_0: json_required_bool(object, "is_team_0")?,
1832        pass_start_time: json_required_f32(object, "pass_start_time")?,
1833        pass_start_frame: json_required_usize(object, "pass_start_frame")?,
1834        pass_duration: json_required_f32(object, "pass_duration")?,
1835        pass_travel_distance: json_required_f32(object, "pass_travel_distance")?,
1836        pass_advance_distance: json_required_f32(object, "pass_advance_distance")?,
1837        ball_speed: json_required_f32(object, "ball_speed")?,
1838        goal_alignment: json_required_f32(object, "goal_alignment")?,
1839    })
1840}
1841
1842fn parse_goal_tag_event(value: &Value) -> SubtrActorResult<GoalTagEvent> {
1843    let object = json_object(value, "goal tag event")?;
1844    Ok(GoalTagEvent {
1845        goal_index: json_required_usize(object, "goal_index")?,
1846        time: json_required_f32(object, "time")?,
1847        frame: json_required_usize(object, "frame")?,
1848        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1849        scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
1850        scorer: json_optional_remote_id(object.get("scorer"))?,
1851        confidence: json_required_f32(object, "confidence")?,
1852        modifiers: json_optional_array(object.get("modifiers"))?
1853            .iter()
1854            .map(|modifier| decode_json_value(modifier.clone()))
1855            .collect::<SubtrActorResult<Vec<_>>>()?,
1856        evidence: json_required_array(object, "evidence")?
1857            .iter()
1858            .map(parse_goal_tag_evidence)
1859            .collect::<SubtrActorResult<Vec<_>>>()?,
1860    })
1861}
1862
1863fn parse_goal_tag_evidence(value: &Value) -> SubtrActorResult<GoalTagEvidence> {
1864    let object = json_object(value, "goal tag evidence")?;
1865    Ok(GoalTagEvidence {
1866        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1867        time: json_required_f32(object, "time")?,
1868        frame: json_required_usize(object, "frame")?,
1869        player: json_optional_remote_id(object.get("player"))?,
1870    })
1871}
1872
1873fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
1874    let object = json_object(value, "fifty fifty event")?;
1875    Ok(FiftyFiftyEvent {
1876        start_time: json_required_f32(object, "start_time")?,
1877        start_frame: json_required_usize(object, "start_frame")?,
1878        resolve_time: json_required_f32(object, "resolve_time")?,
1879        resolve_frame: json_required_usize(object, "resolve_frame")?,
1880        is_kickoff: json_required_bool(object, "is_kickoff")?,
1881        team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
1882        team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
1883        team_zero_position: json_required_vec3(object, "team_zero_position")?,
1884        team_one_position: json_required_vec3(object, "team_one_position")?,
1885        midpoint: json_required_vec3(object, "midpoint")?,
1886        plane_normal: json_required_vec3(object, "plane_normal")?,
1887        winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
1888        possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
1889    })
1890}
1891
1892fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
1893    let object = json_object(value, "speed flip event")?;
1894    Ok(SpeedFlipEvent {
1895        time: json_required_f32(object, "time")?,
1896        frame: json_required_usize(object, "frame")?,
1897        player: json_required_remote_id(object, "player")?,
1898        is_team_0: json_required_bool(object, "is_team_0")?,
1899        time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
1900        start_position: json_required_vec3(object, "start_position")?,
1901        end_position: json_required_vec3(object, "end_position")?,
1902        start_speed: json_required_f32(object, "start_speed")?,
1903        max_speed: json_required_f32(object, "max_speed")?,
1904        best_alignment: json_required_f32(object, "best_alignment")?,
1905        diagonal_score: json_required_f32(object, "diagonal_score")?,
1906        cancel_score: json_required_f32(object, "cancel_score")?,
1907        speed_score: json_required_f32(object, "speed_score")?,
1908        confidence: json_required_f32(object, "confidence")?,
1909    })
1910}
1911
1912fn parse_half_flip_event(value: &Value) -> SubtrActorResult<HalfFlipEvent> {
1913    let object = json_object(value, "half flip event")?;
1914    Ok(HalfFlipEvent {
1915        time: json_required_f32(object, "time")?,
1916        frame: json_required_usize(object, "frame")?,
1917        player: json_required_remote_id(object, "player")?,
1918        is_team_0: json_required_bool(object, "is_team_0")?,
1919        start_position: json_required_vec3(object, "start_position")?,
1920        end_position: json_required_vec3(object, "end_position")?,
1921        start_speed: json_required_f32(object, "start_speed")?,
1922        end_speed: json_required_f32(object, "end_speed")?,
1923        start_backward_alignment: json_required_f32(object, "start_backward_alignment")?,
1924        best_reorientation_alignment: json_required_f32(object, "best_reorientation_alignment")?,
1925        best_forward_reversal: json_required_f32(object, "best_forward_reversal")?,
1926        max_forward_vertical: json_required_f32(object, "max_forward_vertical")?,
1927        confidence: json_required_f32(object, "confidence")?,
1928    })
1929}
1930
1931fn parse_wavedash_event(value: &Value) -> SubtrActorResult<WavedashEvent> {
1932    let object = json_object(value, "wavedash event")?;
1933    Ok(WavedashEvent {
1934        time: json_required_f32(object, "time")?,
1935        frame: json_required_usize(object, "frame")?,
1936        player: json_required_remote_id(object, "player")?,
1937        is_team_0: json_required_bool(object, "is_team_0")?,
1938        dodge_time: json_required_f32(object, "dodge_time")?,
1939        dodge_frame: json_required_usize(object, "dodge_frame")?,
1940        time_since_dodge: json_required_f32(object, "time_since_dodge")?,
1941        dodge_position: json_required_vec3(object, "dodge_position")?,
1942        landing_position: json_required_vec3(object, "landing_position")?,
1943        start_speed: json_required_f32(object, "start_speed")?,
1944        landing_speed: json_required_f32(object, "landing_speed")?,
1945        horizontal_speed_gain: json_required_f32(object, "horizontal_speed_gain")?,
1946        landing_uprightness: json_required_f32(object, "landing_uprightness")?,
1947        confidence: json_required_f32(object, "confidence")?,
1948    })
1949}
1950
1951fn parse_whiff_event(value: &Value) -> SubtrActorResult<WhiffEvent> {
1952    let object = json_object(value, "whiff event")?;
1953    Ok(WhiffEvent {
1954        time: json_required_f32(object, "time")?,
1955        frame: json_required_usize(object, "frame")?,
1956        player: json_required_remote_id(object, "player")?,
1957        is_team_0: json_required_bool(object, "is_team_0")?,
1958        closest_approach_distance: json_required_f32(object, "closest_approach_distance")?,
1959        forward_alignment: json_required_f32(object, "forward_alignment")?,
1960        approach_speed: json_required_f32(object, "approach_speed")?,
1961        dodge_active: json_required_bool(object, "dodge_active")?,
1962        aerial: json_required_bool(object, "aerial")?,
1963    })
1964}
1965
1966fn parse_boost_pickup_comparison_event(
1967    value: &Value,
1968) -> SubtrActorResult<BoostPickupComparisonEvent> {
1969    let object = json_object(value, "boost pickup comparison event")?;
1970    Ok(BoostPickupComparisonEvent {
1971        comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
1972        frame: json_required_usize(object, "frame")?,
1973        time: json_required_f32(object, "time")?,
1974        player_id: json_required_remote_id(object, "player_id")?,
1975        is_team_0: json_required_bool(object, "is_team_0")?,
1976        pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
1977        field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
1978        activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
1979        reported_frame: json_optional_usize(object.get("reported_frame"))?,
1980        reported_time: json_optional_f32(object.get("reported_time"))?,
1981        inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
1982        inferred_time: json_optional_f32(object.get("inferred_time"))?,
1983        boost_before: json_optional_f32(object.get("boost_before"))?,
1984        boost_after: json_optional_f32(object.get("boost_after"))?,
1985    })
1986}
1987
1988fn json_object<'a>(
1989    value: &'a Value,
1990    context: &str,
1991) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
1992    value.as_object().ok_or_else(|| {
1993        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1994            "Expected {context} to be a JSON object"
1995        )))
1996    })
1997}
1998
1999fn json_required_value<'a>(
2000    object: &'a serde_json::Map<String, Value>,
2001    field: &str,
2002) -> SubtrActorResult<&'a Value> {
2003    object.get(field).ok_or_else(|| {
2004        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2005            "Missing JSON field '{field}'"
2006        )))
2007    })
2008}
2009
2010fn json_required_array<'a>(
2011    object: &'a serde_json::Map<String, Value>,
2012    field: &str,
2013) -> SubtrActorResult<&'a Vec<Value>> {
2014    json_required_value(object, field)?
2015        .as_array()
2016        .ok_or_else(|| {
2017            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2018                "Expected JSON field '{field}' to be an array"
2019            )))
2020        })
2021}
2022
2023fn json_optional_array<'a>(value: Option<&'a Value>) -> SubtrActorResult<&'a [Value]> {
2024    match value {
2025        Some(Value::Array(values)) => Ok(values),
2026        Some(_) => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2027            "Expected optional JSON value to be an array".to_owned(),
2028        )),
2029        None => Ok(&[]),
2030    }
2031}
2032
2033fn json_f32(value: &Value) -> Option<f32> {
2034    value.as_f64().map(|number| number as f32)
2035}
2036
2037fn json_config_f32(
2038    config: Option<&Map<String, Value>>,
2039    key: &str,
2040    legacy_key: &str,
2041) -> Option<f32> {
2042    config.and_then(|config| {
2043        config
2044            .get(key)
2045            .or_else(|| config.get(legacy_key))
2046            .and_then(json_f32)
2047    })
2048}
2049
2050fn json_required_f32(
2051    object: &serde_json::Map<String, Value>,
2052    field: &str,
2053) -> SubtrActorResult<f32> {
2054    json_f32(json_required_value(object, field)?).ok_or_else(|| {
2055        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2056            "Expected JSON field '{field}' to be a float"
2057        )))
2058    })
2059}
2060
2061fn json_required_usize(
2062    object: &serde_json::Map<String, Value>,
2063    field: &str,
2064) -> SubtrActorResult<usize> {
2065    json_required_value(object, field)?
2066        .as_u64()
2067        .map(|number| number as usize)
2068        .ok_or_else(|| {
2069            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2070                "Expected JSON field '{field}' to be an unsigned integer"
2071            )))
2072        })
2073}
2074
2075fn json_required_bool(
2076    object: &serde_json::Map<String, Value>,
2077    field: &str,
2078) -> SubtrActorResult<bool> {
2079    json_required_value(object, field)?
2080        .as_bool()
2081        .ok_or_else(|| {
2082            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2083                "Expected JSON field '{field}' to be a bool"
2084            )))
2085        })
2086}
2087
2088fn json_required_str<'a>(
2089    object: &'a serde_json::Map<String, Value>,
2090    field: &str,
2091) -> SubtrActorResult<&'a str> {
2092    json_required_value(object, field)?.as_str().ok_or_else(|| {
2093        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2094            "Expected JSON field '{field}' to be a string"
2095        )))
2096    })
2097}
2098
2099fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
2100    value.and_then(Value::as_bool)
2101}
2102
2103fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
2104    match value {
2105        None | Some(Value::Null) => Ok(None),
2106        Some(value) => json_f32(value).map(Some).ok_or_else(|| {
2107            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2108                "Expected optional JSON value to be a float".to_owned(),
2109            ))
2110        }),
2111    }
2112}
2113
2114fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
2115    match value {
2116        None | Some(Value::Null) => Ok(None),
2117        Some(value) => value
2118            .as_u64()
2119            .map(|number| Some(number as usize))
2120            .ok_or_else(|| {
2121                SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2122                    "Expected optional JSON value to be an unsigned integer".to_owned(),
2123                ))
2124            }),
2125    }
2126}
2127
2128fn json_goal_context_position(value: &Value) -> SubtrActorResult<GoalContextPosition> {
2129    let object = json_object(value, "goal context position")?;
2130    Ok(GoalContextPosition {
2131        x: json_required_f32(object, "x")?,
2132        y: json_required_f32(object, "y")?,
2133        z: json_required_f32(object, "z")?,
2134    })
2135}
2136
2137fn json_optional_goal_context_position(
2138    value: Option<&Value>,
2139) -> SubtrActorResult<Option<GoalContextPosition>> {
2140    match value {
2141        None | Some(Value::Null) => Ok(None),
2142        Some(value) => json_goal_context_position(value).map(Some),
2143    }
2144}
2145
2146fn json_required_vec3(
2147    object: &serde_json::Map<String, Value>,
2148    field: &str,
2149) -> SubtrActorResult<[f32; 3]> {
2150    let array = json_required_value(object, field)?
2151        .as_array()
2152        .ok_or_else(|| {
2153            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2154                "Expected JSON field '{field}' to be a 3-element array"
2155            )))
2156        })?;
2157    if array.len() != 3 {
2158        return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2159            format!("Expected JSON field '{field}' to contain exactly 3 elements"),
2160        ));
2161    }
2162    Ok([
2163        json_f32(&array[0]).ok_or_else(|| {
2164            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2165                "Expected JSON field '{field}[0]' to be a float"
2166            )))
2167        })?,
2168        json_f32(&array[1]).ok_or_else(|| {
2169            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2170                "Expected JSON field '{field}[1]' to be a float"
2171            )))
2172        })?,
2173        json_f32(&array[2]).ok_or_else(|| {
2174            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2175                "Expected JSON field '{field}[2]' to be a float"
2176            )))
2177        })?,
2178    ])
2179}
2180
2181fn json_required_remote_id(
2182    object: &serde_json::Map<String, Value>,
2183    field: &str,
2184) -> SubtrActorResult<PlayerId> {
2185    json_remote_id(json_required_value(object, field)?)
2186}
2187
2188fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
2189    match value {
2190        None | Some(Value::Null) => Ok(None),
2191        Some(value) => Ok(Some(json_remote_id(value)?)),
2192    }
2193}
2194
2195fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
2196    let object = json_object(value, "remote id")?;
2197    if object.len() != 1 {
2198        return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2199            "Expected remote id to contain exactly one variant".to_owned(),
2200        ));
2201    }
2202
2203    let (variant, payload) = object.iter().next().expect("validated single variant");
2204    match variant.as_str() {
2205        "PlayStation" => {
2206            let payload = json_object(payload, "playstation remote id")?;
2207            Ok(RemoteId::PlayStation(Ps4Id {
2208                online_id: json_u64(json_required_value(payload, "online_id")?)?,
2209                name: json_required_value(payload, "name")?
2210                    .as_str()
2211                    .ok_or_else(|| {
2212                        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2213                            "Expected PlayStation name to be a string".to_owned(),
2214                        ))
2215                    })?
2216                    .to_owned(),
2217                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
2218            }))
2219        }
2220        "PsyNet" => {
2221            let payload = json_object(payload, "psynet remote id")?;
2222            Ok(RemoteId::PsyNet(PsyNetId {
2223                online_id: json_u64(json_required_value(payload, "online_id")?)?,
2224                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
2225            }))
2226        }
2227        "SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
2228        "Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
2229        "Switch" => {
2230            let payload = json_object(payload, "switch remote id")?;
2231            Ok(RemoteId::Switch(SwitchId {
2232                online_id: json_u64(json_required_value(payload, "online_id")?)?,
2233                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
2234            }))
2235        }
2236        "Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
2237        "QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
2238        "Epic" => Ok(RemoteId::Epic(
2239            payload
2240                .as_str()
2241                .ok_or_else(|| {
2242                    SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2243                        "Expected Epic remote id payload to be a string".to_owned(),
2244                    ))
2245                })?
2246                .to_owned(),
2247        )),
2248        variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2249            format!("Unknown remote id variant '{variant}'"),
2250        )),
2251    }
2252}
2253
2254fn json_u64(value: &Value) -> SubtrActorResult<u64> {
2255    value
2256        .as_u64()
2257        .or_else(|| value.as_str().and_then(|text| text.parse().ok()))
2258        .ok_or_else(|| {
2259            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2260                "Expected JSON value to be a u64".to_owned(),
2261            ))
2262        })
2263}
2264
2265fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
2266    value
2267        .as_array()
2268        .ok_or_else(|| {
2269            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2270                "Expected JSON value to be an array of bytes".to_owned(),
2271            ))
2272        })?
2273        .iter()
2274        .map(|entry| {
2275            entry
2276                .as_u64()
2277                .and_then(|number| u8::try_from(number).ok())
2278                .ok_or_else(|| {
2279                    SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2280                        "Expected JSON array entry to be a byte".to_owned(),
2281                    ))
2282                })
2283        })
2284        .collect()
2285}