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