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 to_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
61        self.to_replay_stats_timeline_with_frames(
62            self.frames
63                .iter()
64                .map(|frame| self.replay_stats_frame(frame))
65                .collect::<SubtrActorResult<Vec<_>>>()?,
66        )
67    }
68
69    pub(crate) fn into_replay_stats_timeline_with_frames(
70        self,
71        frames: Vec<ReplayStatsFrame>,
72    ) -> SubtrActorResult<ReplayStatsTimeline> {
73        self.to_replay_stats_timeline_with_frames(frames)
74    }
75
76    fn to_replay_stats_timeline_with_frames(
77        &self,
78        frames: Vec<ReplayStatsFrame>,
79    ) -> SubtrActorResult<ReplayStatsTimeline> {
80        Ok(ReplayStatsTimeline {
81            config: self.timeline_config(),
82            replay_meta: self.replay_meta.clone(),
83            events: self.timeline_event_sets_typed()?,
84            frames,
85        })
86    }
87
88    pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
89        self.to_stats_timeline_value()
90    }
91
92    pub fn to_stats_timeline_value(&self) -> SubtrActorResult<Value> {
93        let mut timeline = Map::new();
94        timeline.insert("config".to_owned(), self.timeline_config_value()?);
95        timeline.insert(
96            "replay_meta".to_owned(),
97            serialize_to_json_value(&self.replay_meta)?,
98        );
99        timeline.insert("events".to_owned(), self.timeline_event_sets_value());
100        timeline.insert(
101            "frames".to_owned(),
102            Value::Array(
103                self.frames
104                    .iter()
105                    .map(|frame| self.timeline_frame_value(frame))
106                    .collect::<SubtrActorResult<Vec<_>>>()?,
107            ),
108        );
109        Ok(Value::Object(timeline))
110    }
111
112    fn timeline_events(&self) -> Vec<Value> {
113        let mut events = self.module_array("core", "timeline");
114        events.extend(self.module_array("demo", "timeline"));
115        events.sort_by(|left, right| {
116            let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
117            let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
118            left_time.total_cmp(&right_time)
119        });
120        events
121    }
122
123    fn timeline_events_typed(&self) -> SubtrActorResult<Vec<TimelineEvent>> {
124        self.timeline_events()
125            .iter()
126            .map(parse_timeline_event)
127            .collect()
128    }
129
130    fn timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
131        Ok(ReplayStatsTimelineEvents {
132            timeline: self.timeline_events_typed()?,
133            backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
134            ceiling_shot: self.module_player_events(
135                "ceiling_shot",
136                "events",
137                parse_ceiling_shot_event,
138            )?,
139            double_tap: self.module_player_events(
140                "double_tap",
141                "events",
142                parse_double_tap_event,
143            )?,
144            fifty_fifty: self.module_player_events(
145                "fifty_fifty",
146                "events",
147                parse_fifty_fifty_event,
148            )?,
149            rush: self.module_typed_array("rush", "events")?,
150            speed_flip: self.module_player_events(
151                "speed_flip",
152                "events",
153                parse_speed_flip_event,
154            )?,
155            boost_pickups: self.module_player_events(
156                "boost",
157                "events",
158                parse_boost_pickup_comparison_event,
159            )?,
160        })
161    }
162
163    fn timeline_event_sets_value(&self) -> Value {
164        let mut events = Map::new();
165        events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
166        events.insert(
167            "backboard".to_owned(),
168            Value::Array(self.module_array("backboard", "events")),
169        );
170        events.insert(
171            "ceiling_shot".to_owned(),
172            Value::Array(self.module_array("ceiling_shot", "events")),
173        );
174        events.insert(
175            "double_tap".to_owned(),
176            Value::Array(self.module_array("double_tap", "events")),
177        );
178        events.insert(
179            "fifty_fifty".to_owned(),
180            Value::Array(self.module_array("fifty_fifty", "events")),
181        );
182        events.insert(
183            "rush".to_owned(),
184            Value::Array(self.module_array("rush", "events")),
185        );
186        events.insert(
187            "speed_flip".to_owned(),
188            Value::Array(self.module_array("speed_flip", "events")),
189        );
190        events.insert(
191            "boost_pickups".to_owned(),
192            Value::Array(self.module_array("boost", "events")),
193        );
194        Value::Object(events)
195    }
196
197    fn timeline_config(&self) -> StatsTimelineConfig {
198        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
199        let pressure_config = self.config.get("pressure").and_then(Value::as_object);
200        let rush_config = self.config.get("rush").and_then(Value::as_object);
201        let rush_defaults = RushCalculatorConfig::default();
202
203        StatsTimelineConfig {
204            most_back_forward_threshold_y: positioning_config
205                .and_then(|config| config.get("most_back_forward_threshold_y"))
206                .and_then(json_f32)
207                .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
208            level_ball_depth_margin: positioning_config
209                .and_then(|config| config.get("level_ball_depth_margin"))
210                .and_then(json_f32)
211                .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
212            pressure_neutral_zone_half_width_y: pressure_config
213                .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
214                .and_then(json_f32)
215                .unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
216            rush_max_start_y: rush_config
217                .and_then(|config| config.get("rush_max_start_y"))
218                .and_then(json_f32)
219                .unwrap_or(rush_defaults.max_start_y),
220            rush_attack_support_distance_y: rush_config
221                .and_then(|config| config.get("rush_attack_support_distance_y"))
222                .and_then(json_f32)
223                .unwrap_or(rush_defaults.attack_support_distance_y),
224            rush_defender_distance_y: rush_config
225                .and_then(|config| config.get("rush_defender_distance_y"))
226                .and_then(json_f32)
227                .unwrap_or(rush_defaults.defender_distance_y),
228            rush_min_possession_retained_seconds: rush_config
229                .and_then(|config| config.get("rush_min_possession_retained_seconds"))
230                .and_then(json_f32)
231                .unwrap_or(rush_defaults.min_possession_retained_seconds),
232        }
233    }
234
235    fn timeline_config_value(&self) -> SubtrActorResult<Value> {
236        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
237        let pressure_config = self.config.get("pressure").and_then(Value::as_object);
238        let rush_config = self.config.get("rush").and_then(Value::as_object);
239
240        let mut config = Map::new();
241        config.insert(
242            "most_back_forward_threshold_y".to_owned(),
243            serialize_to_json_value(
244                &positioning_config
245                    .and_then(|config| config.get("most_back_forward_threshold_y"))
246                    .and_then(Value::as_f64)
247                    .unwrap_or(
248                        PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
249                    ),
250            )?,
251        );
252        config.insert(
253            "level_ball_depth_margin".to_owned(),
254            serialize_to_json_value(
255                &positioning_config
256                    .and_then(|config| config.get("level_ball_depth_margin"))
257                    .and_then(Value::as_f64)
258                    .unwrap_or(
259                        PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
260                    ),
261            )?,
262        );
263        config.insert(
264            "pressure_neutral_zone_half_width_y".to_owned(),
265            serialize_to_json_value(
266                &pressure_config
267                    .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
268                    .and_then(Value::as_f64)
269                    .unwrap_or(
270                        PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
271                    ),
272            )?,
273        );
274        let rush_defaults = RushCalculatorConfig::default();
275        config.insert(
276            "rush_max_start_y".to_owned(),
277            serialize_to_json_value(
278                &rush_config
279                    .and_then(|config| config.get("rush_max_start_y"))
280                    .and_then(Value::as_f64)
281                    .unwrap_or(rush_defaults.max_start_y as f64),
282            )?,
283        );
284        config.insert(
285            "rush_attack_support_distance_y".to_owned(),
286            serialize_to_json_value(
287                &rush_config
288                    .and_then(|config| config.get("rush_attack_support_distance_y"))
289                    .and_then(Value::as_f64)
290                    .unwrap_or(rush_defaults.attack_support_distance_y as f64),
291            )?,
292        );
293        config.insert(
294            "rush_defender_distance_y".to_owned(),
295            serialize_to_json_value(
296                &rush_config
297                    .and_then(|config| config.get("rush_defender_distance_y"))
298                    .and_then(Value::as_f64)
299                    .unwrap_or(rush_defaults.defender_distance_y as f64),
300            )?,
301        );
302        config.insert(
303            "rush_min_possession_retained_seconds".to_owned(),
304            serialize_to_json_value(
305                &rush_config
306                    .and_then(|config| config.get("rush_min_possession_retained_seconds"))
307                    .and_then(Value::as_f64)
308                    .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
309            )?,
310        );
311        Ok(Value::Object(config))
312    }
313
314    fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
315        let mut timeline = Map::new();
316        timeline.insert(
317            "frame_number".to_owned(),
318            serialize_to_json_value(&frame.frame_number)?,
319        );
320        timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
321        timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
322        timeline.insert(
323            "seconds_remaining".to_owned(),
324            serialize_to_json_value(&frame.seconds_remaining)?,
325        );
326        timeline.insert(
327            "game_state".to_owned(),
328            serialize_to_json_value(&frame.game_state)?,
329        );
330        timeline.insert(
331            "gameplay_phase".to_owned(),
332            serialize_to_json_value(&frame.gameplay_phase)?,
333        );
334        timeline.insert(
335            "is_live_play".to_owned(),
336            serialize_to_json_value(&frame.is_live_play)?,
337        );
338        timeline.insert(
339            "fifty_fifty".to_owned(),
340            self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
341        );
342        timeline.insert(
343            "possession".to_owned(),
344            self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
345        );
346        timeline.insert(
347            "pressure".to_owned(),
348            self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
349        );
350        timeline.insert(
351            "rush".to_owned(),
352            self.frame_stats_or_default::<RushStats>(frame, "rush"),
353        );
354        timeline.insert(
355            "team_zero".to_owned(),
356            self.timeline_team_value(frame, "team_zero")?,
357        );
358        timeline.insert(
359            "team_one".to_owned(),
360            self.timeline_team_value(frame, "team_one")?,
361        );
362        timeline.insert(
363            "players".to_owned(),
364            Value::Array(
365                self.replay_meta
366                    .player_order()
367                    .map(|player| self.timeline_player_value(frame, player))
368                    .collect::<SubtrActorResult<Vec<_>>>()?,
369            ),
370        );
371        Ok(Value::Object(timeline))
372    }
373
374    pub(crate) fn replay_stats_frame(
375        &self,
376        frame: &StatsSnapshotFrame,
377    ) -> SubtrActorResult<ReplayStatsFrame> {
378        Ok(ReplayStatsFrame {
379            frame_number: frame.frame_number,
380            time: frame.time,
381            dt: frame.dt,
382            seconds_remaining: frame.seconds_remaining,
383            game_state: frame.game_state,
384            gameplay_phase: frame.gameplay_phase,
385            is_live_play: frame.is_live_play,
386            team_zero: self.replay_team_stats(frame, "team_zero")?,
387            team_one: self.replay_team_stats(frame, "team_one")?,
388            players: self
389                .replay_meta
390                .player_order()
391                .map(|player| self.replay_player_stats(frame, player))
392                .collect::<SubtrActorResult<Vec<_>>>()?,
393        })
394    }
395
396    fn replay_team_stats(
397        &self,
398        frame: &StatsSnapshotFrame,
399        team_key: &str,
400    ) -> SubtrActorResult<TeamStatsSnapshot> {
401        let is_team_zero = team_key == "team_zero";
402        Ok(TeamStatsSnapshot {
403            fifty_fifty: self
404                .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
405                .for_team(is_team_zero),
406            possession: self
407                .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
408                .for_team(is_team_zero),
409            pressure: self
410                .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
411                .for_team(is_team_zero),
412            rush: self
413                .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
414                .for_team(is_team_zero),
415            core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
416            backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
417            double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
418            ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
419            boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
420            movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
421            powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
422            demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
423        })
424    }
425
426    fn replay_player_stats(
427        &self,
428        frame: &StatsSnapshotFrame,
429        player: &PlayerInfo,
430    ) -> SubtrActorResult<PlayerStatsSnapshot> {
431        let player_key = player_info_key(player)?;
432        Ok(PlayerStatsSnapshot {
433            player_id: player.remote_id.clone(),
434            name: player.name.clone(),
435            is_team_0: self.is_team_zero_player(player),
436            core: self.frame_player_stat_or_default_typed_by_key(frame, "core", &player_key)?,
437            backboard: self.frame_player_stat_or_default_typed_by_key(
438                frame,
439                "backboard",
440                &player_key,
441            )?,
442            ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
443                frame,
444                "ceiling_shot",
445                &player_key,
446            )?,
447            double_tap: self.frame_player_stat_or_default_typed_by_key(
448                frame,
449                "double_tap",
450                &player_key,
451            )?,
452            fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
453                frame,
454                "fifty_fifty",
455                &player_key,
456            )?,
457            speed_flip: self.frame_player_stat_or_default_typed_by_key(
458                frame,
459                "speed_flip",
460                &player_key,
461            )?,
462            touch: if frame.modules.contains_key("touch") {
463                self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
464                    TouchStats::default().with_complete_labeled_touch_counts()
465                })?
466            } else {
467                self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
468            },
469            musty_flick: self.frame_player_stat_or_default_typed_by_key(
470                frame,
471                "musty_flick",
472                &player_key,
473            )?,
474            dodge_reset: self.frame_player_stat_or_default_typed_by_key(
475                frame,
476                "dodge_reset",
477                &player_key,
478            )?,
479            ball_carry: self.frame_player_stat_or_default_typed_by_key(
480                frame,
481                "ball_carry",
482                &player_key,
483            )?,
484            boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
485            movement: self.frame_player_stat_or_default_with_by_key(
486                frame,
487                "movement",
488                &player_key,
489                || MovementStats::default().with_complete_labeled_tracked_time(),
490            )?,
491            positioning: self.frame_player_stat_or_default_typed_by_key(
492                frame,
493                "positioning",
494                &player_key,
495            )?,
496            powerslide: self.frame_player_stat_or_default_typed_by_key(
497                frame,
498                "powerslide",
499                &player_key,
500            )?,
501            demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
502        })
503    }
504
505    fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
506        self.replay_meta
507            .team_zero
508            .iter()
509            .any(|team_player| team_player.remote_id == player.remote_id)
510    }
511
512    fn timeline_team_value(
513        &self,
514        frame: &StatsSnapshotFrame,
515        team_key: &str,
516    ) -> SubtrActorResult<Value> {
517        let is_team_zero = team_key == "team_zero";
518        let mut team = Map::new();
519        team.insert(
520            "fifty_fifty".to_owned(),
521            serialize_to_json_value(
522                &self
523                    .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
524                    .for_team(is_team_zero),
525            )?,
526        );
527        team.insert(
528            "possession".to_owned(),
529            serialize_to_json_value(
530                &self
531                    .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
532                    .for_team(is_team_zero),
533            )?,
534        );
535        team.insert(
536            "pressure".to_owned(),
537            serialize_to_json_value(
538                &self
539                    .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
540                    .for_team(is_team_zero),
541            )?,
542        );
543        team.insert(
544            "rush".to_owned(),
545            serialize_to_json_value(
546                &self
547                    .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
548                    .for_team(is_team_zero),
549            )?,
550        );
551        team.insert(
552            "core".to_owned(),
553            self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
554        );
555        team.insert(
556            "backboard".to_owned(),
557            self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
558        );
559        team.insert(
560            "double_tap".to_owned(),
561            self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
562        );
563        team.insert(
564            "ball_carry".to_owned(),
565            self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
566        );
567        team.insert(
568            "boost".to_owned(),
569            self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
570        );
571        team.insert(
572            "movement".to_owned(),
573            self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
574        );
575        team.insert(
576            "powerslide".to_owned(),
577            self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
578        );
579        team.insert(
580            "demo".to_owned(),
581            self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
582        );
583        Ok(Value::Object(team))
584    }
585
586    fn timeline_player_value(
587        &self,
588        frame: &StatsSnapshotFrame,
589        player: &PlayerInfo,
590    ) -> SubtrActorResult<Value> {
591        let player_key = player_info_key(player)?;
592        let mut player_value = Map::new();
593        player_value.insert(
594            "player_id".to_owned(),
595            serialize_to_json_value(&player.remote_id)?,
596        );
597        player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
598        player_value.insert(
599            "is_team_0".to_owned(),
600            serialize_to_json_value(
601                &self
602                    .replay_meta
603                    .team_zero
604                    .iter()
605                    .any(|team_player| team_player.remote_id == player.remote_id),
606            )?,
607        );
608        player_value.insert(
609            "core".to_owned(),
610            self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
611                frame,
612                "core",
613                &player_key,
614            )?,
615        );
616        player_value.insert(
617            "backboard".to_owned(),
618            self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
619                frame,
620                "backboard",
621                &player_key,
622            )?,
623        );
624        player_value.insert(
625            "ceiling_shot".to_owned(),
626            self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
627                frame,
628                "ceiling_shot",
629                &player_key,
630            )?,
631        );
632        player_value.insert(
633            "double_tap".to_owned(),
634            self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
635                frame,
636                "double_tap",
637                &player_key,
638            )?,
639        );
640        player_value.insert(
641            "fifty_fifty".to_owned(),
642            self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
643                frame,
644                "fifty_fifty",
645                &player_key,
646            )?,
647        );
648        player_value.insert(
649            "speed_flip".to_owned(),
650            self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
651                frame,
652                "speed_flip",
653                &player_key,
654            )?,
655        );
656        player_value.insert(
657            "touch".to_owned(),
658            self.frame_player_stat_or_value_by_key(
659                frame,
660                "touch",
661                &player_key,
662                if frame.modules.contains_key("touch") {
663                    serialize_to_json_value(
664                        &TouchStats::default().with_complete_labeled_touch_counts(),
665                    )?
666                } else {
667                    default_json_value::<TouchStats>()
668                },
669            )?,
670        );
671        player_value.insert(
672            "musty_flick".to_owned(),
673            self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
674                frame,
675                "musty_flick",
676                &player_key,
677            )?,
678        );
679        player_value.insert(
680            "dodge_reset".to_owned(),
681            self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
682                frame,
683                "dodge_reset",
684                &player_key,
685            )?,
686        );
687        player_value.insert(
688            "ball_carry".to_owned(),
689            self.frame_player_stat_or_default_by_key::<BallCarryStats>(
690                frame,
691                "ball_carry",
692                &player_key,
693            )?,
694        );
695        player_value.insert(
696            "boost".to_owned(),
697            self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
698        );
699        player_value.insert(
700            "movement".to_owned(),
701            self.frame_player_stat_or_value_by_key(
702                frame,
703                "movement",
704                &player_key,
705                if frame.modules.contains_key("movement") {
706                    serialize_to_json_value(
707                        &MovementStats::default().with_complete_labeled_tracked_time(),
708                    )?
709                } else {
710                    default_json_value::<MovementStats>()
711                },
712            )?,
713        );
714        player_value.insert(
715            "positioning".to_owned(),
716            self.frame_player_stat_or_default_by_key::<PositioningStats>(
717                frame,
718                "positioning",
719                &player_key,
720            )?,
721        );
722        player_value.insert(
723            "powerslide".to_owned(),
724            self.frame_player_stat_or_default_by_key::<PowerslideStats>(
725                frame,
726                "powerslide",
727                &player_key,
728            )?,
729        );
730        player_value.insert(
731            "demo".to_owned(),
732            self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
733                frame,
734                "demo",
735                &player_key,
736            )?,
737        );
738        Ok(Value::Object(player_value))
739    }
740
741    fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
742    where
743        T: Default + Serialize,
744    {
745        frame
746            .modules
747            .get(module_name)
748            .and_then(Value::as_object)
749            .and_then(|module| module.get("stats"))
750            .cloned()
751            .unwrap_or_else(|| default_json_value::<T>())
752    }
753
754    fn frame_team_stat_or_default<T>(
755        &self,
756        frame: &StatsSnapshotFrame,
757        module_name: &str,
758        team_key: &str,
759    ) -> Value
760    where
761        T: Default + Serialize,
762    {
763        frame
764            .modules
765            .get(module_name)
766            .and_then(Value::as_object)
767            .and_then(|module| module.get(team_key))
768            .cloned()
769            .unwrap_or_else(|| default_json_value::<T>())
770    }
771
772    fn frame_player_stat_or_default_by_key<T>(
773        &self,
774        frame: &StatsSnapshotFrame,
775        module_name: &str,
776        player_key: &str,
777    ) -> SubtrActorResult<Value>
778    where
779        T: Default + Serialize,
780    {
781        self.frame_player_stat_or_value_by_key(
782            frame,
783            module_name,
784            player_key,
785            default_json_value::<T>(),
786        )
787    }
788
789    fn frame_player_stat_or_value_by_key(
790        &self,
791        frame: &StatsSnapshotFrame,
792        module_name: &str,
793        player_key: &str,
794        default_value: Value,
795    ) -> SubtrActorResult<Value> {
796        Ok(
797            player_stats_value_for_key(frame.modules.get(module_name), player_key)?
798                .cloned()
799                .unwrap_or(default_value),
800        )
801    }
802
803    fn frame_stats_or_default_typed<T>(
804        &self,
805        frame: &StatsSnapshotFrame,
806        module_name: &str,
807    ) -> SubtrActorResult<T>
808    where
809        T: Default + DeserializeOwned + Serialize,
810    {
811        decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
812    }
813
814    fn frame_team_stat_or_default_typed<T>(
815        &self,
816        frame: &StatsSnapshotFrame,
817        module_name: &str,
818        team_key: &str,
819    ) -> SubtrActorResult<T>
820    where
821        T: Default + DeserializeOwned + Serialize,
822    {
823        decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
824    }
825
826    fn frame_player_stat_or_default_typed_by_key<T>(
827        &self,
828        frame: &StatsSnapshotFrame,
829        module_name: &str,
830        player_key: &str,
831    ) -> SubtrActorResult<T>
832    where
833        T: Default + DeserializeOwned + Serialize,
834    {
835        self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
836    }
837
838    fn frame_player_stat_or_default_with_by_key<T, F>(
839        &self,
840        frame: &StatsSnapshotFrame,
841        module_name: &str,
842        player_key: &str,
843        default: F,
844    ) -> SubtrActorResult<T>
845    where
846        T: DeserializeOwned + Serialize,
847        F: FnOnce() -> T,
848    {
849        decode_json_value(self.frame_player_stat_or_value_by_key(
850            frame,
851            module_name,
852            player_key,
853            serialize_to_json_value(&default())?,
854        )?)
855    }
856
857    fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
858    where
859        T: DeserializeOwned,
860    {
861        decode_json_value(Value::Array(self.module_array(module_name, field)))
862    }
863
864    fn module_player_events<T, F>(
865        &self,
866        module_name: &str,
867        field: &str,
868        parse: F,
869    ) -> SubtrActorResult<Vec<T>>
870    where
871        F: Fn(&Value) -> SubtrActorResult<T>,
872    {
873        self.module_array(module_name, field)
874            .iter()
875            .map(parse)
876            .collect()
877    }
878
879    fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
880        self.modules
881            .get(module_name)
882            .and_then(Value::as_object)
883            .and_then(|module| module.get(field))
884            .and_then(Value::as_array)
885            .cloned()
886            .unwrap_or_default()
887    }
888}
889
890impl CapturedStatsData<ReplayStatsFrame> {
891    pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
892        let CapturedStatsData {
893            replay_meta,
894            config,
895            modules,
896            frames,
897        } = self;
898        CapturedStatsData::<StatsSnapshotFrame> {
899            replay_meta,
900            config,
901            modules,
902            frames: Vec::new(),
903        }
904        .into_replay_stats_timeline_with_frames(frames)
905    }
906}
907
908fn player_stats_value_for_key<'a>(
909    module: Option<&'a Value>,
910    player_key: &str,
911) -> SubtrActorResult<Option<&'a Value>> {
912    let Some(entries) = module
913        .and_then(Value::as_object)
914        .and_then(|module| module.get("player_stats"))
915        .and_then(Value::as_array)
916    else {
917        return Ok(None);
918    };
919
920    for entry in entries {
921        let Some(entry_object) = entry.as_object() else {
922            continue;
923        };
924        let Some(player_id) = entry_object.get("player_id") else {
925            continue;
926        };
927        let Some(player_stats) = entry_object.get("stats") else {
928            continue;
929        };
930        if player_id_key(player_id)? == player_key {
931            return Ok(Some(player_stats));
932        }
933    }
934
935    Ok(None)
936}
937
938fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
939    player_id_key(&serialize_to_json_value(&player.remote_id)?)
940}
941
942fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
943    serde_json::to_string(player_id).map_err(|error| {
944        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
945            error.to_string(),
946        ))
947    })
948}
949
950fn default_json_value<T>() -> Value
951where
952    T: Default + Serialize,
953{
954    serde_json::to_value(T::default()).expect("default stats should serialize to json")
955}
956
957fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
958where
959    T: DeserializeOwned,
960{
961    serde_json::from_value(value).map_err(|error| {
962        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
963            error.to_string(),
964        ))
965    })
966}
967
968fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
969    let object = json_object(value, "timeline event")?;
970    Ok(TimelineEvent {
971        time: json_required_f32(object, "time")?,
972        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
973        player_id: json_optional_remote_id(object.get("player_id"))?,
974        is_team_0: json_optional_bool(object.get("is_team_0")),
975    })
976}
977
978fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
979    let object = json_object(value, "backboard event")?;
980    Ok(BackboardBounceEvent {
981        time: json_required_f32(object, "time")?,
982        frame: json_required_usize(object, "frame")?,
983        player: json_required_remote_id(object, "player")?,
984        is_team_0: json_required_bool(object, "is_team_0")?,
985    })
986}
987
988fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
989    let object = json_object(value, "ceiling shot event")?;
990    Ok(CeilingShotEvent {
991        time: json_required_f32(object, "time")?,
992        frame: json_required_usize(object, "frame")?,
993        player: json_required_remote_id(object, "player")?,
994        is_team_0: json_required_bool(object, "is_team_0")?,
995        ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
996        ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
997        time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
998        ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
999        touch_position: json_required_vec3(object, "touch_position")?,
1000        local_ball_position: json_required_vec3(object, "local_ball_position")?,
1001        separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
1002        roof_alignment: json_required_f32(object, "roof_alignment")?,
1003        forward_alignment: json_required_f32(object, "forward_alignment")?,
1004        forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
1005        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
1006        confidence: json_required_f32(object, "confidence")?,
1007    })
1008}
1009
1010fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
1011    let object = json_object(value, "double tap event")?;
1012    Ok(DoubleTapEvent {
1013        time: json_required_f32(object, "time")?,
1014        frame: json_required_usize(object, "frame")?,
1015        player: json_required_remote_id(object, "player")?,
1016        is_team_0: json_required_bool(object, "is_team_0")?,
1017        backboard_time: json_required_f32(object, "backboard_time")?,
1018        backboard_frame: json_required_usize(object, "backboard_frame")?,
1019    })
1020}
1021
1022fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
1023    let object = json_object(value, "fifty fifty event")?;
1024    Ok(FiftyFiftyEvent {
1025        start_time: json_required_f32(object, "start_time")?,
1026        start_frame: json_required_usize(object, "start_frame")?,
1027        resolve_time: json_required_f32(object, "resolve_time")?,
1028        resolve_frame: json_required_usize(object, "resolve_frame")?,
1029        is_kickoff: json_required_bool(object, "is_kickoff")?,
1030        team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
1031        team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
1032        team_zero_position: json_required_vec3(object, "team_zero_position")?,
1033        team_one_position: json_required_vec3(object, "team_one_position")?,
1034        midpoint: json_required_vec3(object, "midpoint")?,
1035        plane_normal: json_required_vec3(object, "plane_normal")?,
1036        winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
1037        possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
1038    })
1039}
1040
1041fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
1042    let object = json_object(value, "speed flip event")?;
1043    Ok(SpeedFlipEvent {
1044        time: json_required_f32(object, "time")?,
1045        frame: json_required_usize(object, "frame")?,
1046        player: json_required_remote_id(object, "player")?,
1047        is_team_0: json_required_bool(object, "is_team_0")?,
1048        time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
1049        start_position: json_required_vec3(object, "start_position")?,
1050        end_position: json_required_vec3(object, "end_position")?,
1051        start_speed: json_required_f32(object, "start_speed")?,
1052        max_speed: json_required_f32(object, "max_speed")?,
1053        best_alignment: json_required_f32(object, "best_alignment")?,
1054        diagonal_score: json_required_f32(object, "diagonal_score")?,
1055        cancel_score: json_required_f32(object, "cancel_score")?,
1056        speed_score: json_required_f32(object, "speed_score")?,
1057        confidence: json_required_f32(object, "confidence")?,
1058    })
1059}
1060
1061fn parse_boost_pickup_comparison_event(
1062    value: &Value,
1063) -> SubtrActorResult<BoostPickupComparisonEvent> {
1064    let object = json_object(value, "boost pickup comparison event")?;
1065    Ok(BoostPickupComparisonEvent {
1066        comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
1067        frame: json_required_usize(object, "frame")?,
1068        time: json_required_f32(object, "time")?,
1069        player_id: json_required_remote_id(object, "player_id")?,
1070        is_team_0: json_required_bool(object, "is_team_0")?,
1071        pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
1072        field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
1073        activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
1074        reported_frame: json_optional_usize(object.get("reported_frame"))?,
1075        reported_time: json_optional_f32(object.get("reported_time"))?,
1076        inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
1077        inferred_time: json_optional_f32(object.get("inferred_time"))?,
1078        boost_before: json_optional_f32(object.get("boost_before"))?,
1079        boost_after: json_optional_f32(object.get("boost_after"))?,
1080    })
1081}
1082
1083fn json_object<'a>(
1084    value: &'a Value,
1085    context: &str,
1086) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
1087    value.as_object().ok_or_else(|| {
1088        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1089            "Expected {context} to be a JSON object"
1090        )))
1091    })
1092}
1093
1094fn json_required_value<'a>(
1095    object: &'a serde_json::Map<String, Value>,
1096    field: &str,
1097) -> SubtrActorResult<&'a Value> {
1098    object.get(field).ok_or_else(|| {
1099        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1100            "Missing JSON field '{field}'"
1101        )))
1102    })
1103}
1104
1105fn json_f32(value: &Value) -> Option<f32> {
1106    value.as_f64().map(|number| number as f32)
1107}
1108
1109fn json_required_f32(
1110    object: &serde_json::Map<String, Value>,
1111    field: &str,
1112) -> SubtrActorResult<f32> {
1113    json_f32(json_required_value(object, field)?).ok_or_else(|| {
1114        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1115            "Expected JSON field '{field}' to be a float"
1116        )))
1117    })
1118}
1119
1120fn json_required_usize(
1121    object: &serde_json::Map<String, Value>,
1122    field: &str,
1123) -> SubtrActorResult<usize> {
1124    json_required_value(object, field)?
1125        .as_u64()
1126        .map(|number| number as usize)
1127        .ok_or_else(|| {
1128            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1129                "Expected JSON field '{field}' to be an unsigned integer"
1130            )))
1131        })
1132}
1133
1134fn json_required_bool(
1135    object: &serde_json::Map<String, Value>,
1136    field: &str,
1137) -> SubtrActorResult<bool> {
1138    json_required_value(object, field)?
1139        .as_bool()
1140        .ok_or_else(|| {
1141            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1142                "Expected JSON field '{field}' to be a bool"
1143            )))
1144        })
1145}
1146
1147fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
1148    value.and_then(Value::as_bool)
1149}
1150
1151fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
1152    match value {
1153        None | Some(Value::Null) => Ok(None),
1154        Some(value) => json_f32(value).map(Some).ok_or_else(|| {
1155            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1156                "Expected optional JSON value to be a float".to_owned(),
1157            ))
1158        }),
1159    }
1160}
1161
1162fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
1163    match value {
1164        None | Some(Value::Null) => Ok(None),
1165        Some(value) => value
1166            .as_u64()
1167            .map(|number| Some(number as usize))
1168            .ok_or_else(|| {
1169                SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1170                    "Expected optional JSON value to be an unsigned integer".to_owned(),
1171                ))
1172            }),
1173    }
1174}
1175
1176fn json_required_vec3(
1177    object: &serde_json::Map<String, Value>,
1178    field: &str,
1179) -> SubtrActorResult<[f32; 3]> {
1180    let array = json_required_value(object, field)?
1181        .as_array()
1182        .ok_or_else(|| {
1183            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1184                "Expected JSON field '{field}' to be a 3-element array"
1185            )))
1186        })?;
1187    if array.len() != 3 {
1188        return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
1189            format!("Expected JSON field '{field}' to contain exactly 3 elements"),
1190        ));
1191    }
1192    Ok([
1193        json_f32(&array[0]).ok_or_else(|| {
1194            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1195                "Expected JSON field '{field}[0]' to be a float"
1196            )))
1197        })?,
1198        json_f32(&array[1]).ok_or_else(|| {
1199            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1200                "Expected JSON field '{field}[1]' to be a float"
1201            )))
1202        })?,
1203        json_f32(&array[2]).ok_or_else(|| {
1204            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1205                "Expected JSON field '{field}[2]' to be a float"
1206            )))
1207        })?,
1208    ])
1209}
1210
1211fn json_required_remote_id(
1212    object: &serde_json::Map<String, Value>,
1213    field: &str,
1214) -> SubtrActorResult<PlayerId> {
1215    json_remote_id(json_required_value(object, field)?)
1216}
1217
1218fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
1219    match value {
1220        None | Some(Value::Null) => Ok(None),
1221        Some(value) => Ok(Some(json_remote_id(value)?)),
1222    }
1223}
1224
1225fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
1226    let object = json_object(value, "remote id")?;
1227    if object.len() != 1 {
1228        return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
1229            "Expected remote id to contain exactly one variant".to_owned(),
1230        ));
1231    }
1232
1233    let (variant, payload) = object.iter().next().expect("validated single variant");
1234    match variant.as_str() {
1235        "PlayStation" => {
1236            let payload = json_object(payload, "playstation remote id")?;
1237            Ok(RemoteId::PlayStation(Ps4Id {
1238                online_id: json_u64(json_required_value(payload, "online_id")?)?,
1239                name: json_required_value(payload, "name")?
1240                    .as_str()
1241                    .ok_or_else(|| {
1242                        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1243                            "Expected PlayStation name to be a string".to_owned(),
1244                        ))
1245                    })?
1246                    .to_owned(),
1247                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
1248            }))
1249        }
1250        "PsyNet" => {
1251            let payload = json_object(payload, "psynet remote id")?;
1252            Ok(RemoteId::PsyNet(PsyNetId {
1253                online_id: json_u64(json_required_value(payload, "online_id")?)?,
1254                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
1255            }))
1256        }
1257        "SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
1258        "Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
1259        "Switch" => {
1260            let payload = json_object(payload, "switch remote id")?;
1261            Ok(RemoteId::Switch(SwitchId {
1262                online_id: json_u64(json_required_value(payload, "online_id")?)?,
1263                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
1264            }))
1265        }
1266        "Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
1267        "QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
1268        "Epic" => Ok(RemoteId::Epic(
1269            payload
1270                .as_str()
1271                .ok_or_else(|| {
1272                    SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1273                        "Expected Epic remote id payload to be a string".to_owned(),
1274                    ))
1275                })?
1276                .to_owned(),
1277        )),
1278        variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
1279            format!("Unknown remote id variant '{variant}'"),
1280        )),
1281    }
1282}
1283
1284fn json_u64(value: &Value) -> SubtrActorResult<u64> {
1285    value
1286        .as_u64()
1287        .or_else(|| value.as_str().and_then(|text| text.parse().ok()))
1288        .ok_or_else(|| {
1289            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1290                "Expected JSON value to be a u64".to_owned(),
1291            ))
1292        })
1293}
1294
1295fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
1296    value
1297        .as_array()
1298        .ok_or_else(|| {
1299            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1300                "Expected JSON value to be an array of bytes".to_owned(),
1301            ))
1302        })?
1303        .iter()
1304        .map(|entry| {
1305            entry
1306                .as_u64()
1307                .and_then(|number| u8::try_from(number).ok())
1308                .ok_or_else(|| {
1309                    SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1310                        "Expected JSON array entry to be a byte".to_owned(),
1311                    ))
1312                })
1313        })
1314        .collect()
1315}