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