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