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