Skip to main content

subtr_actor/collector/stats/
playback.rs

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