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 ball_has_been_hit: Option<bool>,
18    pub kickoff_countdown_time: Option<i32>,
19    pub gameplay_phase: GameplayPhase,
20    pub is_live_play: bool,
21    pub modules: Modules,
22}
23
24pub type StatsSnapshotFrame = CapturedStatsFrame<Map<String, Value>>;
25
26#[derive(Debug, Clone, PartialEq, Serialize)]
27pub struct CapturedStatsData<Frame> {
28    pub replay_meta: ReplayMeta,
29    pub config: Map<String, Value>,
30    pub modules: Map<String, Value>,
31    pub frames: Vec<Frame>,
32}
33
34pub type StatsSnapshotData = CapturedStatsData<StatsSnapshotFrame>;
35
36impl<Modules> CapturedStatsFrame<Modules> {
37    pub fn map_modules<Mapped, F>(
38        self,
39        transform: F,
40    ) -> SubtrActorResult<CapturedStatsFrame<Mapped>>
41    where
42        F: FnOnce(Modules) -> SubtrActorResult<Mapped>,
43    {
44        Ok(CapturedStatsFrame {
45            frame_number: self.frame_number,
46            time: self.time,
47            dt: self.dt,
48            seconds_remaining: self.seconds_remaining,
49            game_state: self.game_state,
50            ball_has_been_hit: self.ball_has_been_hit,
51            kickoff_countdown_time: self.kickoff_countdown_time,
52            gameplay_phase: self.gameplay_phase,
53            is_live_play: self.is_live_play,
54            modules: transform(self.modules)?,
55        })
56    }
57}
58
59impl CapturedStatsData<StatsSnapshotFrame> {
60    pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
61        self.to_legacy_replay_stats_timeline()
62    }
63
64    #[deprecated(
65        note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
66    )]
67    pub fn into_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
68        self.into_legacy_replay_stats_timeline()
69    }
70
71    pub fn into_legacy_replay_stats_timeline_with_progress<F>(
72        self,
73        frame_interval: usize,
74        mut on_progress: F,
75    ) -> SubtrActorResult<ReplayStatsTimeline>
76    where
77        F: FnMut(usize, usize) -> SubtrActorResult<()>,
78    {
79        let frame_interval = frame_interval.max(1);
80        let total_frames = self.frames.len();
81        on_progress(0, total_frames)?;
82        let frames = self
83            .frames
84            .iter()
85            .enumerate()
86            .map(|(frame_index, frame)| {
87                let replay_frame = self.replay_stats_frame(frame)?;
88                let processed_frames = frame_index + 1;
89                if processed_frames == total_frames
90                    || processed_frames.is_multiple_of(frame_interval)
91                {
92                    on_progress(processed_frames, total_frames)?;
93                }
94                Ok(replay_frame)
95            })
96            .collect::<SubtrActorResult<Vec<_>>>()?;
97        self.to_replay_stats_timeline_with_frames(frames)
98    }
99
100    #[deprecated(
101        note = "use into_legacy_replay_stats_timeline_with_progress for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
102    )]
103    pub fn into_stats_timeline_with_progress<F>(
104        self,
105        frame_interval: usize,
106        on_progress: F,
107    ) -> SubtrActorResult<ReplayStatsTimeline>
108    where
109        F: FnMut(usize, usize) -> SubtrActorResult<()>,
110    {
111        self.into_legacy_replay_stats_timeline_with_progress(frame_interval, on_progress)
112    }
113
114    pub fn to_legacy_replay_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
115        self.to_replay_stats_timeline_with_frames(
116            self.frames
117                .iter()
118                .map(|frame| self.replay_stats_frame(frame))
119                .collect::<SubtrActorResult<Vec<_>>>()?,
120        )
121    }
122
123    #[deprecated(
124        note = "use to_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
125    )]
126    pub fn to_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
127        self.to_legacy_replay_stats_timeline()
128    }
129
130    pub(crate) fn into_replay_stats_timeline_with_frames(
131        self,
132        frames: Vec<ReplayStatsFrame>,
133    ) -> SubtrActorResult<ReplayStatsTimeline> {
134        self.to_replay_stats_timeline_with_frames(frames)
135    }
136
137    fn to_replay_stats_timeline_with_frames(
138        &self,
139        frames: Vec<ReplayStatsFrame>,
140    ) -> SubtrActorResult<ReplayStatsTimeline> {
141        Ok(ReplayStatsTimeline {
142            config: self.timeline_config(),
143            replay_meta: self.replay_meta.clone(),
144            events: self.timeline_event_sets_typed()?,
145            frames,
146        })
147    }
148
149    pub fn into_legacy_stats_timeline_value(self) -> SubtrActorResult<Value> {
150        self.to_legacy_stats_timeline_value()
151    }
152
153    #[deprecated(
154        note = "use into_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
155    )]
156    pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
157        self.into_legacy_stats_timeline_value()
158    }
159
160    pub fn to_legacy_stats_timeline_value(&self) -> SubtrActorResult<Value> {
161        let mut timeline = Map::new();
162        timeline.insert("config".to_owned(), self.timeline_config_value()?);
163        timeline.insert(
164            "replay_meta".to_owned(),
165            serialize_to_json_value(&self.replay_meta)?,
166        );
167        timeline.insert("events".to_owned(), self.timeline_event_sets_value());
168        timeline.insert(
169            "frames".to_owned(),
170            Value::Array(
171                self.frames
172                    .iter()
173                    .map(|frame| self.timeline_frame_value(frame))
174                    .collect::<SubtrActorResult<Vec<_>>>()?,
175            ),
176        );
177        Ok(Value::Object(timeline))
178    }
179
180    #[deprecated(
181        note = "use to_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
182    )]
183    pub fn to_stats_timeline_value(&self) -> SubtrActorResult<Value> {
184        self.to_legacy_stats_timeline_value()
185    }
186
187    fn timeline_events(&self) -> Vec<Value> {
188        let mut events = self.module_array("core", "timeline");
189        events.extend(self.module_array("demo", "timeline"));
190        events.sort_by(|left, right| {
191            let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
192            let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
193            left_time.total_cmp(&right_time)
194        });
195        events
196    }
197
198    fn timeline_events_typed(&self) -> SubtrActorResult<Vec<TimelineEvent>> {
199        self.timeline_events()
200            .iter()
201            .map(parse_timeline_event)
202            .collect()
203    }
204
205    fn goal_tag_events_typed(&self) -> SubtrActorResult<Vec<GoalTagEvent>> {
206        let mut events = Vec::new();
207        for module_name in [
208            "aerial_goal",
209            "high_aerial_goal",
210            "long_distance_goal",
211            "own_half_goal",
212            "empty_net_goal",
213            "counter_attack_goal",
214            "flick_goal",
215            "double_tap_goal",
216            "one_timer_goal",
217            "passing_goal",
218            "air_dribble_goal",
219            "flip_reset_goal",
220            "half_volley_goal",
221        ] {
222            events.extend(self.module_player_events(
223                module_name,
224                "events",
225                parse_goal_tag_event,
226            )?);
227        }
228        events.sort_by(|left, right| {
229            left.time
230                .total_cmp(&right.time)
231                .then_with(|| left.frame.cmp(&right.frame))
232                .then_with(|| left.goal_index.cmp(&right.goal_index))
233                .then_with(|| format!("{:?}", left.kind).cmp(&format!("{:?}", right.kind)))
234        });
235        Ok(events)
236    }
237
238    fn mechanic_events_typed(&self) -> SubtrActorResult<Vec<MechanicEvent>> {
239        let mut events = Vec::new();
240
241        for (index, value) in self.module_array("ball_carry", "events").iter().enumerate() {
242            events.push(parse_ball_carry_mechanic_event(value, index)?);
243        }
244        for (index, value) in self
245            .module_array("ceiling_shot", "events")
246            .iter()
247            .enumerate()
248        {
249            let event = parse_ceiling_shot_event(value)?;
250            events.push(span_mechanic_event(
251                "ceiling_shot",
252                index,
253                event.ceiling_contact_frame,
254                event.frame,
255                event.ceiling_contact_time,
256                event.time,
257                event.player,
258                event.is_team_0,
259            ));
260        }
261        for (index, value) in self
262            .module_array("wall_aerial", "events")
263            .iter()
264            .enumerate()
265        {
266            let event = parse_wall_aerial_event(value)?;
267            let mut mechanic_event = span_mechanic_event(
268                "wall_aerial",
269                index,
270                event.wall_contact_frame,
271                event.frame,
272                event.wall_contact_time,
273                event.time,
274                event.player,
275                event.is_team_0,
276            );
277            mechanic_event.properties = vec![mechanic_event_text_property(
278                "wall",
279                event.wall.as_label_value(),
280            )];
281            events.push(mechanic_event);
282        }
283        for (index, value) in self
284            .module_array("wall_aerial_shot", "events")
285            .iter()
286            .enumerate()
287        {
288            let event = parse_wall_aerial_shot_event(value)?;
289            let mut mechanic_event = span_mechanic_event(
290                "wall_aerial_shot",
291                index,
292                event.wall_contact_frame,
293                event.frame,
294                event.wall_contact_time,
295                event.time,
296                event.player,
297                event.is_team_0,
298            );
299            mechanic_event.properties = vec![mechanic_event_text_property(
300                "wall",
301                event.wall.as_label_value(),
302            )];
303            events.push(mechanic_event);
304        }
305        for (index, value) in self.module_array("center", "events").iter().enumerate() {
306            let event = parse_center_event(value)?;
307            events.push(span_mechanic_event(
308                "center",
309                index,
310                event.start_frame,
311                event.frame,
312                event.start_time,
313                event.time,
314                event.player,
315                event.is_team_0,
316            ));
317        }
318        for (index, value) in self
319            .module_array("dodge_reset", "on_ball_events")
320            .iter()
321            .enumerate()
322        {
323            events.push(parse_dodge_reset_mechanic_event(value, index)?);
324        }
325        for (index, value) in self.module_array("double_tap", "events").iter().enumerate() {
326            let event = parse_double_tap_event(value)?;
327            events.push(span_mechanic_event(
328                "double_tap",
329                index,
330                event.backboard_frame,
331                event.frame,
332                event.backboard_time,
333                event.time,
334                event.player,
335                event.is_team_0,
336            ));
337        }
338        for (index, value) in self.module_array("flick", "events").iter().enumerate() {
339            events.push(parse_flick_mechanic_event(value, index)?);
340        }
341        for (index, value) in self
342            .module_array("musty_flick", "events")
343            .iter()
344            .enumerate()
345        {
346            events.push(parse_musty_flick_mechanic_event(value, index)?);
347        }
348        for (index, value) in self.module_array("one_timer", "events").iter().enumerate() {
349            let event = parse_one_timer_event(value)?;
350            events.push(span_mechanic_event(
351                "one_timer",
352                index,
353                event.pass_start_frame,
354                event.frame,
355                event.pass_start_time,
356                event.time,
357                event.player,
358                event.is_team_0,
359            ));
360        }
361        for (index, value) in self.module_array("pass", "events").iter().enumerate() {
362            let event = parse_pass_event(value)?;
363            events.push(span_mechanic_event(
364                "pass",
365                index,
366                event.start_frame,
367                event.frame,
368                event.start_time,
369                event.time,
370                event.passer,
371                event.is_team_0,
372            ));
373        }
374        for (index, value) in self.module_array("speed_flip", "events").iter().enumerate() {
375            let event = parse_speed_flip_event(value)?;
376            events.push(moment_mechanic_event(
377                "speed_flip",
378                index,
379                event.frame,
380                event.time,
381                event.player,
382                event.is_team_0,
383            ));
384        }
385        for (index, value) in self.module_array("half_flip", "events").iter().enumerate() {
386            let event = parse_half_flip_event(value)?;
387            events.push(moment_mechanic_event(
388                "half_flip",
389                index,
390                event.frame,
391                event.time,
392                event.player,
393                event.is_team_0,
394            ));
395        }
396        for (index, value) in self
397            .module_array("half_volley", "events")
398            .iter()
399            .enumerate()
400        {
401            let event = parse_half_volley_event(value)?;
402            events.push(moment_mechanic_event(
403                "half_volley",
404                index,
405                event.frame,
406                event.time,
407                event.player,
408                event.is_team_0,
409            ));
410        }
411        for (index, value) in self.module_array("wavedash", "events").iter().enumerate() {
412            let event = parse_wavedash_event(value)?;
413            events.push(span_mechanic_event(
414                "wavedash",
415                index,
416                event.dodge_frame,
417                event.frame,
418                event.dodge_time,
419                event.time,
420                event.player,
421                event.is_team_0,
422            ));
423        }
424        events.sort_by(|left, right| {
425            let left_time = mechanic_event_start_time(left);
426            let right_time = mechanic_event_start_time(right);
427            left_time
428                .total_cmp(&right_time)
429                .then_with(|| left.kind.cmp(&right.kind))
430                .then_with(|| left.id.cmp(&right.id))
431        });
432        Ok(events)
433    }
434
435    fn goal_tag_events_value(&self) -> Vec<Value> {
436        let mut events = Vec::new();
437        for module_name in [
438            "aerial_goal",
439            "high_aerial_goal",
440            "long_distance_goal",
441            "own_half_goal",
442            "empty_net_goal",
443            "counter_attack_goal",
444            "flick_goal",
445            "double_tap_goal",
446            "one_timer_goal",
447            "passing_goal",
448            "air_dribble_goal",
449            "flip_reset_goal",
450            "half_volley_goal",
451        ] {
452            events.extend(self.module_array(module_name, "events"));
453        }
454        events.sort_by(|left, right| {
455            let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
456            let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
457            left_time.total_cmp(&right_time)
458        });
459        events
460    }
461
462    fn timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
463        Ok(ReplayStatsTimelineEvents {
464            timeline: self.timeline_events_typed()?,
465            core_player: self.module_player_events(
466                "core",
467                "player_events",
468                parse_core_player_stats_event,
469            )?,
470            core_team: self.module_player_events(
471                "core",
472                "team_events",
473                parse_core_team_stats_event,
474            )?,
475            possession: self.module_player_events(
476                "possession",
477                "events",
478                parse_possession_event,
479            )?,
480            pressure: self.module_player_events("pressure", "events", parse_pressure_event)?,
481            movement: self.module_player_events("movement", "events", parse_movement_event)?,
482            positioning: self.module_player_events(
483                "positioning",
484                "events",
485                parse_positioning_event,
486            )?,
487            rotation_player: self.module_player_events(
488                "rotation",
489                "player_events",
490                parse_rotation_player_event,
491            )?,
492            rotation_team: self.module_player_events(
493                "rotation",
494                "team_events",
495                parse_rotation_team_event,
496            )?,
497            mechanics: self.mechanic_events_typed()?,
498            goal_context: self.module_player_events(
499                "core",
500                "goal_context",
501                parse_goal_context_event,
502            )?,
503            backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
504            ceiling_shot: self.module_player_events(
505                "ceiling_shot",
506                "events",
507                parse_ceiling_shot_event,
508            )?,
509            wall_aerial: self.module_player_events(
510                "wall_aerial",
511                "events",
512                parse_wall_aerial_event,
513            )?,
514            wall_aerial_shot: self.module_player_events(
515                "wall_aerial_shot",
516                "events",
517                parse_wall_aerial_shot_event,
518            )?,
519            center: self.module_player_events("center", "events", parse_center_event)?,
520            flick: self.module_player_events("flick", "events", parse_flick_event)?,
521            musty_flick: self.module_player_events(
522                "musty_flick",
523                "events",
524                parse_musty_flick_event,
525            )?,
526            dodge_reset: self.module_player_events(
527                "dodge_reset",
528                "events",
529                parse_dodge_reset_event,
530            )?,
531            double_tap: self.module_player_events(
532                "double_tap",
533                "events",
534                parse_double_tap_event,
535            )?,
536            one_timer: self.module_player_events("one_timer", "events", parse_one_timer_event)?,
537            fifty_fifty: self.module_player_events(
538                "fifty_fifty",
539                "events",
540                parse_fifty_fifty_event,
541            )?,
542            pass: self.module_player_events("pass", "events", parse_pass_event)?,
543            pass_last_completed: self.module_player_events(
544                "pass",
545                "last_completed_events",
546                parse_pass_last_completed_event,
547            )?,
548            ball_carry: self.module_player_events(
549                "ball_carry",
550                "events",
551                parse_ball_carry_event,
552            )?,
553            goal_tags: self.goal_tag_events_typed()?,
554            rush: self.module_typed_array("rush", "events")?,
555            speed_flip: self.module_player_events(
556                "speed_flip",
557                "events",
558                parse_speed_flip_event,
559            )?,
560            half_flip: self.module_player_events("half_flip", "events", parse_half_flip_event)?,
561            half_volley: self.module_player_events(
562                "half_volley",
563                "events",
564                parse_half_volley_event,
565            )?,
566            wavedash: self.module_player_events("wavedash", "events", parse_wavedash_event)?,
567            whiff: self.module_player_events("whiff", "events", parse_whiff_event)?,
568            powerslide: self.module_player_events(
569                "powerslide",
570                "events",
571                parse_powerslide_event,
572            )?,
573            touch: self.module_player_events("touch", "events", parse_touch_stats_event)?,
574            touch_ball_movement: self.module_player_events(
575                "touch",
576                "ball_movement_events",
577                parse_touch_ball_movement_event,
578            )?,
579            touch_last_touch: self.module_player_events(
580                "touch",
581                "last_touch_events",
582                parse_touch_last_touch_event,
583            )?,
584            boost_pickups: self.module_player_events(
585                "boost",
586                "events",
587                parse_boost_pickup_comparison_event,
588            )?,
589            boost_ledger: self.module_player_events(
590                "boost",
591                "ledger_events",
592                parse_boost_ledger_event,
593            )?,
594            boost_state: self.module_player_events(
595                "boost",
596                "state_events",
597                parse_boost_state_event,
598            )?,
599            bump: self.module_player_events("bump", "events", parse_bump_event)?,
600        })
601    }
602
603    fn timeline_event_sets_value(&self) -> Value {
604        let mut events = Map::new();
605        events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
606        events.insert(
607            "core_player".to_owned(),
608            Value::Array(self.module_array("core", "player_events")),
609        );
610        events.insert(
611            "core_team".to_owned(),
612            Value::Array(self.module_array("core", "team_events")),
613        );
614        events.insert(
615            "possession".to_owned(),
616            Value::Array(self.module_array("possession", "events")),
617        );
618        events.insert(
619            "pressure".to_owned(),
620            Value::Array(self.module_array("pressure", "events")),
621        );
622        events.insert(
623            "movement".to_owned(),
624            Value::Array(self.module_array("movement", "events")),
625        );
626        events.insert(
627            "positioning".to_owned(),
628            Value::Array(self.module_array("positioning", "events")),
629        );
630        events.insert(
631            "rotation_player".to_owned(),
632            Value::Array(self.module_array("rotation", "player_events")),
633        );
634        events.insert(
635            "rotation_team".to_owned(),
636            Value::Array(self.module_array("rotation", "team_events")),
637        );
638        events.insert("mechanics".to_owned(), Value::Array(Vec::new()));
639        events.insert(
640            "backboard".to_owned(),
641            Value::Array(self.module_array("backboard", "events")),
642        );
643        events.insert(
644            "ceiling_shot".to_owned(),
645            Value::Array(self.module_array("ceiling_shot", "events")),
646        );
647        events.insert(
648            "wall_aerial".to_owned(),
649            Value::Array(self.module_array("wall_aerial", "events")),
650        );
651        events.insert(
652            "wall_aerial_shot".to_owned(),
653            Value::Array(self.module_array("wall_aerial_shot", "events")),
654        );
655        events.insert(
656            "center".to_owned(),
657            Value::Array(self.module_array("center", "events")),
658        );
659        events.insert(
660            "double_tap".to_owned(),
661            Value::Array(self.module_array("double_tap", "events")),
662        );
663        events.insert(
664            "one_timer".to_owned(),
665            Value::Array(self.module_array("one_timer", "events")),
666        );
667        events.insert(
668            "pass".to_owned(),
669            Value::Array(self.module_array("pass", "events")),
670        );
671        events.insert(
672            "goal_tags".to_owned(),
673            Value::Array(self.goal_tag_events_value()),
674        );
675        events.insert(
676            "fifty_fifty".to_owned(),
677            Value::Array(self.module_array("fifty_fifty", "events")),
678        );
679        events.insert(
680            "rush".to_owned(),
681            Value::Array(self.module_array("rush", "events")),
682        );
683        events.insert(
684            "speed_flip".to_owned(),
685            Value::Array(self.module_array("speed_flip", "events")),
686        );
687        events.insert(
688            "half_flip".to_owned(),
689            Value::Array(self.module_array("half_flip", "events")),
690        );
691        events.insert(
692            "half_volley".to_owned(),
693            Value::Array(self.module_array("half_volley", "events")),
694        );
695        events.insert(
696            "wavedash".to_owned(),
697            Value::Array(self.module_array("wavedash", "events")),
698        );
699        events.insert(
700            "whiff".to_owned(),
701            Value::Array(self.module_array("whiff", "events")),
702        );
703        events.insert(
704            "touch".to_owned(),
705            Value::Array(self.module_array("touch", "events")),
706        );
707        events.insert(
708            "touch_ball_movement".to_owned(),
709            Value::Array(self.module_array("touch", "ball_movement_events")),
710        );
711        events.insert(
712            "touch_last_touch".to_owned(),
713            Value::Array(self.module_array("touch", "last_touch_events")),
714        );
715        events.insert(
716            "boost_pickups".to_owned(),
717            Value::Array(self.module_array("boost", "events")),
718        );
719        events.insert(
720            "boost_ledger".to_owned(),
721            Value::Array(self.module_array("boost", "ledger_events")),
722        );
723        events.insert(
724            "boost_state".to_owned(),
725            Value::Array(self.module_array("boost", "state_events")),
726        );
727        events.insert(
728            "bump".to_owned(),
729            Value::Array(self.module_array("bump", "events")),
730        );
731        Value::Object(events)
732    }
733
734    fn timeline_config(&self) -> StatsTimelineConfig {
735        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
736        let pressure_config = self.config.get("pressure").and_then(Value::as_object);
737        let rotation_config = self.config.get("rotation").and_then(Value::as_object);
738        let rotation_defaults = RotationCalculatorConfig::default();
739        let rush_config = self.config.get("rush").and_then(Value::as_object);
740        let rush_defaults = RushCalculatorConfig::default();
741        let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
742        let high_aerial_goal_config = self
743            .config
744            .get("high_aerial_goal")
745            .and_then(Value::as_object);
746        let long_distance_goal_config = self
747            .config
748            .get("long_distance_goal")
749            .and_then(Value::as_object);
750        let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
751        let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
752        let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
753        let double_tap_goal_config = self
754            .config
755            .get("double_tap_goal")
756            .and_then(Value::as_object);
757        let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
758        let air_dribble_goal_config = self
759            .config
760            .get("air_dribble_goal")
761            .and_then(Value::as_object);
762        let flip_reset_goal_config = self
763            .config
764            .get("flip_reset_goal")
765            .and_then(Value::as_object);
766        let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
767        let half_volley_goal_config = self
768            .config
769            .get("half_volley_goal")
770            .and_then(Value::as_object);
771
772        StatsTimelineConfig {
773            most_back_forward_threshold_y: positioning_config
774                .and_then(|config| config.get("most_back_forward_threshold_y"))
775                .and_then(json_f32)
776                .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
777            level_ball_depth_margin: positioning_config
778                .and_then(|config| config.get("level_ball_depth_margin"))
779                .and_then(json_f32)
780                .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
781            pressure_neutral_zone_half_width_y: pressure_config
782                .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
783                .and_then(json_f32)
784                .unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
785            rotation_role_depth_margin: rotation_config
786                .and_then(|config| config.get("role_depth_margin"))
787                .and_then(json_f32)
788                .unwrap_or(rotation_defaults.role_depth_margin),
789            rotation_first_man_ambiguity_margin: rotation_config
790                .and_then(|config| config.get("first_man_ambiguity_margin"))
791                .and_then(json_f32)
792                .unwrap_or(rotation_defaults.first_man_ambiguity_margin),
793            rotation_first_man_debounce_seconds: rotation_config
794                .and_then(|config| config.get("first_man_debounce_seconds"))
795                .and_then(json_f32)
796                .unwrap_or(rotation_defaults.first_man_debounce_seconds),
797            rush_max_start_y: rush_config
798                .and_then(|config| config.get("rush_max_start_y"))
799                .and_then(json_f32)
800                .unwrap_or(rush_defaults.max_start_y),
801            rush_attack_support_distance_y: rush_config
802                .and_then(|config| config.get("rush_attack_support_distance_y"))
803                .and_then(json_f32)
804                .unwrap_or(rush_defaults.attack_support_distance_y),
805            rush_defender_distance_y: rush_config
806                .and_then(|config| config.get("rush_defender_distance_y"))
807                .and_then(json_f32)
808                .unwrap_or(rush_defaults.defender_distance_y),
809            rush_min_possession_retained_seconds: rush_config
810                .and_then(|config| config.get("rush_min_possession_retained_seconds"))
811                .and_then(json_f32)
812                .unwrap_or(rush_defaults.min_possession_retained_seconds),
813            aerial_goal_min_ball_z: aerial_goal_config
814                .and_then(|config| config.get("aerial_goal_min_ball_z"))
815                .and_then(json_f32)
816                .unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
817            high_aerial_goal_min_ball_z: high_aerial_goal_config
818                .and_then(|config| config.get("high_aerial_goal_min_ball_z"))
819                .and_then(json_f32)
820                .unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
821            long_distance_goal_max_attacking_y: long_distance_goal_config
822                .and_then(|config| config.get("long_distance_goal_max_attacking_y"))
823                .and_then(json_f32)
824                .unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
825            own_half_goal_max_attacking_y: own_half_goal_config
826                .and_then(|config| config.get("own_half_goal_max_attacking_y"))
827                .and_then(json_f32)
828                .unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
829            empty_net_min_defender_y_margin: empty_net_goal_config
830                .and_then(|config| config.get("empty_net_min_defender_y_margin"))
831                .and_then(json_f32)
832                .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
833            empty_net_min_defender_distance: empty_net_goal_config
834                .and_then(|config| config.get("empty_net_min_defender_distance"))
835                .and_then(json_f32)
836                .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
837            empty_net_max_touch_attacking_y: empty_net_goal_config
838                .and_then(|config| config.get("empty_net_max_touch_attacking_y"))
839                .and_then(json_f32)
840                .unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
841            flick_goal_max_event_to_goal_seconds: json_config_f32(
842                flick_goal_config,
843                "flick_goal_max_event_to_goal_seconds",
844                "flick_goal_max_event_to_touch_seconds",
845            )
846            .unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
847            double_tap_goal_max_event_to_goal_seconds: json_config_f32(
848                double_tap_goal_config,
849                "double_tap_goal_max_event_to_goal_seconds",
850                "double_tap_goal_max_event_to_touch_seconds",
851            )
852            .unwrap_or(DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds),
853            one_timer_goal_max_event_to_goal_seconds: json_config_f32(
854                one_timer_goal_config,
855                "one_timer_goal_max_event_to_goal_seconds",
856                "one_timer_goal_max_event_to_touch_seconds",
857            )
858            .unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
859            air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
860                air_dribble_goal_config,
861                "air_dribble_goal_max_end_to_goal_seconds",
862                "air_dribble_goal_max_end_to_touch_seconds",
863            )
864            .unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
865            flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
866                flip_reset_goal_config,
867                "flip_reset_goal_max_event_to_goal_seconds",
868                "flip_reset_goal_max_event_to_touch_seconds",
869            )
870            .unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
871            half_volley_max_bounce_to_touch_seconds: half_volley_config
872                .and_then(|config| config.get("half_volley_max_bounce_to_touch_seconds"))
873                .and_then(json_f32)
874                .unwrap_or(HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds),
875            half_volley_min_ball_speed: half_volley_config
876                .and_then(|config| config.get("half_volley_min_ball_speed"))
877                .and_then(json_f32)
878                .unwrap_or(HalfVolleyCalculatorConfig::default().min_ball_speed),
879            half_volley_goal_max_touch_to_goal_seconds: half_volley_goal_config
880                .and_then(|config| config.get("half_volley_goal_max_touch_to_goal_seconds"))
881                .and_then(json_f32)
882                .unwrap_or(HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds),
883            half_volley_goal_min_goal_alignment: half_volley_goal_config
884                .and_then(|config| config.get("half_volley_goal_min_goal_alignment"))
885                .and_then(json_f32)
886                .unwrap_or(HalfVolleyGoalCalculatorConfig::default().min_goal_alignment),
887        }
888    }
889
890    fn timeline_config_value(&self) -> SubtrActorResult<Value> {
891        let positioning_config = self.config.get("positioning").and_then(Value::as_object);
892        let pressure_config = self.config.get("pressure").and_then(Value::as_object);
893        let rotation_config = self.config.get("rotation").and_then(Value::as_object);
894        let rush_config = self.config.get("rush").and_then(Value::as_object);
895        let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
896        let high_aerial_goal_config = self
897            .config
898            .get("high_aerial_goal")
899            .and_then(Value::as_object);
900        let long_distance_goal_config = self
901            .config
902            .get("long_distance_goal")
903            .and_then(Value::as_object);
904        let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
905        let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
906        let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
907        let double_tap_goal_config = self
908            .config
909            .get("double_tap_goal")
910            .and_then(Value::as_object);
911        let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
912        let air_dribble_goal_config = self
913            .config
914            .get("air_dribble_goal")
915            .and_then(Value::as_object);
916        let flip_reset_goal_config = self
917            .config
918            .get("flip_reset_goal")
919            .and_then(Value::as_object);
920        let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
921        let half_volley_goal_config = self
922            .config
923            .get("half_volley_goal")
924            .and_then(Value::as_object);
925
926        let mut config = Map::new();
927        config.insert(
928            "most_back_forward_threshold_y".to_owned(),
929            serialize_to_json_value(
930                &positioning_config
931                    .and_then(|config| config.get("most_back_forward_threshold_y"))
932                    .and_then(Value::as_f64)
933                    .unwrap_or(
934                        PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
935                    ),
936            )?,
937        );
938        config.insert(
939            "level_ball_depth_margin".to_owned(),
940            serialize_to_json_value(
941                &positioning_config
942                    .and_then(|config| config.get("level_ball_depth_margin"))
943                    .and_then(Value::as_f64)
944                    .unwrap_or(
945                        PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
946                    ),
947            )?,
948        );
949        config.insert(
950            "pressure_neutral_zone_half_width_y".to_owned(),
951            serialize_to_json_value(
952                &pressure_config
953                    .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
954                    .and_then(Value::as_f64)
955                    .unwrap_or(
956                        PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
957                    ),
958            )?,
959        );
960        let rotation_defaults = RotationCalculatorConfig::default();
961        for (key, default_value) in [
962            (
963                "rotation_role_depth_margin",
964                rotation_defaults.role_depth_margin,
965            ),
966            (
967                "rotation_first_man_ambiguity_margin",
968                rotation_defaults.first_man_ambiguity_margin,
969            ),
970            (
971                "rotation_first_man_debounce_seconds",
972                rotation_defaults.first_man_debounce_seconds,
973            ),
974        ] {
975            let source_key = key.strip_prefix("rotation_").unwrap_or(key);
976            config.insert(
977                key.to_owned(),
978                serialize_to_json_value(
979                    &rotation_config
980                        .and_then(|config| config.get(source_key))
981                        .and_then(Value::as_f64)
982                        .unwrap_or(default_value as f64),
983                )?,
984            );
985        }
986        let rush_defaults = RushCalculatorConfig::default();
987        config.insert(
988            "rush_max_start_y".to_owned(),
989            serialize_to_json_value(
990                &rush_config
991                    .and_then(|config| config.get("rush_max_start_y"))
992                    .and_then(Value::as_f64)
993                    .unwrap_or(rush_defaults.max_start_y as f64),
994            )?,
995        );
996        config.insert(
997            "rush_attack_support_distance_y".to_owned(),
998            serialize_to_json_value(
999                &rush_config
1000                    .and_then(|config| config.get("rush_attack_support_distance_y"))
1001                    .and_then(Value::as_f64)
1002                    .unwrap_or(rush_defaults.attack_support_distance_y as f64),
1003            )?,
1004        );
1005        config.insert(
1006            "rush_defender_distance_y".to_owned(),
1007            serialize_to_json_value(
1008                &rush_config
1009                    .and_then(|config| config.get("rush_defender_distance_y"))
1010                    .and_then(Value::as_f64)
1011                    .unwrap_or(rush_defaults.defender_distance_y as f64),
1012            )?,
1013        );
1014        config.insert(
1015            "rush_min_possession_retained_seconds".to_owned(),
1016            serialize_to_json_value(
1017                &rush_config
1018                    .and_then(|config| config.get("rush_min_possession_retained_seconds"))
1019                    .and_then(Value::as_f64)
1020                    .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
1021            )?,
1022        );
1023        for (module_config, key, default_value) in [
1024            (
1025                aerial_goal_config,
1026                "aerial_goal_min_ball_z",
1027                AerialGoalCalculatorConfig::default().min_ball_z,
1028            ),
1029            (
1030                high_aerial_goal_config,
1031                "high_aerial_goal_min_ball_z",
1032                HighAerialGoalCalculatorConfig::default().min_ball_z,
1033            ),
1034            (
1035                long_distance_goal_config,
1036                "long_distance_goal_max_attacking_y",
1037                LongDistanceGoalCalculatorConfig::default().max_attacking_y,
1038            ),
1039            (
1040                own_half_goal_config,
1041                "own_half_goal_max_attacking_y",
1042                OwnHalfGoalCalculatorConfig::default().max_attacking_y,
1043            ),
1044            (
1045                empty_net_goal_config,
1046                "empty_net_min_defender_y_margin",
1047                EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
1048            ),
1049            (
1050                empty_net_goal_config,
1051                "empty_net_min_defender_distance",
1052                EmptyNetGoalCalculatorConfig::default().min_defender_distance,
1053            ),
1054            (
1055                empty_net_goal_config,
1056                "empty_net_max_touch_attacking_y",
1057                EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
1058            ),
1059            (
1060                flick_goal_config,
1061                "flick_goal_max_event_to_goal_seconds",
1062                FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
1063            ),
1064            (
1065                double_tap_goal_config,
1066                "double_tap_goal_max_event_to_goal_seconds",
1067                DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds,
1068            ),
1069            (
1070                one_timer_goal_config,
1071                "one_timer_goal_max_event_to_goal_seconds",
1072                OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
1073            ),
1074            (
1075                air_dribble_goal_config,
1076                "air_dribble_goal_max_end_to_goal_seconds",
1077                AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
1078            ),
1079            (
1080                flip_reset_goal_config,
1081                "flip_reset_goal_max_event_to_goal_seconds",
1082                FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
1083            ),
1084            (
1085                half_volley_config,
1086                "half_volley_max_bounce_to_touch_seconds",
1087                HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds,
1088            ),
1089            (
1090                half_volley_config,
1091                "half_volley_min_ball_speed",
1092                HalfVolleyCalculatorConfig::default().min_ball_speed,
1093            ),
1094            (
1095                half_volley_goal_config,
1096                "half_volley_goal_max_touch_to_goal_seconds",
1097                HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds,
1098            ),
1099            (
1100                half_volley_goal_config,
1101                "half_volley_goal_min_goal_alignment",
1102                HalfVolleyGoalCalculatorConfig::default().min_goal_alignment,
1103            ),
1104        ] {
1105            config.insert(
1106                key.to_owned(),
1107                serialize_to_json_value(
1108                    &module_config
1109                        .and_then(|config| config.get(key))
1110                        .and_then(Value::as_f64)
1111                        .unwrap_or(default_value as f64),
1112                )?,
1113            );
1114        }
1115        Ok(Value::Object(config))
1116    }
1117
1118    fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
1119        let mut timeline = Map::new();
1120        timeline.insert(
1121            "frame_number".to_owned(),
1122            serialize_to_json_value(&frame.frame_number)?,
1123        );
1124        timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
1125        timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
1126        timeline.insert(
1127            "seconds_remaining".to_owned(),
1128            serialize_to_json_value(&frame.seconds_remaining)?,
1129        );
1130        timeline.insert(
1131            "game_state".to_owned(),
1132            serialize_to_json_value(&frame.game_state)?,
1133        );
1134        timeline.insert(
1135            "ball_has_been_hit".to_owned(),
1136            serialize_to_json_value(&frame.ball_has_been_hit)?,
1137        );
1138        timeline.insert(
1139            "kickoff_countdown_time".to_owned(),
1140            serialize_to_json_value(&frame.kickoff_countdown_time)?,
1141        );
1142        timeline.insert(
1143            "gameplay_phase".to_owned(),
1144            serialize_to_json_value(&frame.gameplay_phase)?,
1145        );
1146        timeline.insert(
1147            "is_live_play".to_owned(),
1148            serialize_to_json_value(&frame.is_live_play)?,
1149        );
1150        timeline.insert(
1151            "fifty_fifty".to_owned(),
1152            self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
1153        );
1154        timeline.insert(
1155            "possession".to_owned(),
1156            self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
1157        );
1158        timeline.insert(
1159            "pressure".to_owned(),
1160            self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
1161        );
1162        timeline.insert(
1163            "rush".to_owned(),
1164            self.frame_stats_or_default::<RushStats>(frame, "rush"),
1165        );
1166        timeline.insert(
1167            "team_zero".to_owned(),
1168            self.timeline_team_value(frame, "team_zero")?,
1169        );
1170        timeline.insert(
1171            "team_one".to_owned(),
1172            self.timeline_team_value(frame, "team_one")?,
1173        );
1174        timeline.insert(
1175            "players".to_owned(),
1176            Value::Array(
1177                self.replay_meta
1178                    .player_order()
1179                    .map(|player| self.timeline_player_value(frame, player))
1180                    .collect::<SubtrActorResult<Vec<_>>>()?,
1181            ),
1182        );
1183        Ok(Value::Object(timeline))
1184    }
1185
1186    pub(crate) fn replay_stats_frame(
1187        &self,
1188        frame: &StatsSnapshotFrame,
1189    ) -> SubtrActorResult<ReplayStatsFrame> {
1190        Ok(ReplayStatsFrame {
1191            frame_number: frame.frame_number,
1192            time: frame.time,
1193            dt: frame.dt,
1194            seconds_remaining: frame.seconds_remaining,
1195            game_state: frame.game_state,
1196            ball_has_been_hit: frame.ball_has_been_hit,
1197            kickoff_countdown_time: frame.kickoff_countdown_time,
1198            gameplay_phase: frame.gameplay_phase,
1199            is_live_play: frame.is_live_play,
1200            team_zero: self.replay_team_stats(frame, "team_zero")?,
1201            team_one: self.replay_team_stats(frame, "team_one")?,
1202            players: self
1203                .replay_meta
1204                .player_order()
1205                .map(|player| self.replay_player_stats(frame, player))
1206                .collect::<SubtrActorResult<Vec<_>>>()?,
1207        })
1208    }
1209
1210    fn replay_team_stats(
1211        &self,
1212        frame: &StatsSnapshotFrame,
1213        team_key: &str,
1214    ) -> SubtrActorResult<TeamStatsSnapshot> {
1215        let is_team_zero = team_key == "team_zero";
1216        Ok(TeamStatsSnapshot {
1217            fifty_fifty: self
1218                .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
1219                .for_team(is_team_zero),
1220            possession: self
1221                .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
1222                .for_team(is_team_zero),
1223            pressure: self
1224                .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
1225                .for_team(is_team_zero),
1226            rotation: self.frame_team_stat_or_default_typed(frame, "rotation", team_key)?,
1227            rush: self
1228                .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
1229                .for_team(is_team_zero),
1230            core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
1231            backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
1232            double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
1233            one_timer: self.frame_team_stat_or_default_typed(frame, "one_timer", team_key)?,
1234            pass: self.frame_team_stat_or_default_typed(frame, "pass", team_key)?,
1235            ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
1236            air_dribble: self.frame_team_stat_or_default_typed(frame, "air_dribble", team_key)?,
1237            boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
1238            bump: self.frame_team_stat_or_default_typed(frame, "bump", team_key)?,
1239            half_volley: self.frame_team_stat_or_default_typed(frame, "half_volley", team_key)?,
1240            movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
1241            powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
1242            demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
1243        })
1244    }
1245
1246    fn replay_player_stats(
1247        &self,
1248        frame: &StatsSnapshotFrame,
1249        player: &PlayerInfo,
1250    ) -> SubtrActorResult<PlayerStatsSnapshot> {
1251        let player_key = player_info_key(player)?;
1252        Ok(PlayerStatsSnapshot {
1253            player_id: player.remote_id.clone(),
1254            name: player.name.clone(),
1255            is_team_0: self.is_team_zero_player(player),
1256            core: self.frame_core_player_stat_or_default_by_key(frame, &player_key)?,
1257            backboard: self.frame_player_stat_or_default_typed_by_key(
1258                frame,
1259                "backboard",
1260                &player_key,
1261            )?,
1262            ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
1263                frame,
1264                "ceiling_shot",
1265                &player_key,
1266            )?,
1267            wall_aerial: self.frame_player_stat_or_default_typed_by_key(
1268                frame,
1269                "wall_aerial",
1270                &player_key,
1271            )?,
1272            wall_aerial_shot: self.frame_player_stat_or_default_typed_by_key(
1273                frame,
1274                "wall_aerial_shot",
1275                &player_key,
1276            )?,
1277            double_tap: self.frame_player_stat_or_default_typed_by_key(
1278                frame,
1279                "double_tap",
1280                &player_key,
1281            )?,
1282            one_timer: self.frame_player_stat_or_default_typed_by_key(
1283                frame,
1284                "one_timer",
1285                &player_key,
1286            )?,
1287            pass: self.frame_player_stat_or_default_typed_by_key(frame, "pass", &player_key)?,
1288            fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
1289                frame,
1290                "fifty_fifty",
1291                &player_key,
1292            )?,
1293            speed_flip: self.frame_player_stat_or_default_typed_by_key(
1294                frame,
1295                "speed_flip",
1296                &player_key,
1297            )?,
1298            half_flip: self.frame_player_stat_or_default_typed_by_key(
1299                frame,
1300                "half_flip",
1301                &player_key,
1302            )?,
1303            wavedash: self.frame_player_stat_or_default_typed_by_key(
1304                frame,
1305                "wavedash",
1306                &player_key,
1307            )?,
1308            touch: if frame.modules.contains_key("touch") {
1309                self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
1310                    TouchStats::default().with_complete_labeled_touch_counts()
1311                })?
1312            } else {
1313                self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
1314            },
1315            whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
1316            flick: self.frame_player_stat_or_default_typed_by_key(frame, "flick", &player_key)?,
1317            musty_flick: self.frame_player_stat_or_default_typed_by_key(
1318                frame,
1319                "musty_flick",
1320                &player_key,
1321            )?,
1322            dodge_reset: self.frame_player_stat_or_default_typed_by_key(
1323                frame,
1324                "dodge_reset",
1325                &player_key,
1326            )?,
1327            ball_carry: self.frame_player_stat_or_default_typed_by_key(
1328                frame,
1329                "ball_carry",
1330                &player_key,
1331            )?,
1332            air_dribble: self.frame_player_stat_or_default_typed_by_key(
1333                frame,
1334                "air_dribble",
1335                &player_key,
1336            )?,
1337            boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
1338            bump: self.frame_player_stat_or_default_typed_by_key(frame, "bump", &player_key)?,
1339            half_volley: self.frame_player_stat_or_default_typed_by_key(
1340                frame,
1341                "half_volley",
1342                &player_key,
1343            )?,
1344            movement: self.frame_player_stat_or_default_with_by_key(
1345                frame,
1346                "movement",
1347                &player_key,
1348                || MovementStats::default().with_complete_labeled_tracked_time(),
1349            )?,
1350            positioning: self.frame_player_stat_or_default_typed_by_key(
1351                frame,
1352                "positioning",
1353                &player_key,
1354            )?,
1355            rotation: self.frame_player_stat_or_default_typed_by_key(
1356                frame,
1357                "rotation",
1358                &player_key,
1359            )?,
1360            powerslide: self.frame_player_stat_or_default_typed_by_key(
1361                frame,
1362                "powerslide",
1363                &player_key,
1364            )?,
1365            demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
1366        })
1367    }
1368
1369    fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
1370        self.replay_meta
1371            .team_zero
1372            .iter()
1373            .any(|team_player| team_player.remote_id == player.remote_id)
1374    }
1375
1376    fn timeline_team_value(
1377        &self,
1378        frame: &StatsSnapshotFrame,
1379        team_key: &str,
1380    ) -> SubtrActorResult<Value> {
1381        let is_team_zero = team_key == "team_zero";
1382        let mut team = Map::new();
1383        team.insert(
1384            "fifty_fifty".to_owned(),
1385            serialize_to_json_value(
1386                &self
1387                    .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
1388                    .for_team(is_team_zero),
1389            )?,
1390        );
1391        team.insert(
1392            "possession".to_owned(),
1393            serialize_to_json_value(
1394                &self
1395                    .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
1396                    .for_team(is_team_zero),
1397            )?,
1398        );
1399        team.insert(
1400            "pressure".to_owned(),
1401            serialize_to_json_value(
1402                &self
1403                    .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
1404                    .for_team(is_team_zero),
1405            )?,
1406        );
1407        team.insert(
1408            "rotation".to_owned(),
1409            self.frame_team_stat_or_default::<RotationTeamStats>(frame, "rotation", team_key),
1410        );
1411        team.insert(
1412            "rush".to_owned(),
1413            serialize_to_json_value(
1414                &self
1415                    .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
1416                    .for_team(is_team_zero),
1417            )?,
1418        );
1419        team.insert(
1420            "core".to_owned(),
1421            self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
1422        );
1423        team.insert(
1424            "backboard".to_owned(),
1425            self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
1426        );
1427        team.insert(
1428            "double_tap".to_owned(),
1429            self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
1430        );
1431        team.insert(
1432            "one_timer".to_owned(),
1433            self.frame_team_stat_or_default::<OneTimerTeamStats>(frame, "one_timer", team_key),
1434        );
1435        team.insert(
1436            "pass".to_owned(),
1437            self.frame_team_stat_or_default::<PassTeamStats>(frame, "pass", team_key),
1438        );
1439        team.insert(
1440            "ball_carry".to_owned(),
1441            self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
1442        );
1443        team.insert(
1444            "air_dribble".to_owned(),
1445            self.frame_team_stat_or_default::<AirDribbleStats>(frame, "air_dribble", team_key),
1446        );
1447        team.insert(
1448            "boost".to_owned(),
1449            self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
1450        );
1451        team.insert(
1452            "bump".to_owned(),
1453            self.frame_team_stat_or_default::<BumpTeamStats>(frame, "bump", team_key),
1454        );
1455        team.insert(
1456            "half_volley".to_owned(),
1457            self.frame_team_stat_or_default::<HalfVolleyTeamStats>(frame, "half_volley", team_key),
1458        );
1459        team.insert(
1460            "movement".to_owned(),
1461            self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
1462        );
1463        team.insert(
1464            "powerslide".to_owned(),
1465            self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
1466        );
1467        team.insert(
1468            "demo".to_owned(),
1469            self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
1470        );
1471        Ok(Value::Object(team))
1472    }
1473
1474    fn timeline_player_value(
1475        &self,
1476        frame: &StatsSnapshotFrame,
1477        player: &PlayerInfo,
1478    ) -> SubtrActorResult<Value> {
1479        let player_key = player_info_key(player)?;
1480        let mut player_value = Map::new();
1481        player_value.insert(
1482            "player_id".to_owned(),
1483            serialize_to_json_value(&player.remote_id)?,
1484        );
1485        player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
1486        player_value.insert(
1487            "is_team_0".to_owned(),
1488            serialize_to_json_value(
1489                &self
1490                    .replay_meta
1491                    .team_zero
1492                    .iter()
1493                    .any(|team_player| team_player.remote_id == player.remote_id),
1494            )?,
1495        );
1496        player_value.insert(
1497            "core".to_owned(),
1498            self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
1499                frame,
1500                "core",
1501                &player_key,
1502            )?,
1503        );
1504        player_value.insert(
1505            "backboard".to_owned(),
1506            self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
1507                frame,
1508                "backboard",
1509                &player_key,
1510            )?,
1511        );
1512        player_value.insert(
1513            "ceiling_shot".to_owned(),
1514            self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
1515                frame,
1516                "ceiling_shot",
1517                &player_key,
1518            )?,
1519        );
1520        player_value.insert(
1521            "wall_aerial".to_owned(),
1522            self.frame_player_stat_or_default_by_key::<WallAerialStats>(
1523                frame,
1524                "wall_aerial",
1525                &player_key,
1526            )?,
1527        );
1528        player_value.insert(
1529            "wall_aerial_shot".to_owned(),
1530            self.frame_player_stat_or_default_by_key::<WallAerialShotStats>(
1531                frame,
1532                "wall_aerial_shot",
1533                &player_key,
1534            )?,
1535        );
1536        player_value.insert(
1537            "double_tap".to_owned(),
1538            self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
1539                frame,
1540                "double_tap",
1541                &player_key,
1542            )?,
1543        );
1544        player_value.insert(
1545            "one_timer".to_owned(),
1546            self.frame_player_stat_or_default_by_key::<OneTimerPlayerStats>(
1547                frame,
1548                "one_timer",
1549                &player_key,
1550            )?,
1551        );
1552        player_value.insert(
1553            "pass".to_owned(),
1554            self.frame_player_stat_or_default_by_key::<PassPlayerStats>(
1555                frame,
1556                "pass",
1557                &player_key,
1558            )?,
1559        );
1560        player_value.insert(
1561            "fifty_fifty".to_owned(),
1562            self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
1563                frame,
1564                "fifty_fifty",
1565                &player_key,
1566            )?,
1567        );
1568        player_value.insert(
1569            "speed_flip".to_owned(),
1570            self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
1571                frame,
1572                "speed_flip",
1573                &player_key,
1574            )?,
1575        );
1576        player_value.insert(
1577            "half_flip".to_owned(),
1578            self.frame_player_stat_or_default_by_key::<HalfFlipStats>(
1579                frame,
1580                "half_flip",
1581                &player_key,
1582            )?,
1583        );
1584        player_value.insert(
1585            "half_volley".to_owned(),
1586            self.frame_player_stat_or_default_by_key::<HalfVolleyPlayerStats>(
1587                frame,
1588                "half_volley",
1589                &player_key,
1590            )?,
1591        );
1592        player_value.insert(
1593            "wavedash".to_owned(),
1594            self.frame_player_stat_or_default_by_key::<WavedashStats>(
1595                frame,
1596                "wavedash",
1597                &player_key,
1598            )?,
1599        );
1600        player_value.insert(
1601            "touch".to_owned(),
1602            self.frame_player_stat_or_value_by_key(
1603                frame,
1604                "touch",
1605                &player_key,
1606                if frame.modules.contains_key("touch") {
1607                    serialize_to_json_value(
1608                        &TouchStats::default().with_complete_labeled_touch_counts(),
1609                    )?
1610                } else {
1611                    default_json_value::<TouchStats>()
1612                },
1613            )?,
1614        );
1615        player_value.insert(
1616            "whiff".to_owned(),
1617            self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
1618        );
1619        player_value.insert(
1620            "flick".to_owned(),
1621            self.frame_player_stat_or_default_by_key::<FlickStats>(frame, "flick", &player_key)?,
1622        );
1623        player_value.insert(
1624            "musty_flick".to_owned(),
1625            self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
1626                frame,
1627                "musty_flick",
1628                &player_key,
1629            )?,
1630        );
1631        player_value.insert(
1632            "dodge_reset".to_owned(),
1633            self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
1634                frame,
1635                "dodge_reset",
1636                &player_key,
1637            )?,
1638        );
1639        player_value.insert(
1640            "ball_carry".to_owned(),
1641            self.frame_player_stat_or_default_by_key::<BallCarryStats>(
1642                frame,
1643                "ball_carry",
1644                &player_key,
1645            )?,
1646        );
1647        player_value.insert(
1648            "air_dribble".to_owned(),
1649            self.frame_player_stat_or_default_by_key::<AirDribbleStats>(
1650                frame,
1651                "air_dribble",
1652                &player_key,
1653            )?,
1654        );
1655        player_value.insert(
1656            "boost".to_owned(),
1657            self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
1658        );
1659        player_value.insert(
1660            "bump".to_owned(),
1661            self.frame_player_stat_or_default_by_key::<BumpPlayerStats>(
1662                frame,
1663                "bump",
1664                &player_key,
1665            )?,
1666        );
1667        player_value.insert(
1668            "movement".to_owned(),
1669            self.frame_player_stat_or_value_by_key(
1670                frame,
1671                "movement",
1672                &player_key,
1673                if frame.modules.contains_key("movement") {
1674                    serialize_to_json_value(
1675                        &MovementStats::default().with_complete_labeled_tracked_time(),
1676                    )?
1677                } else {
1678                    default_json_value::<MovementStats>()
1679                },
1680            )?,
1681        );
1682        player_value.insert(
1683            "positioning".to_owned(),
1684            self.frame_player_stat_or_default_by_key::<PositioningStats>(
1685                frame,
1686                "positioning",
1687                &player_key,
1688            )?,
1689        );
1690        player_value.insert(
1691            "rotation".to_owned(),
1692            self.frame_player_stat_or_default_by_key::<RotationPlayerStats>(
1693                frame,
1694                "rotation",
1695                &player_key,
1696            )?,
1697        );
1698        player_value.insert(
1699            "powerslide".to_owned(),
1700            self.frame_player_stat_or_default_by_key::<PowerslideStats>(
1701                frame,
1702                "powerslide",
1703                &player_key,
1704            )?,
1705        );
1706        player_value.insert(
1707            "demo".to_owned(),
1708            self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
1709                frame,
1710                "demo",
1711                &player_key,
1712            )?,
1713        );
1714        Ok(Value::Object(player_value))
1715    }
1716
1717    fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
1718    where
1719        T: Default + Serialize,
1720    {
1721        frame
1722            .modules
1723            .get(module_name)
1724            .and_then(Value::as_object)
1725            .and_then(|module| module.get("stats"))
1726            .cloned()
1727            .unwrap_or_else(|| default_json_value::<T>())
1728    }
1729
1730    fn frame_team_stat_or_default<T>(
1731        &self,
1732        frame: &StatsSnapshotFrame,
1733        module_name: &str,
1734        team_key: &str,
1735    ) -> Value
1736    where
1737        T: Default + Serialize,
1738    {
1739        frame
1740            .modules
1741            .get(module_name)
1742            .and_then(Value::as_object)
1743            .and_then(|module| module.get(team_key))
1744            .cloned()
1745            .unwrap_or_else(|| default_json_value::<T>())
1746    }
1747
1748    fn frame_player_stat_or_default_by_key<T>(
1749        &self,
1750        frame: &StatsSnapshotFrame,
1751        module_name: &str,
1752        player_key: &str,
1753    ) -> SubtrActorResult<Value>
1754    where
1755        T: Default + Serialize,
1756    {
1757        self.frame_player_stat_or_value_by_key(
1758            frame,
1759            module_name,
1760            player_key,
1761            default_json_value::<T>(),
1762        )
1763    }
1764
1765    fn frame_player_stat_or_value_by_key(
1766        &self,
1767        frame: &StatsSnapshotFrame,
1768        module_name: &str,
1769        player_key: &str,
1770        default_value: Value,
1771    ) -> SubtrActorResult<Value> {
1772        Ok(
1773            player_stats_value_for_key(frame.modules.get(module_name), player_key)?
1774                .cloned()
1775                .unwrap_or(default_value),
1776        )
1777    }
1778
1779    fn frame_stats_or_default_typed<T>(
1780        &self,
1781        frame: &StatsSnapshotFrame,
1782        module_name: &str,
1783    ) -> SubtrActorResult<T>
1784    where
1785        T: Default + DeserializeOwned + Serialize,
1786    {
1787        decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
1788    }
1789
1790    fn frame_team_stat_or_default_typed<T>(
1791        &self,
1792        frame: &StatsSnapshotFrame,
1793        module_name: &str,
1794        team_key: &str,
1795    ) -> SubtrActorResult<T>
1796    where
1797        T: Default + DeserializeOwned + Serialize,
1798    {
1799        decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
1800    }
1801
1802    fn frame_player_stat_or_default_typed_by_key<T>(
1803        &self,
1804        frame: &StatsSnapshotFrame,
1805        module_name: &str,
1806        player_key: &str,
1807    ) -> SubtrActorResult<T>
1808    where
1809        T: Default + DeserializeOwned + Serialize,
1810    {
1811        self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
1812    }
1813
1814    fn frame_core_player_stat_or_default_by_key(
1815        &self,
1816        frame: &StatsSnapshotFrame,
1817        player_key: &str,
1818    ) -> SubtrActorResult<CorePlayerStats> {
1819        decode_core_player_stats_value(self.frame_player_stat_or_value_by_key(
1820            frame,
1821            "core",
1822            player_key,
1823            default_json_value::<CorePlayerStats>(),
1824        )?)
1825    }
1826
1827    fn frame_player_stat_or_default_with_by_key<T, F>(
1828        &self,
1829        frame: &StatsSnapshotFrame,
1830        module_name: &str,
1831        player_key: &str,
1832        default: F,
1833    ) -> SubtrActorResult<T>
1834    where
1835        T: DeserializeOwned + Serialize,
1836        F: FnOnce() -> T,
1837    {
1838        decode_json_value(self.frame_player_stat_or_value_by_key(
1839            frame,
1840            module_name,
1841            player_key,
1842            serialize_to_json_value(&default())?,
1843        )?)
1844    }
1845
1846    fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
1847    where
1848        T: DeserializeOwned,
1849    {
1850        decode_json_value(Value::Array(self.module_array(module_name, field)))
1851    }
1852
1853    fn module_player_events<T, F>(
1854        &self,
1855        module_name: &str,
1856        field: &str,
1857        parse: F,
1858    ) -> SubtrActorResult<Vec<T>>
1859    where
1860        F: Fn(&Value) -> SubtrActorResult<T>,
1861    {
1862        self.module_array(module_name, field)
1863            .iter()
1864            .map(parse)
1865            .collect()
1866    }
1867
1868    fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
1869        self.modules
1870            .get(module_name)
1871            .and_then(Value::as_object)
1872            .and_then(|module| module.get(field))
1873            .and_then(Value::as_array)
1874            .cloned()
1875            .unwrap_or_default()
1876    }
1877}
1878
1879impl CapturedStatsData<ReplayStatsFrame> {
1880    pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
1881        let CapturedStatsData {
1882            replay_meta,
1883            config,
1884            modules,
1885            frames,
1886        } = self;
1887        CapturedStatsData::<StatsSnapshotFrame> {
1888            replay_meta,
1889            config,
1890            modules,
1891            frames: Vec::new(),
1892        }
1893        .into_replay_stats_timeline_with_frames(frames)
1894    }
1895
1896    #[deprecated(
1897        note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
1898    )]
1899    pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
1900        self.into_legacy_replay_stats_timeline()
1901    }
1902}
1903
1904fn player_stats_value_for_key<'a>(
1905    module: Option<&'a Value>,
1906    player_key: &str,
1907) -> SubtrActorResult<Option<&'a Value>> {
1908    let Some(entries) = module
1909        .and_then(Value::as_object)
1910        .and_then(|module| module.get("player_stats"))
1911        .and_then(Value::as_array)
1912    else {
1913        return Ok(None);
1914    };
1915
1916    for entry in entries {
1917        let Some(entry_object) = entry.as_object() else {
1918            continue;
1919        };
1920        let Some(player_id) = entry_object.get("player_id") else {
1921            continue;
1922        };
1923        let Some(player_stats) = entry_object.get("stats") else {
1924            continue;
1925        };
1926        if player_id_key(player_id)? == player_key {
1927            return Ok(Some(player_stats));
1928        }
1929    }
1930
1931    Ok(None)
1932}
1933
1934fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
1935    player_id_key(&serialize_to_json_value(&player.remote_id)?)
1936}
1937
1938fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
1939    serde_json::to_string(player_id).map_err(|error| {
1940        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1941            error.to_string(),
1942        ))
1943    })
1944}
1945
1946fn default_json_value<T>() -> Value
1947where
1948    T: Default + Serialize,
1949{
1950    serde_json::to_value(T::default()).expect("default stats should serialize to json")
1951}
1952
1953fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
1954where
1955    T: DeserializeOwned,
1956{
1957    serde_json::from_value(value).map_err(|error| {
1958        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1959            error.to_string(),
1960        ))
1961    })
1962}
1963
1964fn decode_core_player_stats_value(mut value: Value) -> SubtrActorResult<CorePlayerStats> {
1965    normalize_core_player_stats_snapshot(&mut value)?;
1966    decode_json_value(value)
1967}
1968
1969fn normalize_core_player_stats_snapshot(value: &mut Value) -> SubtrActorResult<()> {
1970    let Some(object) = value.as_object_mut() else {
1971        return Ok(());
1972    };
1973
1974    insert_cumulative_from_average(
1975        object,
1976        "cumulative_boost_on_goals_against",
1977        "average_boost_on_goals_against",
1978        "goal_against_boost_sample_count",
1979    )?;
1980    insert_cumulative_from_average(
1981        object,
1982        "cumulative_average_boost_in_goal_against_leadup",
1983        "average_boost_in_goal_against_leadup",
1984        "goal_against_boost_leadup_sample_count",
1985    )?;
1986    insert_cumulative_from_average(
1987        object,
1988        "cumulative_min_boost_in_goal_against_leadup",
1989        "average_min_boost_in_goal_against_leadup",
1990        "goal_against_boost_leadup_sample_count",
1991    )?;
1992    insert_cumulative_from_average(
1993        object,
1994        "cumulative_goal_against_position_x",
1995        "average_goal_against_position_x",
1996        "goal_against_position_sample_count",
1997    )?;
1998    insert_cumulative_from_average(
1999        object,
2000        "cumulative_goal_against_position_y",
2001        "average_goal_against_position_y",
2002        "goal_against_position_sample_count",
2003    )?;
2004    insert_cumulative_from_average(
2005        object,
2006        "cumulative_goal_against_position_z",
2007        "average_goal_against_position_z",
2008        "goal_against_position_sample_count",
2009    )?;
2010    insert_cumulative_from_average(
2011        object,
2012        "cumulative_scoring_goal_last_touch_position_x",
2013        "average_scoring_goal_last_touch_position_x",
2014        "scoring_goal_last_touch_position_sample_count",
2015    )?;
2016    insert_cumulative_from_average(
2017        object,
2018        "cumulative_scoring_goal_last_touch_position_y",
2019        "average_scoring_goal_last_touch_position_y",
2020        "scoring_goal_last_touch_position_sample_count",
2021    )?;
2022    insert_cumulative_from_average(
2023        object,
2024        "cumulative_scoring_goal_last_touch_position_z",
2025        "average_scoring_goal_last_touch_position_z",
2026        "scoring_goal_last_touch_position_sample_count",
2027    )?;
2028    insert_cumulative_from_average(
2029        object,
2030        "cumulative_goal_ball_air_time",
2031        "average_goal_ball_air_time",
2032        "goal_ball_air_time_sample_count",
2033    )?;
2034
2035    if let Value::Object(defaults) = default_json_value::<CorePlayerStats>() {
2036        for (field, default_value) in defaults {
2037            object.entry(field).or_insert(default_value);
2038        }
2039    }
2040
2041    Ok(())
2042}
2043
2044fn insert_cumulative_from_average(
2045    object: &mut Map<String, Value>,
2046    cumulative_field: &str,
2047    average_field: &str,
2048    sample_count_field: &str,
2049) -> SubtrActorResult<()> {
2050    if object.contains_key(cumulative_field) {
2051        return Ok(());
2052    }
2053
2054    let average = object
2055        .get(average_field)
2056        .and_then(Value::as_f64)
2057        .unwrap_or(0.0) as f32;
2058    let sample_count = object
2059        .get(sample_count_field)
2060        .and_then(Value::as_u64)
2061        .unwrap_or(0) as f32;
2062    object.insert(
2063        cumulative_field.to_owned(),
2064        serialize_to_json_value(&(average * sample_count))?,
2065    );
2066
2067    Ok(())
2068}
2069
2070fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
2071    let object = json_object(value, "timeline event")?;
2072    Ok(TimelineEvent {
2073        time: json_required_f32(object, "time")?,
2074        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
2075        player_id: json_optional_remote_id(object.get("player_id"))?,
2076        is_team_0: json_optional_bool(object.get("is_team_0")),
2077    })
2078}
2079
2080fn moment_mechanic_event(
2081    kind: &str,
2082    index: usize,
2083    frame: usize,
2084    time: f32,
2085    player_id: PlayerId,
2086    is_team_0: bool,
2087) -> MechanicEvent {
2088    MechanicEvent {
2089        id: format!("{kind}:{frame}:{index}"),
2090        kind: kind.to_owned(),
2091        player_id,
2092        is_team_0,
2093        timing: MechanicTiming::Moment { frame, time },
2094        properties: Vec::new(),
2095    }
2096}
2097
2098#[allow(clippy::too_many_arguments)]
2099fn span_mechanic_event(
2100    kind: &str,
2101    index: usize,
2102    start_frame: usize,
2103    end_frame: usize,
2104    start_time: f32,
2105    end_time: f32,
2106    player_id: PlayerId,
2107    is_team_0: bool,
2108) -> MechanicEvent {
2109    MechanicEvent {
2110        id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
2111        kind: kind.to_owned(),
2112        player_id,
2113        is_team_0,
2114        timing: MechanicTiming::Span {
2115            start_frame,
2116            end_frame,
2117            start_time,
2118            end_time,
2119        },
2120        properties: Vec::new(),
2121    }
2122}
2123
2124fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
2125    match event.timing {
2126        MechanicTiming::Moment { time, .. } => time,
2127        MechanicTiming::Span { start_time, .. } => start_time,
2128    }
2129}
2130
2131fn mechanic_event_text_property(key: &str, value: &str) -> MechanicEventProperty {
2132    MechanicEventProperty {
2133        key: key.to_owned(),
2134        value: MechanicEventPropertyValue::Text(value.to_owned()),
2135    }
2136}
2137
2138fn mechanic_event_unsigned_property(key: &str, value: u32) -> MechanicEventProperty {
2139    MechanicEventProperty {
2140        key: key.to_owned(),
2141        value: MechanicEventPropertyValue::Unsigned(value),
2142    }
2143}
2144
2145fn ball_carry_mechanic_event_properties(
2146    object: &serde_json::Map<String, Value>,
2147) -> Vec<MechanicEventProperty> {
2148    let mut properties = Vec::new();
2149    if let Some(origin) = object.get("air_dribble_origin").and_then(Value::as_str) {
2150        properties.push(mechanic_event_text_property("origin", origin));
2151    }
2152    if let Some(touch_count) = object.get("touch_count").and_then(Value::as_u64) {
2153        properties.push(mechanic_event_unsigned_property(
2154            "touch_count",
2155            touch_count as u32,
2156        ));
2157    }
2158    properties
2159}
2160
2161fn parse_ball_carry_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
2162    let object = json_object(value, "ball carry mechanic event")?;
2163    let serialized_kind = json_required_str(object, "kind")?;
2164    let kind = match serialized_kind {
2165        "carry" => "ball_carry",
2166        "air_dribble" => "air_dribble",
2167        other => other,
2168    };
2169    let mut mechanic_event = span_mechanic_event(
2170        kind,
2171        index,
2172        json_required_usize(object, "start_frame")?,
2173        json_required_usize(object, "end_frame")?,
2174        json_required_f32(object, "start_time")?,
2175        json_required_f32(object, "end_time")?,
2176        json_required_remote_id(object, "player_id")?,
2177        json_required_bool(object, "is_team_0")?,
2178    );
2179    if kind == "air_dribble" {
2180        mechanic_event.properties = ball_carry_mechanic_event_properties(object);
2181    }
2182    Ok(mechanic_event)
2183}
2184
2185fn parse_dodge_reset_mechanic_event(
2186    value: &Value,
2187    index: usize,
2188) -> SubtrActorResult<MechanicEvent> {
2189    let object = json_object(value, "dodge reset mechanic event")?;
2190    Ok(moment_mechanic_event(
2191        "flip_reset",
2192        index,
2193        json_required_usize(object, "frame")?,
2194        json_required_f32(object, "time")?,
2195        json_required_remote_id(object, "player")?,
2196        json_required_bool(object, "is_team_0")?,
2197    ))
2198}
2199
2200fn parse_dodge_reset_event(value: &Value) -> SubtrActorResult<DodgeResetEvent> {
2201    let object = json_object(value, "dodge reset event")?;
2202    Ok(DodgeResetEvent {
2203        time: json_required_f32(object, "time")?,
2204        frame: json_required_usize(object, "frame")?,
2205        player: json_required_remote_id(object, "player")?,
2206        is_team_0: json_required_bool(object, "is_team_0")?,
2207        counter_value: json_required_i32(object, "counter_value")?,
2208        on_ball: json_required_bool(object, "on_ball")?,
2209    })
2210}
2211
2212fn parse_powerslide_event(value: &Value) -> SubtrActorResult<PowerslideEvent> {
2213    let object = json_object(value, "powerslide event")?;
2214    Ok(PowerslideEvent {
2215        time: json_required_f32(object, "time")?,
2216        frame: json_required_usize(object, "frame")?,
2217        player: json_required_remote_id(object, "player")?,
2218        is_team_0: json_required_bool(object, "is_team_0")?,
2219        active: json_required_bool(object, "active")?,
2220    })
2221}
2222
2223fn parse_core_player_stats_event(value: &Value) -> SubtrActorResult<CorePlayerStatsEvent> {
2224    let object = json_object(value, "core player stats event")?;
2225    Ok(CorePlayerStatsEvent {
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        delta: decode_json_value(json_required_value(object, "delta")?.clone())?,
2231    })
2232}
2233
2234fn parse_core_team_stats_event(value: &Value) -> SubtrActorResult<CoreTeamStatsEvent> {
2235    let object = json_object(value, "core team stats event")?;
2236    Ok(CoreTeamStatsEvent {
2237        time: json_required_f32(object, "time")?,
2238        frame: json_required_usize(object, "frame")?,
2239        is_team_0: json_required_bool(object, "is_team_0")?,
2240        delta: decode_json_value(json_required_value(object, "delta")?.clone())?,
2241    })
2242}
2243
2244fn parse_possession_event(value: &Value) -> SubtrActorResult<PossessionEvent> {
2245    let object = json_object(value, "possession event")?;
2246    Ok(PossessionEvent {
2247        time: json_required_f32(object, "time")?,
2248        frame: json_required_usize(object, "frame")?,
2249        active: json_required_bool(object, "active")?,
2250        possession_state: json_required_str(object, "possession_state")?.to_owned(),
2251        field_third: match object.get("field_third") {
2252            None | Some(Value::Null) => None,
2253            Some(_) => Some(json_required_str(object, "field_third")?.to_owned()),
2254        },
2255    })
2256}
2257
2258fn parse_pressure_event(value: &Value) -> SubtrActorResult<PressureEvent> {
2259    let object = json_object(value, "pressure event")?;
2260    Ok(PressureEvent {
2261        time: json_required_f32(object, "time")?,
2262        frame: json_required_usize(object, "frame")?,
2263        active: json_required_bool(object, "active")?,
2264        field_half: json_required_str(object, "field_half")?.to_owned(),
2265    })
2266}
2267
2268fn parse_movement_event(value: &Value) -> SubtrActorResult<MovementEvent> {
2269    let object = json_object(value, "movement event")?;
2270    Ok(MovementEvent {
2271        time: json_required_f32(object, "time")?,
2272        frame: json_required_usize(object, "frame")?,
2273        player: json_required_remote_id(object, "player")?,
2274        is_team_0: json_required_bool(object, "is_team_0")?,
2275        dt: json_required_f32(object, "dt")?,
2276        speed: json_required_f32(object, "speed")?,
2277        distance: json_required_f32(object, "distance")?,
2278        speed_band: json_required_str(object, "speed_band")?.to_owned(),
2279        height_band: json_required_str(object, "height_band")?.to_owned(),
2280    })
2281}
2282
2283fn parse_positioning_event(value: &Value) -> SubtrActorResult<PositioningEvent> {
2284    let object = json_object(value, "positioning event")?;
2285    Ok(PositioningEvent {
2286        time: json_required_f32(object, "time")?,
2287        frame: json_required_usize(object, "frame")?,
2288        player: json_required_remote_id(object, "player")?,
2289        is_team_0: json_required_bool(object, "is_team_0")?,
2290        active_game_time: json_required_f32(object, "active_game_time")?,
2291        tracked_time: json_required_f32(object, "tracked_time")?,
2292        sum_distance_to_teammates: json_required_f32(object, "sum_distance_to_teammates")?,
2293        sum_distance_to_ball: json_required_f32(object, "sum_distance_to_ball")?,
2294        sum_distance_to_ball_has_possession: json_required_f32(
2295            object,
2296            "sum_distance_to_ball_has_possession",
2297        )?,
2298        time_has_possession: json_required_f32(object, "time_has_possession")?,
2299        sum_distance_to_ball_no_possession: json_required_f32(
2300            object,
2301            "sum_distance_to_ball_no_possession",
2302        )?,
2303        time_no_possession: json_required_f32(object, "time_no_possession")?,
2304        time_demolished: json_required_f32(object, "time_demolished")?,
2305        time_no_teammates: json_required_f32(object, "time_no_teammates")?,
2306        time_most_back: json_required_f32(object, "time_most_back")?,
2307        time_most_forward: json_required_f32(object, "time_most_forward")?,
2308        time_mid_role: json_required_f32(object, "time_mid_role")?,
2309        time_other_role: json_required_f32(object, "time_other_role")?,
2310        time_defensive_zone: json_required_f32(object, "time_defensive_third")?,
2311        time_neutral_zone: json_required_f32(object, "time_neutral_third")?,
2312        time_offensive_zone: json_required_f32(object, "time_offensive_third")?,
2313        time_defensive_half: json_required_f32(object, "time_defensive_half")?,
2314        time_offensive_half: json_required_f32(object, "time_offensive_half")?,
2315        time_closest_to_ball: json_required_f32(object, "time_closest_to_ball")?,
2316        time_farthest_from_ball: json_required_f32(object, "time_farthest_from_ball")?,
2317        time_behind_ball: json_required_f32(object, "time_behind_ball")?,
2318        time_level_with_ball: json_required_f32(object, "time_level_with_ball")?,
2319        time_in_front_of_ball: json_required_f32(object, "time_in_front_of_ball")?,
2320        times_caught_ahead_of_play_on_conceded_goals: json_required_usize(
2321            object,
2322            "times_caught_ahead_of_play_on_conceded_goals",
2323        )? as u32,
2324    })
2325}
2326
2327fn parse_rotation_player_event(value: &Value) -> SubtrActorResult<RotationPlayerEvent> {
2328    let object = json_object(value, "rotation player event")?;
2329    Ok(RotationPlayerEvent {
2330        time: json_required_f32(object, "time")?,
2331        frame: json_required_usize(object, "frame")?,
2332        player: json_required_remote_id(object, "player")?,
2333        is_team_0: json_required_bool(object, "is_team_0")?,
2334        active: json_required_bool(object, "active")?,
2335        became_first_man_count: json_required_usize(object, "became_first_man_count")? as u32,
2336        lost_first_man_count: json_required_usize(object, "lost_first_man_count")? as u32,
2337        current_role_state: decode_json_value(
2338            json_required_value(object, "current_role_state")?.clone(),
2339        )?,
2340        current_depth_state: decode_json_value(
2341            json_required_value(object, "current_depth_state")?.clone(),
2342        )?,
2343    })
2344}
2345
2346fn parse_rotation_team_event(value: &Value) -> SubtrActorResult<RotationTeamEvent> {
2347    let object = json_object(value, "rotation team event")?;
2348    Ok(RotationTeamEvent {
2349        time: json_required_f32(object, "time")?,
2350        frame: json_required_usize(object, "frame")?,
2351        is_team_0: json_required_bool(object, "is_team_0")?,
2352        first_man_changes_for_team: json_required_usize(object, "first_man_changes_for_team")?
2353            as u32,
2354        rotation_count: json_required_usize(object, "rotation_count")? as u32,
2355    })
2356}
2357
2358fn parse_touch_stats_event(value: &Value) -> SubtrActorResult<TouchStatsEvent> {
2359    let object = json_object(value, "touch stats event")?;
2360    let time = json_required_f32(object, "time")?;
2361    let frame = json_required_usize(object, "frame")?;
2362    Ok(TouchStatsEvent {
2363        time,
2364        frame,
2365        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2366        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2367        player: json_required_remote_id(object, "player")?,
2368        is_team_0: json_required_bool(object, "is_team_0")?,
2369        kind: json_required_str(object, "kind")?.to_owned(),
2370        height_band: json_required_str(object, "height_band")?.to_owned(),
2371        surface: json_required_str(object, "surface")?.to_owned(),
2372        dodge_state: json_required_str(object, "dodge_state")?.to_owned(),
2373        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2374    })
2375}
2376
2377fn parse_touch_ball_movement_event(value: &Value) -> SubtrActorResult<TouchBallMovementEvent> {
2378    let object = json_object(value, "touch ball movement event")?;
2379    Ok(TouchBallMovementEvent {
2380        time: json_required_f32(object, "time")?,
2381        frame: json_required_usize(object, "frame")?,
2382        player: json_required_remote_id(object, "player")?,
2383        is_team_0: json_required_bool(object, "is_team_0")?,
2384        travel_distance: json_required_f32(object, "travel_distance")?,
2385        advance_distance: json_required_f32(object, "advance_distance")?,
2386        retreat_distance: json_required_f32(object, "retreat_distance")?,
2387    })
2388}
2389
2390fn parse_touch_last_touch_event(value: &Value) -> SubtrActorResult<TouchLastTouchEvent> {
2391    let object = json_object(value, "touch last-touch event")?;
2392    let time = json_required_f32(object, "time")?;
2393    let frame = json_required_usize(object, "frame")?;
2394    Ok(TouchLastTouchEvent {
2395        time,
2396        frame,
2397        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2398        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2399        is_team_0: json_required_bool(object, "is_team_0")?,
2400        player: json_optional_remote_id(object.get("player"))?,
2401    })
2402}
2403
2404fn parse_flick_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
2405    let object = json_object(value, "flick mechanic event")?;
2406    Ok(span_mechanic_event(
2407        "flick",
2408        index,
2409        json_required_usize(object, "setup_start_frame")?,
2410        json_required_usize(object, "frame")?,
2411        json_required_f32(object, "setup_start_time")?,
2412        json_required_f32(object, "time")?,
2413        json_required_remote_id(object, "player")?,
2414        json_required_bool(object, "is_team_0")?,
2415    ))
2416}
2417
2418fn parse_flick_event(value: &Value) -> SubtrActorResult<FlickEvent> {
2419    let object = json_object(value, "flick event")?;
2420    let time = json_required_f32(object, "time")?;
2421    let frame = json_required_usize(object, "frame")?;
2422    Ok(FlickEvent {
2423        time,
2424        frame,
2425        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2426        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2427        player: json_required_remote_id(object, "player")?,
2428        is_team_0: json_required_bool(object, "is_team_0")?,
2429        dodge_time: json_required_f32(object, "dodge_time")?,
2430        dodge_frame: json_required_usize(object, "dodge_frame")?,
2431        time_since_dodge: json_required_f32(object, "time_since_dodge")?,
2432        setup_start_time: json_required_f32(object, "setup_start_time")?,
2433        setup_start_frame: json_required_usize(object, "setup_start_frame")?,
2434        setup_duration: json_required_f32(object, "setup_duration")?,
2435        setup_touch_count: json_required_usize(object, "setup_touch_count")? as u32,
2436        average_horizontal_gap: json_required_f32(object, "average_horizontal_gap")?,
2437        average_vertical_gap: json_required_f32(object, "average_vertical_gap")?,
2438        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2439        ball_impulse: json_required_vec3(object, "ball_impulse")?,
2440        impulse_away_alignment: json_required_f32(object, "impulse_away_alignment")?,
2441        vertical_impulse: json_required_f32(object, "vertical_impulse")?,
2442        confidence: json_required_f32(object, "confidence")?,
2443    })
2444}
2445
2446fn parse_musty_flick_mechanic_event(
2447    value: &Value,
2448    index: usize,
2449) -> SubtrActorResult<MechanicEvent> {
2450    let object = json_object(value, "musty flick mechanic event")?;
2451    Ok(span_mechanic_event(
2452        "musty_flick",
2453        index,
2454        json_required_usize(object, "dodge_frame")?,
2455        json_required_usize(object, "frame")?,
2456        json_required_f32(object, "dodge_time")?,
2457        json_required_f32(object, "time")?,
2458        json_required_remote_id(object, "player")?,
2459        json_required_bool(object, "is_team_0")?,
2460    ))
2461}
2462
2463fn parse_musty_flick_event(value: &Value) -> SubtrActorResult<MustyFlickEvent> {
2464    let object = json_object(value, "musty flick event")?;
2465    let time = json_required_f32(object, "time")?;
2466    let frame = json_required_usize(object, "frame")?;
2467    Ok(MustyFlickEvent {
2468        time,
2469        frame,
2470        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2471        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2472        player: json_required_remote_id(object, "player")?,
2473        is_team_0: json_required_bool(object, "is_team_0")?,
2474        aerial: json_required_bool(object, "aerial")?,
2475        dodge_time: json_required_f32(object, "dodge_time")?,
2476        dodge_frame: json_required_usize(object, "dodge_frame")?,
2477        time_since_dodge: json_required_f32(object, "time_since_dodge")?,
2478        confidence: json_required_f32(object, "confidence")?,
2479        local_ball_position: json_required_vec3(object, "local_ball_position")?,
2480        rear_alignment: json_required_f32(object, "rear_alignment")?,
2481        top_alignment: json_required_f32(object, "top_alignment")?,
2482        forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
2483        pitch_rate: json_required_f32(object, "pitch_rate")?,
2484        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2485    })
2486}
2487
2488fn parse_goal_context_event(value: &Value) -> SubtrActorResult<GoalContextEvent> {
2489    let object = json_object(value, "goal context event")?;
2490    Ok(GoalContextEvent {
2491        time: json_required_f32(object, "time")?,
2492        frame: json_required_usize(object, "frame")?,
2493        scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
2494        scorer: json_optional_remote_id(object.get("scorer"))?,
2495        scoring_team_most_back_player: json_optional_remote_id(
2496            object.get("scoring_team_most_back_player"),
2497        )?,
2498        defending_team_most_back_player: json_optional_remote_id(
2499            object.get("defending_team_most_back_player"),
2500        )?,
2501        ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
2502        ball_air_time_before_goal: json_optional_f32(object.get("ball_air_time_before_goal"))?,
2503        goal_buildup: object
2504            .get("goal_buildup")
2505            .map(|value| decode_json_value(value.clone()))
2506            .transpose()?
2507            .unwrap_or_default(),
2508        scorer_last_touch: match object.get("scorer_last_touch") {
2509            None | Some(Value::Null) => None,
2510            Some(value) => Some(parse_goal_touch_context(value)?),
2511        },
2512        players: json_required_array(object, "players")?
2513            .iter()
2514            .map(parse_goal_player_context)
2515            .collect::<SubtrActorResult<Vec<_>>>()?,
2516    })
2517}
2518
2519fn parse_goal_player_context(value: &Value) -> SubtrActorResult<GoalPlayerContext> {
2520    let object = json_object(value, "goal player context")?;
2521    Ok(GoalPlayerContext {
2522        player: json_required_remote_id(object, "player")?,
2523        is_team_0: json_required_bool(object, "is_team_0")?,
2524        position: json_optional_goal_context_position(object.get("position"))?,
2525        boost_amount: json_optional_f32(object.get("boost_amount"))?,
2526        average_boost_in_leadup: json_optional_f32(object.get("average_boost_in_leadup"))?,
2527        min_boost_in_leadup: json_optional_f32(object.get("min_boost_in_leadup"))?,
2528        is_most_back: json_required_bool(object, "is_most_back")?,
2529    })
2530}
2531
2532fn parse_goal_touch_context(value: &Value) -> SubtrActorResult<GoalTouchContext> {
2533    let object = json_object(value, "goal touch context")?;
2534    Ok(GoalTouchContext {
2535        time: json_required_f32(object, "time")?,
2536        frame: json_required_usize(object, "frame")?,
2537        player: json_required_remote_id(object, "player")?,
2538        is_team_0: json_required_bool(object, "is_team_0")?,
2539        ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
2540        player_position: json_optional_goal_context_position(object.get("player_position"))?,
2541        players: match object.get("players").and_then(Value::as_array) {
2542            Some(players) => players
2543                .iter()
2544                .map(parse_goal_player_context)
2545                .collect::<SubtrActorResult<Vec<_>>>()?,
2546            None => Vec::new(),
2547        },
2548    })
2549}
2550
2551fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
2552    let object = json_object(value, "backboard event")?;
2553    Ok(BackboardBounceEvent {
2554        time: json_required_f32(object, "time")?,
2555        frame: json_required_usize(object, "frame")?,
2556        player: json_required_remote_id(object, "player")?,
2557        is_team_0: json_required_bool(object, "is_team_0")?,
2558    })
2559}
2560
2561fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
2562    let object = json_object(value, "ceiling shot event")?;
2563    Ok(CeilingShotEvent {
2564        time: json_required_f32(object, "time")?,
2565        frame: json_required_usize(object, "frame")?,
2566        player: json_required_remote_id(object, "player")?,
2567        is_team_0: json_required_bool(object, "is_team_0")?,
2568        ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
2569        ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
2570        time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
2571        ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
2572        touch_position: json_required_vec3(object, "touch_position")?,
2573        local_ball_position: json_required_vec3(object, "local_ball_position")?,
2574        separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
2575        roof_alignment: json_required_f32(object, "roof_alignment")?,
2576        forward_alignment: json_required_f32(object, "forward_alignment")?,
2577        forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
2578        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2579        confidence: json_required_f32(object, "confidence")?,
2580    })
2581}
2582
2583fn parse_wall_aerial_event(value: &Value) -> SubtrActorResult<WallAerialEvent> {
2584    let object = json_object(value, "wall aerial event")?;
2585    let time = json_required_f32(object, "time")?;
2586    let frame = json_required_usize(object, "frame")?;
2587    Ok(WallAerialEvent {
2588        time,
2589        frame,
2590        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2591        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2592        player: json_required_remote_id(object, "player")?,
2593        is_team_0: json_required_bool(object, "is_team_0")?,
2594        wall: decode_json_value(json_required_value(object, "wall")?.clone())?,
2595        wall_contact_time: json_required_f32(object, "wall_contact_time")?,
2596        wall_contact_frame: json_required_usize(object, "wall_contact_frame")?,
2597        takeoff_time: json_required_f32(object, "takeoff_time")?,
2598        takeoff_frame: json_required_usize(object, "takeoff_frame")?,
2599        time_since_takeoff: json_required_f32(object, "time_since_takeoff")?,
2600        wall_contact_position: json_required_vec3(object, "wall_contact_position")?,
2601        takeoff_position: json_required_vec3(object, "takeoff_position")?,
2602        player_position: json_required_vec3(object, "player_position")?,
2603        ball_position: json_required_vec3(object, "ball_position")?,
2604        setup_start_time: json_required_f32(object, "setup_start_time")?,
2605        setup_start_frame: json_required_usize(object, "setup_start_frame")?,
2606        setup_duration: json_required_f32(object, "setup_duration")?,
2607        ball_speed: json_required_f32(object, "ball_speed")?,
2608        ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2609        goal_alignment: json_required_f32(object, "goal_alignment")?,
2610        confidence: json_required_f32(object, "confidence")?,
2611    })
2612}
2613
2614fn parse_wall_aerial_shot_event(value: &Value) -> SubtrActorResult<WallAerialShotEvent> {
2615    let object = json_object(value, "wall aerial shot event")?;
2616    Ok(WallAerialShotEvent {
2617        time: json_required_f32(object, "time")?,
2618        frame: json_required_usize(object, "frame")?,
2619        player: json_required_remote_id(object, "player")?,
2620        is_team_0: json_required_bool(object, "is_team_0")?,
2621        wall: decode_json_value(json_required_value(object, "wall")?.clone())?,
2622        wall_contact_time: json_required_f32(object, "wall_contact_time")?,
2623        wall_contact_frame: json_required_usize(object, "wall_contact_frame")?,
2624        takeoff_time: json_required_f32(object, "takeoff_time")?,
2625        takeoff_frame: json_required_usize(object, "takeoff_frame")?,
2626        time_since_takeoff: json_required_f32(object, "time_since_takeoff")?,
2627        wall_contact_position: json_required_vec3(object, "wall_contact_position")?,
2628        takeoff_position: json_required_vec3(object, "takeoff_position")?,
2629        player_position: json_required_vec3(object, "player_position")?,
2630        ball_position: json_required_vec3(object, "ball_position")?,
2631        ball_speed: json_optional_f32(object.get("ball_speed"))?,
2632        goal_alignment: json_optional_f32(object.get("goal_alignment"))?,
2633        confidence: json_required_f32(object, "confidence")?,
2634    })
2635}
2636
2637fn parse_center_event(value: &Value) -> SubtrActorResult<CenterEvent> {
2638    let object = json_object(value, "center event")?;
2639    Ok(CenterEvent {
2640        time: json_required_f32(object, "time")?,
2641        frame: json_required_usize(object, "frame")?,
2642        player: json_required_remote_id(object, "player")?,
2643        is_team_0: json_required_bool(object, "is_team_0")?,
2644        start_time: json_required_f32(object, "start_time")?,
2645        start_frame: json_required_usize(object, "start_frame")?,
2646        duration: json_required_f32(object, "duration")?,
2647        start_ball_position: json_required_vec3(object, "start_ball_position")?,
2648        end_ball_position: json_required_vec3(object, "end_ball_position")?,
2649        ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
2650        ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
2651        lateral_centering_distance: json_required_f32(object, "lateral_centering_distance")?,
2652    })
2653}
2654
2655fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
2656    let object = json_object(value, "double tap event")?;
2657    Ok(DoubleTapEvent {
2658        time: json_required_f32(object, "time")?,
2659        frame: json_required_usize(object, "frame")?,
2660        player: json_required_remote_id(object, "player")?,
2661        is_team_0: json_required_bool(object, "is_team_0")?,
2662        backboard_time: json_required_f32(object, "backboard_time")?,
2663        backboard_frame: json_required_usize(object, "backboard_frame")?,
2664    })
2665}
2666
2667fn parse_pass_event(value: &Value) -> SubtrActorResult<PassEvent> {
2668    let object = json_object(value, "pass event")?;
2669    let time = json_required_f32(object, "time")?;
2670    let frame = json_required_usize(object, "frame")?;
2671    Ok(PassEvent {
2672        time,
2673        frame,
2674        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2675        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2676        passer: json_required_remote_id(object, "passer")?,
2677        receiver: json_required_remote_id(object, "receiver")?,
2678        is_team_0: json_required_bool(object, "is_team_0")?,
2679        start_time: json_required_f32(object, "start_time")?,
2680        start_frame: json_required_usize(object, "start_frame")?,
2681        duration: json_required_f32(object, "duration")?,
2682        ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
2683        ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
2684        pass_kind: parse_pass_kind(object.get("pass_kind"))?,
2685    })
2686}
2687
2688fn parse_pass_last_completed_event(value: &Value) -> SubtrActorResult<PassLastCompletedEvent> {
2689    let object = json_object(value, "pass last completed event")?;
2690    Ok(PassLastCompletedEvent {
2691        time: json_required_f32(object, "time")?,
2692        frame: json_required_usize(object, "frame")?,
2693        player: json_optional_remote_id(object.get("player"))?,
2694    })
2695}
2696
2697fn parse_pass_kind(value: Option<&Value>) -> SubtrActorResult<PassKind> {
2698    let Some(value) = value else {
2699        return Ok(PassKind::Direct);
2700    };
2701    let kind = value.as_str().ok_or_else(|| {
2702        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2703            "Expected JSON field 'pass_kind' to be a string".to_owned(),
2704        ))
2705    })?;
2706    match kind {
2707        "direct" => Ok(PassKind::Direct),
2708        "backboard" => Ok(PassKind::Backboard),
2709        "fifty_fifty" => Ok(PassKind::FiftyFifty),
2710        "fifty_fifty_backboard" => Ok(PassKind::FiftyFiftyBackboard),
2711        other => Err(SubtrActorError::new(
2712            SubtrActorErrorVariant::StatsSerializationError(format!("Unknown pass kind '{other}'")),
2713        )),
2714    }
2715}
2716
2717fn parse_ball_carry_event(value: &Value) -> SubtrActorResult<BallCarryEvent> {
2718    let object = json_object(value, "ball carry event")?;
2719    Ok(BallCarryEvent {
2720        player_id: json_required_remote_id(object, "player_id")?,
2721        is_team_0: json_required_bool(object, "is_team_0")?,
2722        kind: parse_ball_carry_kind(json_required_str(object, "kind")?)?,
2723        start_frame: json_required_usize(object, "start_frame")?,
2724        end_frame: json_required_usize(object, "end_frame")?,
2725        start_time: json_required_f32(object, "start_time")?,
2726        end_time: json_required_f32(object, "end_time")?,
2727        duration: json_required_f32(object, "duration")?,
2728        straight_line_distance: json_required_f32(object, "straight_line_distance")?,
2729        path_distance: json_required_f32(object, "path_distance")?,
2730        average_horizontal_gap: json_required_f32(object, "average_horizontal_gap")?,
2731        average_vertical_gap: json_required_f32(object, "average_vertical_gap")?,
2732        average_speed: json_required_f32(object, "average_speed")?,
2733        touch_count: json_required_usize(object, "touch_count")? as u32,
2734        air_touch_count: json_required_usize(object, "air_touch_count")? as u32,
2735        air_dribble_origin: parse_air_dribble_origin(object.get("air_dribble_origin"))?,
2736    })
2737}
2738
2739fn parse_ball_carry_kind(kind: &str) -> SubtrActorResult<BallCarryKind> {
2740    match kind {
2741        "carry" => Ok(BallCarryKind::Carry),
2742        "air_dribble" => Ok(BallCarryKind::AirDribble),
2743        other => Err(SubtrActorError::new(
2744            SubtrActorErrorVariant::StatsSerializationError(format!(
2745                "Unknown ball carry kind '{other}'"
2746            )),
2747        )),
2748    }
2749}
2750
2751fn parse_air_dribble_origin(value: Option<&Value>) -> SubtrActorResult<Option<AirDribbleOrigin>> {
2752    let Some(value) = value else {
2753        return Ok(None);
2754    };
2755    if value.is_null() {
2756        return Ok(None);
2757    }
2758    let origin = value.as_str().ok_or_else(|| {
2759        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2760            "Expected optional JSON field 'air_dribble_origin' to be a string".to_owned(),
2761        ))
2762    })?;
2763    match origin {
2764        "ground_to_air" => Ok(Some(AirDribbleOrigin::GroundToAir)),
2765        "wall_to_air" => Ok(Some(AirDribbleOrigin::WallToAir)),
2766        other => Err(SubtrActorError::new(
2767            SubtrActorErrorVariant::StatsSerializationError(format!(
2768                "Unknown air dribble origin '{other}'"
2769            )),
2770        )),
2771    }
2772}
2773
2774fn parse_one_timer_event(value: &Value) -> SubtrActorResult<OneTimerEvent> {
2775    let object = json_object(value, "one timer event")?;
2776    Ok(OneTimerEvent {
2777        time: json_required_f32(object, "time")?,
2778        frame: json_required_usize(object, "frame")?,
2779        player: json_required_remote_id(object, "player")?,
2780        passer: json_required_remote_id(object, "passer")?,
2781        is_team_0: json_required_bool(object, "is_team_0")?,
2782        pass_start_time: json_required_f32(object, "pass_start_time")?,
2783        pass_start_frame: json_required_usize(object, "pass_start_frame")?,
2784        pass_duration: json_required_f32(object, "pass_duration")?,
2785        pass_travel_distance: json_required_f32(object, "pass_travel_distance")?,
2786        pass_advance_distance: json_required_f32(object, "pass_advance_distance")?,
2787        ball_speed: json_required_f32(object, "ball_speed")?,
2788        goal_alignment: json_required_f32(object, "goal_alignment")?,
2789    })
2790}
2791
2792fn parse_half_volley_event(value: &Value) -> SubtrActorResult<HalfVolleyEvent> {
2793    let object = json_object(value, "half volley event")?;
2794    let time = json_required_f32(object, "time")?;
2795    let frame = json_required_usize(object, "frame")?;
2796    Ok(HalfVolleyEvent {
2797        time,
2798        frame,
2799        sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2800        sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2801        player: json_required_remote_id(object, "player")?,
2802        is_team_0: json_required_bool(object, "is_team_0")?,
2803        bounce_time: json_required_f32(object, "bounce_time")?,
2804        bounce_frame: json_required_usize(object, "bounce_frame")?,
2805        bounce_to_touch_seconds: json_required_f32(object, "bounce_to_touch_seconds")?,
2806        ball_speed: json_required_f32(object, "ball_speed")?,
2807        goal_alignment: json_required_f32(object, "goal_alignment")?,
2808    })
2809}
2810
2811fn parse_goal_tag_event(value: &Value) -> SubtrActorResult<GoalTagEvent> {
2812    let object = json_object(value, "goal tag event")?;
2813    Ok(GoalTagEvent {
2814        goal_index: json_required_usize(object, "goal_index")?,
2815        time: json_required_f32(object, "time")?,
2816        frame: json_required_usize(object, "frame")?,
2817        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
2818        scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
2819        scorer: json_optional_remote_id(object.get("scorer"))?,
2820        confidence: json_required_f32(object, "confidence")?,
2821        modifiers: json_optional_array(object.get("modifiers"))?
2822            .iter()
2823            .map(|modifier| decode_json_value(modifier.clone()))
2824            .collect::<SubtrActorResult<Vec<_>>>()?,
2825        evidence: json_required_array(object, "evidence")?
2826            .iter()
2827            .map(parse_goal_tag_evidence)
2828            .collect::<SubtrActorResult<Vec<_>>>()?,
2829    })
2830}
2831
2832fn parse_goal_tag_evidence(value: &Value) -> SubtrActorResult<GoalTagEvidence> {
2833    let object = json_object(value, "goal tag evidence")?;
2834    Ok(GoalTagEvidence {
2835        kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
2836        time: json_required_f32(object, "time")?,
2837        frame: json_required_usize(object, "frame")?,
2838        player: json_optional_remote_id(object.get("player"))?,
2839    })
2840}
2841
2842fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
2843    let object = json_object(value, "fifty fifty event")?;
2844    Ok(FiftyFiftyEvent {
2845        start_time: json_required_f32(object, "start_time")?,
2846        start_frame: json_required_usize(object, "start_frame")?,
2847        resolve_time: json_required_f32(object, "resolve_time")?,
2848        resolve_frame: json_required_usize(object, "resolve_frame")?,
2849        is_kickoff: json_required_bool(object, "is_kickoff")?,
2850        team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
2851        team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
2852        team_zero_position: json_required_vec3(object, "team_zero_position")?,
2853        team_one_position: json_required_vec3(object, "team_one_position")?,
2854        midpoint: json_required_vec3(object, "midpoint")?,
2855        plane_normal: json_required_vec3(object, "plane_normal")?,
2856        winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
2857        possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
2858    })
2859}
2860
2861fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
2862    let object = json_object(value, "speed flip event")?;
2863    let time = json_required_f32(object, "time")?;
2864    let frame = json_required_usize(object, "frame")?;
2865    Ok(SpeedFlipEvent {
2866        time,
2867        frame,
2868        resolved_time: json_optional_f32(object.get("resolved_time"))?.unwrap_or(time),
2869        resolved_frame: json_optional_usize(object.get("resolved_frame"))?.unwrap_or(frame),
2870        player: json_required_remote_id(object, "player")?,
2871        is_team_0: json_required_bool(object, "is_team_0")?,
2872        time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
2873        start_position: json_required_vec3(object, "start_position")?,
2874        end_position: json_required_vec3(object, "end_position")?,
2875        start_speed: json_required_f32(object, "start_speed")?,
2876        max_speed: json_required_f32(object, "max_speed")?,
2877        best_alignment: json_required_f32(object, "best_alignment")?,
2878        diagonal_score: json_required_f32(object, "diagonal_score")?,
2879        cancel_score: json_required_f32(object, "cancel_score")?,
2880        speed_score: json_required_f32(object, "speed_score")?,
2881        confidence: json_required_f32(object, "confidence")?,
2882    })
2883}
2884
2885fn parse_half_flip_event(value: &Value) -> SubtrActorResult<HalfFlipEvent> {
2886    let object = json_object(value, "half flip event")?;
2887    Ok(HalfFlipEvent {
2888        time: json_required_f32(object, "time")?,
2889        frame: json_required_usize(object, "frame")?,
2890        player: json_required_remote_id(object, "player")?,
2891        is_team_0: json_required_bool(object, "is_team_0")?,
2892        start_position: json_required_vec3(object, "start_position")?,
2893        end_position: json_required_vec3(object, "end_position")?,
2894        start_speed: json_required_f32(object, "start_speed")?,
2895        end_speed: json_required_f32(object, "end_speed")?,
2896        start_backward_alignment: json_required_f32(object, "start_backward_alignment")?,
2897        best_reorientation_alignment: json_required_f32(object, "best_reorientation_alignment")?,
2898        best_forward_reversal: json_required_f32(object, "best_forward_reversal")?,
2899        max_forward_vertical: json_required_f32(object, "max_forward_vertical")?,
2900        confidence: json_required_f32(object, "confidence")?,
2901    })
2902}
2903
2904fn parse_wavedash_event(value: &Value) -> SubtrActorResult<WavedashEvent> {
2905    let object = json_object(value, "wavedash event")?;
2906    Ok(WavedashEvent {
2907        time: json_required_f32(object, "time")?,
2908        frame: json_required_usize(object, "frame")?,
2909        player: json_required_remote_id(object, "player")?,
2910        is_team_0: json_required_bool(object, "is_team_0")?,
2911        dodge_time: json_required_f32(object, "dodge_time")?,
2912        dodge_frame: json_required_usize(object, "dodge_frame")?,
2913        time_since_dodge: json_required_f32(object, "time_since_dodge")?,
2914        dodge_position: json_required_vec3(object, "dodge_position")?,
2915        landing_position: json_required_vec3(object, "landing_position")?,
2916        start_speed: json_required_f32(object, "start_speed")?,
2917        landing_speed: json_required_f32(object, "landing_speed")?,
2918        horizontal_speed_gain: json_required_f32(object, "horizontal_speed_gain")?,
2919        landing_uprightness: json_required_f32(object, "landing_uprightness")?,
2920        confidence: json_required_f32(object, "confidence")?,
2921    })
2922}
2923
2924fn parse_whiff_event(value: &Value) -> SubtrActorResult<WhiffEvent> {
2925    let object = json_object(value, "whiff event")?;
2926    let time = json_required_f32(object, "time")?;
2927    let frame = json_required_usize(object, "frame")?;
2928    Ok(WhiffEvent {
2929        kind: match object.get("kind").and_then(Value::as_str) {
2930            None | Some("whiff") => WhiffEventKind::Whiff,
2931            Some("beaten_to_ball") => WhiffEventKind::BeatenToBall,
2932            Some(kind) => {
2933                return SubtrActorError::new_result(
2934                    SubtrActorErrorVariant::StatsSerializationError(format!(
2935                        "Unknown whiff event kind '{kind}'"
2936                    )),
2937                );
2938            }
2939        },
2940        time,
2941        frame,
2942        resolved_time: json_optional_f32(object.get("resolved_time"))?.unwrap_or(time),
2943        resolved_frame: json_optional_usize(object.get("resolved_frame"))?.unwrap_or(frame),
2944        player: json_required_remote_id(object, "player")?,
2945        is_team_0: json_required_bool(object, "is_team_0")?,
2946        closest_approach_distance: json_required_f32(object, "closest_approach_distance")?,
2947        forward_alignment: json_required_f32(object, "forward_alignment")?,
2948        approach_speed: json_required_f32(object, "approach_speed")?,
2949        dodge_active: json_required_bool(object, "dodge_active")?,
2950        aerial: json_required_bool(object, "aerial")?,
2951    })
2952}
2953
2954fn parse_bump_event(value: &Value) -> SubtrActorResult<BumpEvent> {
2955    let object = json_object(value, "bump event")?;
2956    Ok(BumpEvent {
2957        time: json_required_f32(object, "time")?,
2958        frame: json_required_usize(object, "frame")?,
2959        initiator: json_required_remote_id(object, "initiator")?,
2960        victim: json_required_remote_id(object, "victim")?,
2961        initiator_is_team_0: json_required_bool(object, "initiator_is_team_0")?,
2962        victim_is_team_0: json_required_bool(object, "victim_is_team_0")?,
2963        is_team_bump: json_required_bool(object, "is_team_bump")?,
2964        strength: json_required_f32(object, "strength")?,
2965        confidence: json_required_f32(object, "confidence")?,
2966        contact_distance: json_required_f32(object, "contact_distance")?,
2967        closing_speed: json_required_f32(object, "closing_speed")?,
2968        victim_impulse: json_required_f32(object, "victim_impulse")?,
2969        initiator_position: json_required_vec3(object, "initiator_position")?,
2970        victim_position: json_required_vec3(object, "victim_position")?,
2971    })
2972}
2973
2974fn parse_boost_pickup_comparison_event(
2975    value: &Value,
2976) -> SubtrActorResult<BoostPickupComparisonEvent> {
2977    let object = json_object(value, "boost pickup comparison event")?;
2978    Ok(BoostPickupComparisonEvent {
2979        comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
2980        frame: json_required_usize(object, "frame")?,
2981        time: json_required_f32(object, "time")?,
2982        player_id: json_required_remote_id(object, "player_id")?,
2983        is_team_0: json_required_bool(object, "is_team_0")?,
2984        pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
2985        field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
2986        activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
2987        reported_frame: json_optional_usize(object.get("reported_frame"))?,
2988        reported_time: json_optional_f32(object.get("reported_time"))?,
2989        inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
2990        inferred_time: json_optional_f32(object.get("inferred_time"))?,
2991        boost_before: json_optional_f32(object.get("boost_before"))?,
2992        boost_after: json_optional_f32(object.get("boost_after"))?,
2993    })
2994}
2995
2996fn parse_boost_ledger_event(value: &Value) -> SubtrActorResult<BoostLedgerEvent> {
2997    let object = json_object(value, "boost ledger event")?;
2998    Ok(BoostLedgerEvent {
2999        frame: json_required_usize(object, "frame")?,
3000        time: json_required_f32(object, "time")?,
3001        player_id: json_required_remote_id(object, "player_id")?,
3002        is_team_0: json_required_bool(object, "is_team_0")?,
3003        transaction: decode_json_value(json_required_value(object, "transaction")?.clone())?,
3004        amount: json_required_f32(object, "amount")?,
3005        count: json_required_usize(object, "count")? as u32,
3006        labels: decode_json_value(
3007            object
3008                .get("labels")
3009                .cloned()
3010                .unwrap_or_else(|| Value::Array(Vec::new())),
3011        )?,
3012        boost_before: json_optional_f32(object.get("boost_before"))?,
3013        boost_after: json_optional_f32(object.get("boost_after"))?,
3014    })
3015}
3016
3017fn parse_boost_state_event(value: &Value) -> SubtrActorResult<BoostStateEvent> {
3018    let object = json_object(value, "boost state event")?;
3019    Ok(BoostStateEvent {
3020        frame: json_required_usize(object, "frame")?,
3021        time: json_required_f32(object, "time")?,
3022        player_id: json_required_remote_id(object, "player_id")?,
3023        is_team_0: json_required_bool(object, "is_team_0")?,
3024        boost_amount: json_required_f32(object, "boost_amount")?,
3025        boost_before: json_optional_f32(object.get("boost_before"))?,
3026    })
3027}
3028
3029fn json_object<'a>(
3030    value: &'a Value,
3031    context: &str,
3032) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
3033    value.as_object().ok_or_else(|| {
3034        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3035            "Expected {context} to be a JSON object"
3036        )))
3037    })
3038}
3039
3040fn json_required_value<'a>(
3041    object: &'a serde_json::Map<String, Value>,
3042    field: &str,
3043) -> SubtrActorResult<&'a Value> {
3044    object.get(field).ok_or_else(|| {
3045        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3046            "Missing JSON field '{field}'"
3047        )))
3048    })
3049}
3050
3051fn json_required_array<'a>(
3052    object: &'a serde_json::Map<String, Value>,
3053    field: &str,
3054) -> SubtrActorResult<&'a Vec<Value>> {
3055    json_required_value(object, field)?
3056        .as_array()
3057        .ok_or_else(|| {
3058            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3059                "Expected JSON field '{field}' to be an array"
3060            )))
3061        })
3062}
3063
3064fn json_optional_array(value: Option<&Value>) -> SubtrActorResult<&[Value]> {
3065    match value {
3066        Some(Value::Array(values)) => Ok(values),
3067        Some(_) => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3068            "Expected optional JSON value to be an array".to_owned(),
3069        )),
3070        None => Ok(&[]),
3071    }
3072}
3073
3074fn json_f32(value: &Value) -> Option<f32> {
3075    value.as_f64().map(|number| number as f32)
3076}
3077
3078fn json_config_f32(
3079    config: Option<&Map<String, Value>>,
3080    key: &str,
3081    legacy_key: &str,
3082) -> Option<f32> {
3083    config.and_then(|config| {
3084        config
3085            .get(key)
3086            .or_else(|| config.get(legacy_key))
3087            .and_then(json_f32)
3088    })
3089}
3090
3091fn json_required_f32(
3092    object: &serde_json::Map<String, Value>,
3093    field: &str,
3094) -> SubtrActorResult<f32> {
3095    json_f32(json_required_value(object, field)?).ok_or_else(|| {
3096        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3097            "Expected JSON field '{field}' to be a float"
3098        )))
3099    })
3100}
3101
3102fn json_required_usize(
3103    object: &serde_json::Map<String, Value>,
3104    field: &str,
3105) -> SubtrActorResult<usize> {
3106    json_required_value(object, field)?
3107        .as_u64()
3108        .map(|number| number as usize)
3109        .ok_or_else(|| {
3110            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3111                "Expected JSON field '{field}' to be an unsigned integer"
3112            )))
3113        })
3114}
3115
3116fn json_required_i32(
3117    object: &serde_json::Map<String, Value>,
3118    field: &str,
3119) -> SubtrActorResult<i32> {
3120    json_required_value(object, field)?
3121        .as_i64()
3122        .map(|number| number as i32)
3123        .ok_or_else(|| {
3124            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3125                "Expected JSON field '{field}' to be a signed integer"
3126            )))
3127        })
3128}
3129
3130fn json_required_bool(
3131    object: &serde_json::Map<String, Value>,
3132    field: &str,
3133) -> SubtrActorResult<bool> {
3134    json_required_value(object, field)?
3135        .as_bool()
3136        .ok_or_else(|| {
3137            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3138                "Expected JSON field '{field}' to be a bool"
3139            )))
3140        })
3141}
3142
3143fn json_required_str<'a>(
3144    object: &'a serde_json::Map<String, Value>,
3145    field: &str,
3146) -> SubtrActorResult<&'a str> {
3147    json_required_value(object, field)?.as_str().ok_or_else(|| {
3148        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3149            "Expected JSON field '{field}' to be a string"
3150        )))
3151    })
3152}
3153
3154fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
3155    value.and_then(Value::as_bool)
3156}
3157
3158fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
3159    match value {
3160        None | Some(Value::Null) => Ok(None),
3161        Some(value) => json_f32(value).map(Some).ok_or_else(|| {
3162            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3163                "Expected optional JSON value to be a float".to_owned(),
3164            ))
3165        }),
3166    }
3167}
3168
3169fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
3170    match value {
3171        None | Some(Value::Null) => Ok(None),
3172        Some(value) => value
3173            .as_u64()
3174            .map(|number| Some(number as usize))
3175            .ok_or_else(|| {
3176                SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3177                    "Expected optional JSON value to be an unsigned integer".to_owned(),
3178                ))
3179            }),
3180    }
3181}
3182
3183fn json_goal_context_position(value: &Value) -> SubtrActorResult<GoalContextPosition> {
3184    let object = json_object(value, "goal context position")?;
3185    Ok(GoalContextPosition {
3186        x: json_required_f32(object, "x")?,
3187        y: json_required_f32(object, "y")?,
3188        z: json_required_f32(object, "z")?,
3189    })
3190}
3191
3192fn json_optional_goal_context_position(
3193    value: Option<&Value>,
3194) -> SubtrActorResult<Option<GoalContextPosition>> {
3195    match value {
3196        None | Some(Value::Null) => Ok(None),
3197        Some(value) => json_goal_context_position(value).map(Some),
3198    }
3199}
3200
3201fn json_required_vec3(
3202    object: &serde_json::Map<String, Value>,
3203    field: &str,
3204) -> SubtrActorResult<[f32; 3]> {
3205    let array = json_required_value(object, field)?
3206        .as_array()
3207        .ok_or_else(|| {
3208            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3209                "Expected JSON field '{field}' to be a 3-element array"
3210            )))
3211        })?;
3212    if array.len() != 3 {
3213        return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3214            format!("Expected JSON field '{field}' to contain exactly 3 elements"),
3215        ));
3216    }
3217    Ok([
3218        json_f32(&array[0]).ok_or_else(|| {
3219            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3220                "Expected JSON field '{field}[0]' to be a float"
3221            )))
3222        })?,
3223        json_f32(&array[1]).ok_or_else(|| {
3224            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3225                "Expected JSON field '{field}[1]' to be a float"
3226            )))
3227        })?,
3228        json_f32(&array[2]).ok_or_else(|| {
3229            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3230                "Expected JSON field '{field}[2]' to be a float"
3231            )))
3232        })?,
3233    ])
3234}
3235
3236fn json_required_remote_id(
3237    object: &serde_json::Map<String, Value>,
3238    field: &str,
3239) -> SubtrActorResult<PlayerId> {
3240    json_remote_id(json_required_value(object, field)?)
3241}
3242
3243fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
3244    match value {
3245        None | Some(Value::Null) => Ok(None),
3246        Some(value) => Ok(Some(json_remote_id(value)?)),
3247    }
3248}
3249
3250fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
3251    let object = json_object(value, "remote id")?;
3252    if object.len() != 1 {
3253        return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3254            "Expected remote id to contain exactly one variant".to_owned(),
3255        ));
3256    }
3257
3258    let (variant, payload) = object.iter().next().expect("validated single variant");
3259    match variant.as_str() {
3260        "PlayStation" => {
3261            let payload = json_object(payload, "playstation remote id")?;
3262            Ok(RemoteId::PlayStation(Ps4Id {
3263                online_id: json_u64(json_required_value(payload, "online_id")?)?,
3264                name: json_required_value(payload, "name")?
3265                    .as_str()
3266                    .ok_or_else(|| {
3267                        SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3268                            "Expected PlayStation name to be a string".to_owned(),
3269                        ))
3270                    })?
3271                    .to_owned(),
3272                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
3273            }))
3274        }
3275        "PsyNet" => {
3276            let payload = json_object(payload, "psynet remote id")?;
3277            Ok(RemoteId::PsyNet(PsyNetId {
3278                online_id: json_u64(json_required_value(payload, "online_id")?)?,
3279                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
3280            }))
3281        }
3282        "SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
3283        "Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
3284        "Switch" => {
3285            let payload = json_object(payload, "switch remote id")?;
3286            Ok(RemoteId::Switch(SwitchId {
3287                online_id: json_u64(json_required_value(payload, "online_id")?)?,
3288                unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
3289            }))
3290        }
3291        "Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
3292        "QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
3293        "Epic" => Ok(RemoteId::Epic(
3294            payload
3295                .as_str()
3296                .ok_or_else(|| {
3297                    SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3298                        "Expected Epic remote id payload to be a string".to_owned(),
3299                    ))
3300                })?
3301                .to_owned(),
3302        )),
3303        variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3304            format!("Unknown remote id variant '{variant}'"),
3305        )),
3306    }
3307}
3308
3309fn json_u64(value: &Value) -> SubtrActorResult<u64> {
3310    value
3311        .as_u64()
3312        .or_else(|| value.as_str().and_then(|text| text.parse().ok()))
3313        .ok_or_else(|| {
3314            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3315                "Expected JSON value to be a u64".to_owned(),
3316            ))
3317        })
3318}
3319
3320fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
3321    value
3322        .as_array()
3323        .ok_or_else(|| {
3324            SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3325                "Expected JSON value to be an array of bytes".to_owned(),
3326            ))
3327        })?
3328        .iter()
3329        .map(|entry| {
3330            entry
3331                .as_u64()
3332                .and_then(|number| u8::try_from(number).ok())
3333                .ok_or_else(|| {
3334                    SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3335                        "Expected JSON array entry to be a byte".to_owned(),
3336                    ))
3337                })
3338        })
3339        .collect()
3340}