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