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