Skip to main content

subtr_actor/collector/stats/
playback.rs

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