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