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 goal_tag_events_typed(&self) -> SubtrActorResult<Vec<GoalTagEvent>> {
160 let mut events = Vec::new();
161 for module_name in [
162 "aerial_goal",
163 "high_aerial_goal",
164 "long_distance_goal",
165 "own_half_goal",
166 "empty_net_goal",
167 "flick_goal",
168 "one_timer_goal",
169 "air_dribble_goal",
170 "flip_reset_goal",
171 ] {
172 events.extend(self.module_player_events(
173 module_name,
174 "events",
175 parse_goal_tag_event,
176 )?);
177 }
178 events.sort_by(|left, right| {
179 left.time
180 .total_cmp(&right.time)
181 .then_with(|| left.frame.cmp(&right.frame))
182 .then_with(|| left.goal_index.cmp(&right.goal_index))
183 .then_with(|| format!("{:?}", left.kind).cmp(&format!("{:?}", right.kind)))
184 });
185 Ok(events)
186 }
187
188 fn mechanic_events_typed(&self) -> SubtrActorResult<Vec<MechanicEvent>> {
189 let mut events = Vec::new();
190
191 for (index, value) in self.module_array("ball_carry", "events").iter().enumerate() {
192 events.push(parse_ball_carry_mechanic_event(value, index)?);
193 }
194 for (index, value) in self
195 .module_array("ceiling_shot", "events")
196 .iter()
197 .enumerate()
198 {
199 let event = parse_ceiling_shot_event(value)?;
200 events.push(span_mechanic_event(
201 "ceiling_shot",
202 index,
203 event.ceiling_contact_frame,
204 event.frame,
205 event.ceiling_contact_time,
206 event.time,
207 event.player,
208 event.is_team_0,
209 ));
210 }
211 for (index, value) in self
212 .module_array("dodge_reset", "events")
213 .iter()
214 .enumerate()
215 {
216 events.push(parse_dodge_reset_mechanic_event(value, index)?);
217 }
218 for (index, value) in self.module_array("double_tap", "events").iter().enumerate() {
219 let event = parse_double_tap_event(value)?;
220 events.push(span_mechanic_event(
221 "double_tap",
222 index,
223 event.backboard_frame,
224 event.frame,
225 event.backboard_time,
226 event.time,
227 event.player,
228 event.is_team_0,
229 ));
230 }
231 for (index, value) in self.module_array("flick", "events").iter().enumerate() {
232 events.push(parse_flick_mechanic_event(value, index)?);
233 }
234 for (index, value) in self
235 .module_array("musty_flick", "events")
236 .iter()
237 .enumerate()
238 {
239 events.push(parse_musty_flick_mechanic_event(value, index)?);
240 }
241 for (index, value) in self.module_array("one_timer", "events").iter().enumerate() {
242 let event = parse_one_timer_event(value)?;
243 events.push(span_mechanic_event(
244 "one_timer",
245 index,
246 event.pass_start_frame,
247 event.frame,
248 event.pass_start_time,
249 event.time,
250 event.player,
251 event.is_team_0,
252 ));
253 }
254 for (index, value) in self.module_array("pass", "events").iter().enumerate() {
255 let event = parse_pass_event(value)?;
256 events.push(span_mechanic_event(
257 "pass",
258 index,
259 event.start_frame,
260 event.frame,
261 event.start_time,
262 event.time,
263 event.passer,
264 event.is_team_0,
265 ));
266 }
267 for (index, value) in self.module_array("speed_flip", "events").iter().enumerate() {
268 let event = parse_speed_flip_event(value)?;
269 events.push(moment_mechanic_event(
270 "speed_flip",
271 index,
272 event.frame,
273 event.time,
274 event.player,
275 event.is_team_0,
276 ));
277 }
278 for (index, value) in self.module_array("half_flip", "events").iter().enumerate() {
279 let event = parse_half_flip_event(value)?;
280 events.push(moment_mechanic_event(
281 "half_flip",
282 index,
283 event.frame,
284 event.time,
285 event.player,
286 event.is_team_0,
287 ));
288 }
289 for (index, value) in self.module_array("wavedash", "events").iter().enumerate() {
290 let event = parse_wavedash_event(value)?;
291 events.push(span_mechanic_event(
292 "wavedash",
293 index,
294 event.dodge_frame,
295 event.frame,
296 event.dodge_time,
297 event.time,
298 event.player,
299 event.is_team_0,
300 ));
301 }
302 for (index, value) in self.module_array("whiff", "events").iter().enumerate() {
303 let event = parse_whiff_event(value)?;
304 events.push(moment_mechanic_event(
305 "whiff",
306 index,
307 event.frame,
308 event.time,
309 event.player,
310 event.is_team_0,
311 ));
312 }
313
314 events.sort_by(|left, right| {
315 let left_time = mechanic_event_start_time(left);
316 let right_time = mechanic_event_start_time(right);
317 left_time
318 .total_cmp(&right_time)
319 .then_with(|| left.kind.cmp(&right.kind))
320 .then_with(|| left.id.cmp(&right.id))
321 });
322 Ok(events)
323 }
324
325 fn goal_tag_events_value(&self) -> Vec<Value> {
326 let mut events = Vec::new();
327 for module_name in [
328 "aerial_goal",
329 "high_aerial_goal",
330 "long_distance_goal",
331 "own_half_goal",
332 "empty_net_goal",
333 "flick_goal",
334 "one_timer_goal",
335 "air_dribble_goal",
336 "flip_reset_goal",
337 ] {
338 events.extend(self.module_array(module_name, "events"));
339 }
340 events.sort_by(|left, right| {
341 let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
342 let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
343 left_time.total_cmp(&right_time)
344 });
345 events
346 }
347
348 fn timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
349 Ok(ReplayStatsTimelineEvents {
350 timeline: self.timeline_events_typed()?,
351 mechanics: self.mechanic_events_typed()?,
352 goal_context: self.module_player_events(
353 "core",
354 "goal_context",
355 parse_goal_context_event,
356 )?,
357 backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
358 ceiling_shot: self.module_player_events(
359 "ceiling_shot",
360 "events",
361 parse_ceiling_shot_event,
362 )?,
363 double_tap: self.module_player_events(
364 "double_tap",
365 "events",
366 parse_double_tap_event,
367 )?,
368 one_timer: self.module_player_events("one_timer", "events", parse_one_timer_event)?,
369 fifty_fifty: self.module_player_events(
370 "fifty_fifty",
371 "events",
372 parse_fifty_fifty_event,
373 )?,
374 pass: self.module_player_events("pass", "events", parse_pass_event)?,
375 goal_tags: self.goal_tag_events_typed()?,
376 rush: self.module_typed_array("rush", "events")?,
377 speed_flip: self.module_player_events(
378 "speed_flip",
379 "events",
380 parse_speed_flip_event,
381 )?,
382 half_flip: self.module_player_events("half_flip", "events", parse_half_flip_event)?,
383 wavedash: self.module_player_events("wavedash", "events", parse_wavedash_event)?,
384 whiff: self.module_player_events("whiff", "events", parse_whiff_event)?,
385 boost_pickups: self.module_player_events(
386 "boost",
387 "events",
388 parse_boost_pickup_comparison_event,
389 )?,
390 })
391 }
392
393 fn timeline_event_sets_value(&self) -> Value {
394 let mut events = Map::new();
395 events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
396 events.insert("mechanics".to_owned(), Value::Array(Vec::new()));
397 events.insert(
398 "backboard".to_owned(),
399 Value::Array(self.module_array("backboard", "events")),
400 );
401 events.insert(
402 "ceiling_shot".to_owned(),
403 Value::Array(self.module_array("ceiling_shot", "events")),
404 );
405 events.insert(
406 "double_tap".to_owned(),
407 Value::Array(self.module_array("double_tap", "events")),
408 );
409 events.insert(
410 "one_timer".to_owned(),
411 Value::Array(self.module_array("one_timer", "events")),
412 );
413 events.insert(
414 "pass".to_owned(),
415 Value::Array(self.module_array("pass", "events")),
416 );
417 events.insert(
418 "goal_tags".to_owned(),
419 Value::Array(self.goal_tag_events_value()),
420 );
421 events.insert(
422 "fifty_fifty".to_owned(),
423 Value::Array(self.module_array("fifty_fifty", "events")),
424 );
425 events.insert(
426 "rush".to_owned(),
427 Value::Array(self.module_array("rush", "events")),
428 );
429 events.insert(
430 "speed_flip".to_owned(),
431 Value::Array(self.module_array("speed_flip", "events")),
432 );
433 events.insert(
434 "half_flip".to_owned(),
435 Value::Array(self.module_array("half_flip", "events")),
436 );
437 events.insert(
438 "wavedash".to_owned(),
439 Value::Array(self.module_array("wavedash", "events")),
440 );
441 events.insert(
442 "whiff".to_owned(),
443 Value::Array(self.module_array("whiff", "events")),
444 );
445 events.insert(
446 "boost_pickups".to_owned(),
447 Value::Array(self.module_array("boost", "events")),
448 );
449 Value::Object(events)
450 }
451
452 fn timeline_config(&self) -> StatsTimelineConfig {
453 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
454 let pressure_config = self.config.get("pressure").and_then(Value::as_object);
455 let rush_config = self.config.get("rush").and_then(Value::as_object);
456 let rush_defaults = RushCalculatorConfig::default();
457 let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
458 let high_aerial_goal_config = self
459 .config
460 .get("high_aerial_goal")
461 .and_then(Value::as_object);
462 let long_distance_goal_config = self
463 .config
464 .get("long_distance_goal")
465 .and_then(Value::as_object);
466 let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
467 let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
468 let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
469 let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
470 let air_dribble_goal_config = self
471 .config
472 .get("air_dribble_goal")
473 .and_then(Value::as_object);
474 let flip_reset_goal_config = self
475 .config
476 .get("flip_reset_goal")
477 .and_then(Value::as_object);
478
479 StatsTimelineConfig {
480 most_back_forward_threshold_y: positioning_config
481 .and_then(|config| config.get("most_back_forward_threshold_y"))
482 .and_then(json_f32)
483 .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
484 level_ball_depth_margin: positioning_config
485 .and_then(|config| config.get("level_ball_depth_margin"))
486 .and_then(json_f32)
487 .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
488 pressure_neutral_zone_half_width_y: pressure_config
489 .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
490 .and_then(json_f32)
491 .unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
492 rush_max_start_y: rush_config
493 .and_then(|config| config.get("rush_max_start_y"))
494 .and_then(json_f32)
495 .unwrap_or(rush_defaults.max_start_y),
496 rush_attack_support_distance_y: rush_config
497 .and_then(|config| config.get("rush_attack_support_distance_y"))
498 .and_then(json_f32)
499 .unwrap_or(rush_defaults.attack_support_distance_y),
500 rush_defender_distance_y: rush_config
501 .and_then(|config| config.get("rush_defender_distance_y"))
502 .and_then(json_f32)
503 .unwrap_or(rush_defaults.defender_distance_y),
504 rush_min_possession_retained_seconds: rush_config
505 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
506 .and_then(json_f32)
507 .unwrap_or(rush_defaults.min_possession_retained_seconds),
508 aerial_goal_min_ball_z: aerial_goal_config
509 .and_then(|config| config.get("aerial_goal_min_ball_z"))
510 .and_then(json_f32)
511 .unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
512 high_aerial_goal_min_ball_z: high_aerial_goal_config
513 .and_then(|config| config.get("high_aerial_goal_min_ball_z"))
514 .and_then(json_f32)
515 .unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
516 long_distance_goal_max_attacking_y: long_distance_goal_config
517 .and_then(|config| config.get("long_distance_goal_max_attacking_y"))
518 .and_then(json_f32)
519 .unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
520 own_half_goal_max_attacking_y: own_half_goal_config
521 .and_then(|config| config.get("own_half_goal_max_attacking_y"))
522 .and_then(json_f32)
523 .unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
524 empty_net_min_defender_y_margin: empty_net_goal_config
525 .and_then(|config| config.get("empty_net_min_defender_y_margin"))
526 .and_then(json_f32)
527 .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
528 empty_net_min_defender_distance: empty_net_goal_config
529 .and_then(|config| config.get("empty_net_min_defender_distance"))
530 .and_then(json_f32)
531 .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
532 empty_net_max_touch_attacking_y: empty_net_goal_config
533 .and_then(|config| config.get("empty_net_max_touch_attacking_y"))
534 .and_then(json_f32)
535 .unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
536 flick_goal_max_event_to_goal_seconds: json_config_f32(
537 flick_goal_config,
538 "flick_goal_max_event_to_goal_seconds",
539 "flick_goal_max_event_to_touch_seconds",
540 )
541 .unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
542 one_timer_goal_max_event_to_goal_seconds: json_config_f32(
543 one_timer_goal_config,
544 "one_timer_goal_max_event_to_goal_seconds",
545 "one_timer_goal_max_event_to_touch_seconds",
546 )
547 .unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
548 air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
549 air_dribble_goal_config,
550 "air_dribble_goal_max_end_to_goal_seconds",
551 "air_dribble_goal_max_end_to_touch_seconds",
552 )
553 .unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
554 flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
555 flip_reset_goal_config,
556 "flip_reset_goal_max_event_to_goal_seconds",
557 "flip_reset_goal_max_event_to_touch_seconds",
558 )
559 .unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
560 }
561 }
562
563 fn timeline_config_value(&self) -> SubtrActorResult<Value> {
564 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
565 let pressure_config = self.config.get("pressure").and_then(Value::as_object);
566 let rush_config = self.config.get("rush").and_then(Value::as_object);
567 let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
568 let high_aerial_goal_config = self
569 .config
570 .get("high_aerial_goal")
571 .and_then(Value::as_object);
572 let long_distance_goal_config = self
573 .config
574 .get("long_distance_goal")
575 .and_then(Value::as_object);
576 let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
577 let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
578 let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
579 let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
580 let air_dribble_goal_config = self
581 .config
582 .get("air_dribble_goal")
583 .and_then(Value::as_object);
584 let flip_reset_goal_config = self
585 .config
586 .get("flip_reset_goal")
587 .and_then(Value::as_object);
588
589 let mut config = Map::new();
590 config.insert(
591 "most_back_forward_threshold_y".to_owned(),
592 serialize_to_json_value(
593 &positioning_config
594 .and_then(|config| config.get("most_back_forward_threshold_y"))
595 .and_then(Value::as_f64)
596 .unwrap_or(
597 PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
598 ),
599 )?,
600 );
601 config.insert(
602 "level_ball_depth_margin".to_owned(),
603 serialize_to_json_value(
604 &positioning_config
605 .and_then(|config| config.get("level_ball_depth_margin"))
606 .and_then(Value::as_f64)
607 .unwrap_or(
608 PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
609 ),
610 )?,
611 );
612 config.insert(
613 "pressure_neutral_zone_half_width_y".to_owned(),
614 serialize_to_json_value(
615 &pressure_config
616 .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
617 .and_then(Value::as_f64)
618 .unwrap_or(
619 PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
620 ),
621 )?,
622 );
623 let rush_defaults = RushCalculatorConfig::default();
624 config.insert(
625 "rush_max_start_y".to_owned(),
626 serialize_to_json_value(
627 &rush_config
628 .and_then(|config| config.get("rush_max_start_y"))
629 .and_then(Value::as_f64)
630 .unwrap_or(rush_defaults.max_start_y as f64),
631 )?,
632 );
633 config.insert(
634 "rush_attack_support_distance_y".to_owned(),
635 serialize_to_json_value(
636 &rush_config
637 .and_then(|config| config.get("rush_attack_support_distance_y"))
638 .and_then(Value::as_f64)
639 .unwrap_or(rush_defaults.attack_support_distance_y as f64),
640 )?,
641 );
642 config.insert(
643 "rush_defender_distance_y".to_owned(),
644 serialize_to_json_value(
645 &rush_config
646 .and_then(|config| config.get("rush_defender_distance_y"))
647 .and_then(Value::as_f64)
648 .unwrap_or(rush_defaults.defender_distance_y as f64),
649 )?,
650 );
651 config.insert(
652 "rush_min_possession_retained_seconds".to_owned(),
653 serialize_to_json_value(
654 &rush_config
655 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
656 .and_then(Value::as_f64)
657 .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
658 )?,
659 );
660 for (module_config, key, default_value) in [
661 (
662 aerial_goal_config,
663 "aerial_goal_min_ball_z",
664 AerialGoalCalculatorConfig::default().min_ball_z,
665 ),
666 (
667 high_aerial_goal_config,
668 "high_aerial_goal_min_ball_z",
669 HighAerialGoalCalculatorConfig::default().min_ball_z,
670 ),
671 (
672 long_distance_goal_config,
673 "long_distance_goal_max_attacking_y",
674 LongDistanceGoalCalculatorConfig::default().max_attacking_y,
675 ),
676 (
677 own_half_goal_config,
678 "own_half_goal_max_attacking_y",
679 OwnHalfGoalCalculatorConfig::default().max_attacking_y,
680 ),
681 (
682 empty_net_goal_config,
683 "empty_net_min_defender_y_margin",
684 EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
685 ),
686 (
687 empty_net_goal_config,
688 "empty_net_min_defender_distance",
689 EmptyNetGoalCalculatorConfig::default().min_defender_distance,
690 ),
691 (
692 empty_net_goal_config,
693 "empty_net_max_touch_attacking_y",
694 EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
695 ),
696 (
697 flick_goal_config,
698 "flick_goal_max_event_to_goal_seconds",
699 FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
700 ),
701 (
702 one_timer_goal_config,
703 "one_timer_goal_max_event_to_goal_seconds",
704 OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
705 ),
706 (
707 air_dribble_goal_config,
708 "air_dribble_goal_max_end_to_goal_seconds",
709 AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
710 ),
711 (
712 flip_reset_goal_config,
713 "flip_reset_goal_max_event_to_goal_seconds",
714 FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
715 ),
716 ] {
717 config.insert(
718 key.to_owned(),
719 serialize_to_json_value(
720 &module_config
721 .and_then(|config| config.get(key))
722 .and_then(Value::as_f64)
723 .unwrap_or(default_value as f64),
724 )?,
725 );
726 }
727 Ok(Value::Object(config))
728 }
729
730 fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
731 let mut timeline = Map::new();
732 timeline.insert(
733 "frame_number".to_owned(),
734 serialize_to_json_value(&frame.frame_number)?,
735 );
736 timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
737 timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
738 timeline.insert(
739 "seconds_remaining".to_owned(),
740 serialize_to_json_value(&frame.seconds_remaining)?,
741 );
742 timeline.insert(
743 "game_state".to_owned(),
744 serialize_to_json_value(&frame.game_state)?,
745 );
746 timeline.insert(
747 "gameplay_phase".to_owned(),
748 serialize_to_json_value(&frame.gameplay_phase)?,
749 );
750 timeline.insert(
751 "is_live_play".to_owned(),
752 serialize_to_json_value(&frame.is_live_play)?,
753 );
754 timeline.insert(
755 "fifty_fifty".to_owned(),
756 self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
757 );
758 timeline.insert(
759 "possession".to_owned(),
760 self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
761 );
762 timeline.insert(
763 "pressure".to_owned(),
764 self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
765 );
766 timeline.insert(
767 "rush".to_owned(),
768 self.frame_stats_or_default::<RushStats>(frame, "rush"),
769 );
770 timeline.insert(
771 "team_zero".to_owned(),
772 self.timeline_team_value(frame, "team_zero")?,
773 );
774 timeline.insert(
775 "team_one".to_owned(),
776 self.timeline_team_value(frame, "team_one")?,
777 );
778 timeline.insert(
779 "players".to_owned(),
780 Value::Array(
781 self.replay_meta
782 .player_order()
783 .map(|player| self.timeline_player_value(frame, player))
784 .collect::<SubtrActorResult<Vec<_>>>()?,
785 ),
786 );
787 Ok(Value::Object(timeline))
788 }
789
790 pub(crate) fn replay_stats_frame(
791 &self,
792 frame: &StatsSnapshotFrame,
793 ) -> SubtrActorResult<ReplayStatsFrame> {
794 Ok(ReplayStatsFrame {
795 frame_number: frame.frame_number,
796 time: frame.time,
797 dt: frame.dt,
798 seconds_remaining: frame.seconds_remaining,
799 game_state: frame.game_state,
800 gameplay_phase: frame.gameplay_phase,
801 is_live_play: frame.is_live_play,
802 team_zero: self.replay_team_stats(frame, "team_zero")?,
803 team_one: self.replay_team_stats(frame, "team_one")?,
804 players: self
805 .replay_meta
806 .player_order()
807 .map(|player| self.replay_player_stats(frame, player))
808 .collect::<SubtrActorResult<Vec<_>>>()?,
809 })
810 }
811
812 fn replay_team_stats(
813 &self,
814 frame: &StatsSnapshotFrame,
815 team_key: &str,
816 ) -> SubtrActorResult<TeamStatsSnapshot> {
817 let is_team_zero = team_key == "team_zero";
818 Ok(TeamStatsSnapshot {
819 fifty_fifty: self
820 .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
821 .for_team(is_team_zero),
822 possession: self
823 .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
824 .for_team(is_team_zero),
825 pressure: self
826 .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
827 .for_team(is_team_zero),
828 rush: self
829 .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
830 .for_team(is_team_zero),
831 core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
832 backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
833 double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
834 one_timer: self.frame_team_stat_or_default_typed(frame, "one_timer", team_key)?,
835 pass: self.frame_team_stat_or_default_typed(frame, "pass", team_key)?,
836 ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
837 air_dribble: self.frame_team_stat_or_default_typed(frame, "air_dribble", team_key)?,
838 boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
839 movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
840 powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
841 demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
842 })
843 }
844
845 fn replay_player_stats(
846 &self,
847 frame: &StatsSnapshotFrame,
848 player: &PlayerInfo,
849 ) -> SubtrActorResult<PlayerStatsSnapshot> {
850 let player_key = player_info_key(player)?;
851 Ok(PlayerStatsSnapshot {
852 player_id: player.remote_id.clone(),
853 name: player.name.clone(),
854 is_team_0: self.is_team_zero_player(player),
855 core: self.frame_core_player_stat_or_default_by_key(frame, &player_key)?,
856 backboard: self.frame_player_stat_or_default_typed_by_key(
857 frame,
858 "backboard",
859 &player_key,
860 )?,
861 ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
862 frame,
863 "ceiling_shot",
864 &player_key,
865 )?,
866 double_tap: self.frame_player_stat_or_default_typed_by_key(
867 frame,
868 "double_tap",
869 &player_key,
870 )?,
871 one_timer: self.frame_player_stat_or_default_typed_by_key(
872 frame,
873 "one_timer",
874 &player_key,
875 )?,
876 pass: self.frame_player_stat_or_default_typed_by_key(frame, "pass", &player_key)?,
877 fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
878 frame,
879 "fifty_fifty",
880 &player_key,
881 )?,
882 speed_flip: self.frame_player_stat_or_default_typed_by_key(
883 frame,
884 "speed_flip",
885 &player_key,
886 )?,
887 half_flip: self.frame_player_stat_or_default_typed_by_key(
888 frame,
889 "half_flip",
890 &player_key,
891 )?,
892 wavedash: self.frame_player_stat_or_default_typed_by_key(
893 frame,
894 "wavedash",
895 &player_key,
896 )?,
897 touch: if frame.modules.contains_key("touch") {
898 self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
899 TouchStats::default().with_complete_labeled_touch_counts()
900 })?
901 } else {
902 self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
903 },
904 whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
905 flick: self.frame_player_stat_or_default_typed_by_key(frame, "flick", &player_key)?,
906 musty_flick: self.frame_player_stat_or_default_typed_by_key(
907 frame,
908 "musty_flick",
909 &player_key,
910 )?,
911 dodge_reset: self.frame_player_stat_or_default_typed_by_key(
912 frame,
913 "dodge_reset",
914 &player_key,
915 )?,
916 ball_carry: self.frame_player_stat_or_default_typed_by_key(
917 frame,
918 "ball_carry",
919 &player_key,
920 )?,
921 air_dribble: self.frame_player_stat_or_default_typed_by_key(
922 frame,
923 "air_dribble",
924 &player_key,
925 )?,
926 boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
927 movement: self.frame_player_stat_or_default_with_by_key(
928 frame,
929 "movement",
930 &player_key,
931 || MovementStats::default().with_complete_labeled_tracked_time(),
932 )?,
933 positioning: self.frame_player_stat_or_default_typed_by_key(
934 frame,
935 "positioning",
936 &player_key,
937 )?,
938 powerslide: self.frame_player_stat_or_default_typed_by_key(
939 frame,
940 "powerslide",
941 &player_key,
942 )?,
943 demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
944 })
945 }
946
947 fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
948 self.replay_meta
949 .team_zero
950 .iter()
951 .any(|team_player| team_player.remote_id == player.remote_id)
952 }
953
954 fn timeline_team_value(
955 &self,
956 frame: &StatsSnapshotFrame,
957 team_key: &str,
958 ) -> SubtrActorResult<Value> {
959 let is_team_zero = team_key == "team_zero";
960 let mut team = Map::new();
961 team.insert(
962 "fifty_fifty".to_owned(),
963 serialize_to_json_value(
964 &self
965 .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
966 .for_team(is_team_zero),
967 )?,
968 );
969 team.insert(
970 "possession".to_owned(),
971 serialize_to_json_value(
972 &self
973 .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
974 .for_team(is_team_zero),
975 )?,
976 );
977 team.insert(
978 "pressure".to_owned(),
979 serialize_to_json_value(
980 &self
981 .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
982 .for_team(is_team_zero),
983 )?,
984 );
985 team.insert(
986 "rush".to_owned(),
987 serialize_to_json_value(
988 &self
989 .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
990 .for_team(is_team_zero),
991 )?,
992 );
993 team.insert(
994 "core".to_owned(),
995 self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
996 );
997 team.insert(
998 "backboard".to_owned(),
999 self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
1000 );
1001 team.insert(
1002 "double_tap".to_owned(),
1003 self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
1004 );
1005 team.insert(
1006 "one_timer".to_owned(),
1007 self.frame_team_stat_or_default::<OneTimerTeamStats>(frame, "one_timer", team_key),
1008 );
1009 team.insert(
1010 "pass".to_owned(),
1011 self.frame_team_stat_or_default::<PassTeamStats>(frame, "pass", team_key),
1012 );
1013 team.insert(
1014 "ball_carry".to_owned(),
1015 self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
1016 );
1017 team.insert(
1018 "air_dribble".to_owned(),
1019 self.frame_team_stat_or_default::<AirDribbleStats>(frame, "air_dribble", team_key),
1020 );
1021 team.insert(
1022 "boost".to_owned(),
1023 self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
1024 );
1025 team.insert(
1026 "movement".to_owned(),
1027 self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
1028 );
1029 team.insert(
1030 "powerslide".to_owned(),
1031 self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
1032 );
1033 team.insert(
1034 "demo".to_owned(),
1035 self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
1036 );
1037 Ok(Value::Object(team))
1038 }
1039
1040 fn timeline_player_value(
1041 &self,
1042 frame: &StatsSnapshotFrame,
1043 player: &PlayerInfo,
1044 ) -> SubtrActorResult<Value> {
1045 let player_key = player_info_key(player)?;
1046 let mut player_value = Map::new();
1047 player_value.insert(
1048 "player_id".to_owned(),
1049 serialize_to_json_value(&player.remote_id)?,
1050 );
1051 player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
1052 player_value.insert(
1053 "is_team_0".to_owned(),
1054 serialize_to_json_value(
1055 &self
1056 .replay_meta
1057 .team_zero
1058 .iter()
1059 .any(|team_player| team_player.remote_id == player.remote_id),
1060 )?,
1061 );
1062 player_value.insert(
1063 "core".to_owned(),
1064 self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
1065 frame,
1066 "core",
1067 &player_key,
1068 )?,
1069 );
1070 player_value.insert(
1071 "backboard".to_owned(),
1072 self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
1073 frame,
1074 "backboard",
1075 &player_key,
1076 )?,
1077 );
1078 player_value.insert(
1079 "ceiling_shot".to_owned(),
1080 self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
1081 frame,
1082 "ceiling_shot",
1083 &player_key,
1084 )?,
1085 );
1086 player_value.insert(
1087 "double_tap".to_owned(),
1088 self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
1089 frame,
1090 "double_tap",
1091 &player_key,
1092 )?,
1093 );
1094 player_value.insert(
1095 "one_timer".to_owned(),
1096 self.frame_player_stat_or_default_by_key::<OneTimerPlayerStats>(
1097 frame,
1098 "one_timer",
1099 &player_key,
1100 )?,
1101 );
1102 player_value.insert(
1103 "pass".to_owned(),
1104 self.frame_player_stat_or_default_by_key::<PassPlayerStats>(
1105 frame,
1106 "pass",
1107 &player_key,
1108 )?,
1109 );
1110 player_value.insert(
1111 "fifty_fifty".to_owned(),
1112 self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
1113 frame,
1114 "fifty_fifty",
1115 &player_key,
1116 )?,
1117 );
1118 player_value.insert(
1119 "speed_flip".to_owned(),
1120 self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
1121 frame,
1122 "speed_flip",
1123 &player_key,
1124 )?,
1125 );
1126 player_value.insert(
1127 "half_flip".to_owned(),
1128 self.frame_player_stat_or_default_by_key::<HalfFlipStats>(
1129 frame,
1130 "half_flip",
1131 &player_key,
1132 )?,
1133 );
1134 player_value.insert(
1135 "wavedash".to_owned(),
1136 self.frame_player_stat_or_default_by_key::<WavedashStats>(
1137 frame,
1138 "wavedash",
1139 &player_key,
1140 )?,
1141 );
1142 player_value.insert(
1143 "touch".to_owned(),
1144 self.frame_player_stat_or_value_by_key(
1145 frame,
1146 "touch",
1147 &player_key,
1148 if frame.modules.contains_key("touch") {
1149 serialize_to_json_value(
1150 &TouchStats::default().with_complete_labeled_touch_counts(),
1151 )?
1152 } else {
1153 default_json_value::<TouchStats>()
1154 },
1155 )?,
1156 );
1157 player_value.insert(
1158 "whiff".to_owned(),
1159 self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
1160 );
1161 player_value.insert(
1162 "flick".to_owned(),
1163 self.frame_player_stat_or_default_by_key::<FlickStats>(frame, "flick", &player_key)?,
1164 );
1165 player_value.insert(
1166 "musty_flick".to_owned(),
1167 self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
1168 frame,
1169 "musty_flick",
1170 &player_key,
1171 )?,
1172 );
1173 player_value.insert(
1174 "dodge_reset".to_owned(),
1175 self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
1176 frame,
1177 "dodge_reset",
1178 &player_key,
1179 )?,
1180 );
1181 player_value.insert(
1182 "ball_carry".to_owned(),
1183 self.frame_player_stat_or_default_by_key::<BallCarryStats>(
1184 frame,
1185 "ball_carry",
1186 &player_key,
1187 )?,
1188 );
1189 player_value.insert(
1190 "air_dribble".to_owned(),
1191 self.frame_player_stat_or_default_by_key::<AirDribbleStats>(
1192 frame,
1193 "air_dribble",
1194 &player_key,
1195 )?,
1196 );
1197 player_value.insert(
1198 "boost".to_owned(),
1199 self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
1200 );
1201 player_value.insert(
1202 "movement".to_owned(),
1203 self.frame_player_stat_or_value_by_key(
1204 frame,
1205 "movement",
1206 &player_key,
1207 if frame.modules.contains_key("movement") {
1208 serialize_to_json_value(
1209 &MovementStats::default().with_complete_labeled_tracked_time(),
1210 )?
1211 } else {
1212 default_json_value::<MovementStats>()
1213 },
1214 )?,
1215 );
1216 player_value.insert(
1217 "positioning".to_owned(),
1218 self.frame_player_stat_or_default_by_key::<PositioningStats>(
1219 frame,
1220 "positioning",
1221 &player_key,
1222 )?,
1223 );
1224 player_value.insert(
1225 "powerslide".to_owned(),
1226 self.frame_player_stat_or_default_by_key::<PowerslideStats>(
1227 frame,
1228 "powerslide",
1229 &player_key,
1230 )?,
1231 );
1232 player_value.insert(
1233 "demo".to_owned(),
1234 self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
1235 frame,
1236 "demo",
1237 &player_key,
1238 )?,
1239 );
1240 Ok(Value::Object(player_value))
1241 }
1242
1243 fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
1244 where
1245 T: Default + Serialize,
1246 {
1247 frame
1248 .modules
1249 .get(module_name)
1250 .and_then(Value::as_object)
1251 .and_then(|module| module.get("stats"))
1252 .cloned()
1253 .unwrap_or_else(|| default_json_value::<T>())
1254 }
1255
1256 fn frame_team_stat_or_default<T>(
1257 &self,
1258 frame: &StatsSnapshotFrame,
1259 module_name: &str,
1260 team_key: &str,
1261 ) -> Value
1262 where
1263 T: Default + Serialize,
1264 {
1265 frame
1266 .modules
1267 .get(module_name)
1268 .and_then(Value::as_object)
1269 .and_then(|module| module.get(team_key))
1270 .cloned()
1271 .unwrap_or_else(|| default_json_value::<T>())
1272 }
1273
1274 fn frame_player_stat_or_default_by_key<T>(
1275 &self,
1276 frame: &StatsSnapshotFrame,
1277 module_name: &str,
1278 player_key: &str,
1279 ) -> SubtrActorResult<Value>
1280 where
1281 T: Default + Serialize,
1282 {
1283 self.frame_player_stat_or_value_by_key(
1284 frame,
1285 module_name,
1286 player_key,
1287 default_json_value::<T>(),
1288 )
1289 }
1290
1291 fn frame_player_stat_or_value_by_key(
1292 &self,
1293 frame: &StatsSnapshotFrame,
1294 module_name: &str,
1295 player_key: &str,
1296 default_value: Value,
1297 ) -> SubtrActorResult<Value> {
1298 Ok(
1299 player_stats_value_for_key(frame.modules.get(module_name), player_key)?
1300 .cloned()
1301 .unwrap_or(default_value),
1302 )
1303 }
1304
1305 fn frame_stats_or_default_typed<T>(
1306 &self,
1307 frame: &StatsSnapshotFrame,
1308 module_name: &str,
1309 ) -> SubtrActorResult<T>
1310 where
1311 T: Default + DeserializeOwned + Serialize,
1312 {
1313 decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
1314 }
1315
1316 fn frame_team_stat_or_default_typed<T>(
1317 &self,
1318 frame: &StatsSnapshotFrame,
1319 module_name: &str,
1320 team_key: &str,
1321 ) -> SubtrActorResult<T>
1322 where
1323 T: Default + DeserializeOwned + Serialize,
1324 {
1325 decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
1326 }
1327
1328 fn frame_player_stat_or_default_typed_by_key<T>(
1329 &self,
1330 frame: &StatsSnapshotFrame,
1331 module_name: &str,
1332 player_key: &str,
1333 ) -> SubtrActorResult<T>
1334 where
1335 T: Default + DeserializeOwned + Serialize,
1336 {
1337 self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
1338 }
1339
1340 fn frame_core_player_stat_or_default_by_key(
1341 &self,
1342 frame: &StatsSnapshotFrame,
1343 player_key: &str,
1344 ) -> SubtrActorResult<CorePlayerStats> {
1345 decode_core_player_stats_value(self.frame_player_stat_or_value_by_key(
1346 frame,
1347 "core",
1348 player_key,
1349 default_json_value::<CorePlayerStats>(),
1350 )?)
1351 }
1352
1353 fn frame_player_stat_or_default_with_by_key<T, F>(
1354 &self,
1355 frame: &StatsSnapshotFrame,
1356 module_name: &str,
1357 player_key: &str,
1358 default: F,
1359 ) -> SubtrActorResult<T>
1360 where
1361 T: DeserializeOwned + Serialize,
1362 F: FnOnce() -> T,
1363 {
1364 decode_json_value(self.frame_player_stat_or_value_by_key(
1365 frame,
1366 module_name,
1367 player_key,
1368 serialize_to_json_value(&default())?,
1369 )?)
1370 }
1371
1372 fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
1373 where
1374 T: DeserializeOwned,
1375 {
1376 decode_json_value(Value::Array(self.module_array(module_name, field)))
1377 }
1378
1379 fn module_player_events<T, F>(
1380 &self,
1381 module_name: &str,
1382 field: &str,
1383 parse: F,
1384 ) -> SubtrActorResult<Vec<T>>
1385 where
1386 F: Fn(&Value) -> SubtrActorResult<T>,
1387 {
1388 self.module_array(module_name, field)
1389 .iter()
1390 .map(parse)
1391 .collect()
1392 }
1393
1394 fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
1395 self.modules
1396 .get(module_name)
1397 .and_then(Value::as_object)
1398 .and_then(|module| module.get(field))
1399 .and_then(Value::as_array)
1400 .cloned()
1401 .unwrap_or_default()
1402 }
1403}
1404
1405impl CapturedStatsData<ReplayStatsFrame> {
1406 pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
1407 let CapturedStatsData {
1408 replay_meta,
1409 config,
1410 modules,
1411 frames,
1412 } = self;
1413 CapturedStatsData::<StatsSnapshotFrame> {
1414 replay_meta,
1415 config,
1416 modules,
1417 frames: Vec::new(),
1418 }
1419 .into_replay_stats_timeline_with_frames(frames)
1420 }
1421}
1422
1423fn player_stats_value_for_key<'a>(
1424 module: Option<&'a Value>,
1425 player_key: &str,
1426) -> SubtrActorResult<Option<&'a Value>> {
1427 let Some(entries) = module
1428 .and_then(Value::as_object)
1429 .and_then(|module| module.get("player_stats"))
1430 .and_then(Value::as_array)
1431 else {
1432 return Ok(None);
1433 };
1434
1435 for entry in entries {
1436 let Some(entry_object) = entry.as_object() else {
1437 continue;
1438 };
1439 let Some(player_id) = entry_object.get("player_id") else {
1440 continue;
1441 };
1442 let Some(player_stats) = entry_object.get("stats") else {
1443 continue;
1444 };
1445 if player_id_key(player_id)? == player_key {
1446 return Ok(Some(player_stats));
1447 }
1448 }
1449
1450 Ok(None)
1451}
1452
1453fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
1454 player_id_key(&serialize_to_json_value(&player.remote_id)?)
1455}
1456
1457fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
1458 serde_json::to_string(player_id).map_err(|error| {
1459 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1460 error.to_string(),
1461 ))
1462 })
1463}
1464
1465fn default_json_value<T>() -> Value
1466where
1467 T: Default + Serialize,
1468{
1469 serde_json::to_value(T::default()).expect("default stats should serialize to json")
1470}
1471
1472fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
1473where
1474 T: DeserializeOwned,
1475{
1476 serde_json::from_value(value).map_err(|error| {
1477 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1478 error.to_string(),
1479 ))
1480 })
1481}
1482
1483fn decode_core_player_stats_value(mut value: Value) -> SubtrActorResult<CorePlayerStats> {
1484 normalize_core_player_stats_snapshot(&mut value)?;
1485 decode_json_value(value)
1486}
1487
1488fn normalize_core_player_stats_snapshot(value: &mut Value) -> SubtrActorResult<()> {
1489 let Some(object) = value.as_object_mut() else {
1490 return Ok(());
1491 };
1492
1493 insert_cumulative_from_average(
1494 object,
1495 "cumulative_boost_on_goals_against",
1496 "average_boost_on_goals_against",
1497 "goal_against_boost_sample_count",
1498 )?;
1499 insert_cumulative_from_average(
1500 object,
1501 "cumulative_average_boost_in_goal_against_leadup",
1502 "average_boost_in_goal_against_leadup",
1503 "goal_against_boost_leadup_sample_count",
1504 )?;
1505 insert_cumulative_from_average(
1506 object,
1507 "cumulative_min_boost_in_goal_against_leadup",
1508 "average_min_boost_in_goal_against_leadup",
1509 "goal_against_boost_leadup_sample_count",
1510 )?;
1511 insert_cumulative_from_average(
1512 object,
1513 "cumulative_goal_against_position_x",
1514 "average_goal_against_position_x",
1515 "goal_against_position_sample_count",
1516 )?;
1517 insert_cumulative_from_average(
1518 object,
1519 "cumulative_goal_against_position_y",
1520 "average_goal_against_position_y",
1521 "goal_against_position_sample_count",
1522 )?;
1523 insert_cumulative_from_average(
1524 object,
1525 "cumulative_goal_against_position_z",
1526 "average_goal_against_position_z",
1527 "goal_against_position_sample_count",
1528 )?;
1529 insert_cumulative_from_average(
1530 object,
1531 "cumulative_scoring_goal_last_touch_position_x",
1532 "average_scoring_goal_last_touch_position_x",
1533 "scoring_goal_last_touch_position_sample_count",
1534 )?;
1535 insert_cumulative_from_average(
1536 object,
1537 "cumulative_scoring_goal_last_touch_position_y",
1538 "average_scoring_goal_last_touch_position_y",
1539 "scoring_goal_last_touch_position_sample_count",
1540 )?;
1541 insert_cumulative_from_average(
1542 object,
1543 "cumulative_scoring_goal_last_touch_position_z",
1544 "average_scoring_goal_last_touch_position_z",
1545 "scoring_goal_last_touch_position_sample_count",
1546 )?;
1547
1548 if let Value::Object(defaults) = default_json_value::<CorePlayerStats>() {
1549 for (field, default_value) in defaults {
1550 object.entry(field).or_insert(default_value);
1551 }
1552 }
1553
1554 Ok(())
1555}
1556
1557fn insert_cumulative_from_average(
1558 object: &mut Map<String, Value>,
1559 cumulative_field: &str,
1560 average_field: &str,
1561 sample_count_field: &str,
1562) -> SubtrActorResult<()> {
1563 if object.contains_key(cumulative_field) {
1564 return Ok(());
1565 }
1566
1567 let average = object
1568 .get(average_field)
1569 .and_then(Value::as_f64)
1570 .unwrap_or(0.0) as f32;
1571 let sample_count = object
1572 .get(sample_count_field)
1573 .and_then(Value::as_u64)
1574 .unwrap_or(0) as f32;
1575 object.insert(
1576 cumulative_field.to_owned(),
1577 serialize_to_json_value(&(average * sample_count))?,
1578 );
1579
1580 Ok(())
1581}
1582
1583fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
1584 let object = json_object(value, "timeline event")?;
1585 Ok(TimelineEvent {
1586 time: json_required_f32(object, "time")?,
1587 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1588 player_id: json_optional_remote_id(object.get("player_id"))?,
1589 is_team_0: json_optional_bool(object.get("is_team_0")),
1590 })
1591}
1592
1593fn moment_mechanic_event(
1594 kind: &str,
1595 index: usize,
1596 frame: usize,
1597 time: f32,
1598 player_id: PlayerId,
1599 is_team_0: bool,
1600) -> MechanicEvent {
1601 MechanicEvent {
1602 id: format!("{kind}:{frame}:{index}"),
1603 kind: kind.to_owned(),
1604 player_id,
1605 is_team_0,
1606 timing: MechanicTiming::Moment { frame, time },
1607 }
1608}
1609
1610fn span_mechanic_event(
1611 kind: &str,
1612 index: usize,
1613 start_frame: usize,
1614 end_frame: usize,
1615 start_time: f32,
1616 end_time: f32,
1617 player_id: PlayerId,
1618 is_team_0: bool,
1619) -> MechanicEvent {
1620 MechanicEvent {
1621 id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
1622 kind: kind.to_owned(),
1623 player_id,
1624 is_team_0,
1625 timing: MechanicTiming::Span {
1626 start_frame,
1627 end_frame,
1628 start_time,
1629 end_time,
1630 },
1631 }
1632}
1633
1634fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
1635 match event.timing {
1636 MechanicTiming::Moment { time, .. } => time,
1637 MechanicTiming::Span { start_time, .. } => start_time,
1638 }
1639}
1640
1641fn parse_ball_carry_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
1642 let object = json_object(value, "ball carry mechanic event")?;
1643 let serialized_kind = json_required_str(object, "kind")?;
1644 let kind = match serialized_kind {
1645 "carry" => "ball_carry",
1646 "air_dribble" => "air_dribble",
1647 other => other,
1648 };
1649 Ok(span_mechanic_event(
1650 kind,
1651 index,
1652 json_required_usize(object, "start_frame")?,
1653 json_required_usize(object, "end_frame")?,
1654 json_required_f32(object, "start_time")?,
1655 json_required_f32(object, "end_time")?,
1656 json_required_remote_id(object, "player_id")?,
1657 json_required_bool(object, "is_team_0")?,
1658 ))
1659}
1660
1661fn parse_dodge_reset_mechanic_event(
1662 value: &Value,
1663 index: usize,
1664) -> SubtrActorResult<MechanicEvent> {
1665 let object = json_object(value, "dodge reset mechanic event")?;
1666 Ok(moment_mechanic_event(
1667 "flip_reset",
1668 index,
1669 json_required_usize(object, "frame")?,
1670 json_required_f32(object, "time")?,
1671 json_required_remote_id(object, "player")?,
1672 json_required_bool(object, "is_team_0")?,
1673 ))
1674}
1675
1676fn parse_flick_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
1677 let object = json_object(value, "flick mechanic event")?;
1678 Ok(span_mechanic_event(
1679 "flick",
1680 index,
1681 json_required_usize(object, "setup_start_frame")?,
1682 json_required_usize(object, "frame")?,
1683 json_required_f32(object, "setup_start_time")?,
1684 json_required_f32(object, "time")?,
1685 json_required_remote_id(object, "player")?,
1686 json_required_bool(object, "is_team_0")?,
1687 ))
1688}
1689
1690fn parse_musty_flick_mechanic_event(
1691 value: &Value,
1692 index: usize,
1693) -> SubtrActorResult<MechanicEvent> {
1694 let object = json_object(value, "musty flick mechanic event")?;
1695 Ok(span_mechanic_event(
1696 "musty_flick",
1697 index,
1698 json_required_usize(object, "dodge_frame")?,
1699 json_required_usize(object, "frame")?,
1700 json_required_f32(object, "dodge_time")?,
1701 json_required_f32(object, "time")?,
1702 json_required_remote_id(object, "player")?,
1703 json_required_bool(object, "is_team_0")?,
1704 ))
1705}
1706
1707fn parse_goal_context_event(value: &Value) -> SubtrActorResult<GoalContextEvent> {
1708 let object = json_object(value, "goal context event")?;
1709 Ok(GoalContextEvent {
1710 time: json_required_f32(object, "time")?,
1711 frame: json_required_usize(object, "frame")?,
1712 scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
1713 scorer: json_optional_remote_id(object.get("scorer"))?,
1714 scoring_team_most_back_player: json_optional_remote_id(
1715 object.get("scoring_team_most_back_player"),
1716 )?,
1717 defending_team_most_back_player: json_optional_remote_id(
1718 object.get("defending_team_most_back_player"),
1719 )?,
1720 ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
1721 scorer_last_touch: match object.get("scorer_last_touch") {
1722 None | Some(Value::Null) => None,
1723 Some(value) => Some(parse_goal_touch_context(value)?),
1724 },
1725 players: json_required_array(object, "players")?
1726 .iter()
1727 .map(parse_goal_player_context)
1728 .collect::<SubtrActorResult<Vec<_>>>()?,
1729 })
1730}
1731
1732fn parse_goal_player_context(value: &Value) -> SubtrActorResult<GoalPlayerContext> {
1733 let object = json_object(value, "goal player context")?;
1734 Ok(GoalPlayerContext {
1735 player: json_required_remote_id(object, "player")?,
1736 is_team_0: json_required_bool(object, "is_team_0")?,
1737 position: json_optional_goal_context_position(object.get("position"))?,
1738 boost_amount: json_optional_f32(object.get("boost_amount"))?,
1739 average_boost_in_leadup: json_optional_f32(object.get("average_boost_in_leadup"))?,
1740 min_boost_in_leadup: json_optional_f32(object.get("min_boost_in_leadup"))?,
1741 is_most_back: json_required_bool(object, "is_most_back")?,
1742 })
1743}
1744
1745fn parse_goal_touch_context(value: &Value) -> SubtrActorResult<GoalTouchContext> {
1746 let object = json_object(value, "goal touch context")?;
1747 Ok(GoalTouchContext {
1748 time: json_required_f32(object, "time")?,
1749 frame: json_required_usize(object, "frame")?,
1750 player: json_required_remote_id(object, "player")?,
1751 is_team_0: json_required_bool(object, "is_team_0")?,
1752 ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
1753 player_position: json_optional_goal_context_position(object.get("player_position"))?,
1754 players: match object.get("players").and_then(Value::as_array) {
1755 Some(players) => players
1756 .iter()
1757 .map(parse_goal_player_context)
1758 .collect::<SubtrActorResult<Vec<_>>>()?,
1759 None => Vec::new(),
1760 },
1761 })
1762}
1763
1764fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
1765 let object = json_object(value, "backboard event")?;
1766 Ok(BackboardBounceEvent {
1767 time: json_required_f32(object, "time")?,
1768 frame: json_required_usize(object, "frame")?,
1769 player: json_required_remote_id(object, "player")?,
1770 is_team_0: json_required_bool(object, "is_team_0")?,
1771 })
1772}
1773
1774fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
1775 let object = json_object(value, "ceiling shot event")?;
1776 Ok(CeilingShotEvent {
1777 time: json_required_f32(object, "time")?,
1778 frame: json_required_usize(object, "frame")?,
1779 player: json_required_remote_id(object, "player")?,
1780 is_team_0: json_required_bool(object, "is_team_0")?,
1781 ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
1782 ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
1783 time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
1784 ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
1785 touch_position: json_required_vec3(object, "touch_position")?,
1786 local_ball_position: json_required_vec3(object, "local_ball_position")?,
1787 separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
1788 roof_alignment: json_required_f32(object, "roof_alignment")?,
1789 forward_alignment: json_required_f32(object, "forward_alignment")?,
1790 forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
1791 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
1792 confidence: json_required_f32(object, "confidence")?,
1793 })
1794}
1795
1796fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
1797 let object = json_object(value, "double tap event")?;
1798 Ok(DoubleTapEvent {
1799 time: json_required_f32(object, "time")?,
1800 frame: json_required_usize(object, "frame")?,
1801 player: json_required_remote_id(object, "player")?,
1802 is_team_0: json_required_bool(object, "is_team_0")?,
1803 backboard_time: json_required_f32(object, "backboard_time")?,
1804 backboard_frame: json_required_usize(object, "backboard_frame")?,
1805 })
1806}
1807
1808fn parse_pass_event(value: &Value) -> SubtrActorResult<PassEvent> {
1809 let object = json_object(value, "pass event")?;
1810 Ok(PassEvent {
1811 time: json_required_f32(object, "time")?,
1812 frame: json_required_usize(object, "frame")?,
1813 passer: json_required_remote_id(object, "passer")?,
1814 receiver: json_required_remote_id(object, "receiver")?,
1815 is_team_0: json_required_bool(object, "is_team_0")?,
1816 start_time: json_required_f32(object, "start_time")?,
1817 start_frame: json_required_usize(object, "start_frame")?,
1818 duration: json_required_f32(object, "duration")?,
1819 ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
1820 ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
1821 })
1822}
1823
1824fn parse_one_timer_event(value: &Value) -> SubtrActorResult<OneTimerEvent> {
1825 let object = json_object(value, "one timer event")?;
1826 Ok(OneTimerEvent {
1827 time: json_required_f32(object, "time")?,
1828 frame: json_required_usize(object, "frame")?,
1829 player: json_required_remote_id(object, "player")?,
1830 passer: json_required_remote_id(object, "passer")?,
1831 is_team_0: json_required_bool(object, "is_team_0")?,
1832 pass_start_time: json_required_f32(object, "pass_start_time")?,
1833 pass_start_frame: json_required_usize(object, "pass_start_frame")?,
1834 pass_duration: json_required_f32(object, "pass_duration")?,
1835 pass_travel_distance: json_required_f32(object, "pass_travel_distance")?,
1836 pass_advance_distance: json_required_f32(object, "pass_advance_distance")?,
1837 ball_speed: json_required_f32(object, "ball_speed")?,
1838 goal_alignment: json_required_f32(object, "goal_alignment")?,
1839 })
1840}
1841
1842fn parse_goal_tag_event(value: &Value) -> SubtrActorResult<GoalTagEvent> {
1843 let object = json_object(value, "goal tag event")?;
1844 Ok(GoalTagEvent {
1845 goal_index: json_required_usize(object, "goal_index")?,
1846 time: json_required_f32(object, "time")?,
1847 frame: json_required_usize(object, "frame")?,
1848 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1849 scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
1850 scorer: json_optional_remote_id(object.get("scorer"))?,
1851 confidence: json_required_f32(object, "confidence")?,
1852 modifiers: json_optional_array(object.get("modifiers"))?
1853 .iter()
1854 .map(|modifier| decode_json_value(modifier.clone()))
1855 .collect::<SubtrActorResult<Vec<_>>>()?,
1856 evidence: json_required_array(object, "evidence")?
1857 .iter()
1858 .map(parse_goal_tag_evidence)
1859 .collect::<SubtrActorResult<Vec<_>>>()?,
1860 })
1861}
1862
1863fn parse_goal_tag_evidence(value: &Value) -> SubtrActorResult<GoalTagEvidence> {
1864 let object = json_object(value, "goal tag evidence")?;
1865 Ok(GoalTagEvidence {
1866 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1867 time: json_required_f32(object, "time")?,
1868 frame: json_required_usize(object, "frame")?,
1869 player: json_optional_remote_id(object.get("player"))?,
1870 })
1871}
1872
1873fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
1874 let object = json_object(value, "fifty fifty event")?;
1875 Ok(FiftyFiftyEvent {
1876 start_time: json_required_f32(object, "start_time")?,
1877 start_frame: json_required_usize(object, "start_frame")?,
1878 resolve_time: json_required_f32(object, "resolve_time")?,
1879 resolve_frame: json_required_usize(object, "resolve_frame")?,
1880 is_kickoff: json_required_bool(object, "is_kickoff")?,
1881 team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
1882 team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
1883 team_zero_position: json_required_vec3(object, "team_zero_position")?,
1884 team_one_position: json_required_vec3(object, "team_one_position")?,
1885 midpoint: json_required_vec3(object, "midpoint")?,
1886 plane_normal: json_required_vec3(object, "plane_normal")?,
1887 winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
1888 possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
1889 })
1890}
1891
1892fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
1893 let object = json_object(value, "speed flip event")?;
1894 Ok(SpeedFlipEvent {
1895 time: json_required_f32(object, "time")?,
1896 frame: json_required_usize(object, "frame")?,
1897 player: json_required_remote_id(object, "player")?,
1898 is_team_0: json_required_bool(object, "is_team_0")?,
1899 time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
1900 start_position: json_required_vec3(object, "start_position")?,
1901 end_position: json_required_vec3(object, "end_position")?,
1902 start_speed: json_required_f32(object, "start_speed")?,
1903 max_speed: json_required_f32(object, "max_speed")?,
1904 best_alignment: json_required_f32(object, "best_alignment")?,
1905 diagonal_score: json_required_f32(object, "diagonal_score")?,
1906 cancel_score: json_required_f32(object, "cancel_score")?,
1907 speed_score: json_required_f32(object, "speed_score")?,
1908 confidence: json_required_f32(object, "confidence")?,
1909 })
1910}
1911
1912fn parse_half_flip_event(value: &Value) -> SubtrActorResult<HalfFlipEvent> {
1913 let object = json_object(value, "half flip event")?;
1914 Ok(HalfFlipEvent {
1915 time: json_required_f32(object, "time")?,
1916 frame: json_required_usize(object, "frame")?,
1917 player: json_required_remote_id(object, "player")?,
1918 is_team_0: json_required_bool(object, "is_team_0")?,
1919 start_position: json_required_vec3(object, "start_position")?,
1920 end_position: json_required_vec3(object, "end_position")?,
1921 start_speed: json_required_f32(object, "start_speed")?,
1922 end_speed: json_required_f32(object, "end_speed")?,
1923 start_backward_alignment: json_required_f32(object, "start_backward_alignment")?,
1924 best_reorientation_alignment: json_required_f32(object, "best_reorientation_alignment")?,
1925 best_forward_reversal: json_required_f32(object, "best_forward_reversal")?,
1926 max_forward_vertical: json_required_f32(object, "max_forward_vertical")?,
1927 confidence: json_required_f32(object, "confidence")?,
1928 })
1929}
1930
1931fn parse_wavedash_event(value: &Value) -> SubtrActorResult<WavedashEvent> {
1932 let object = json_object(value, "wavedash event")?;
1933 Ok(WavedashEvent {
1934 time: json_required_f32(object, "time")?,
1935 frame: json_required_usize(object, "frame")?,
1936 player: json_required_remote_id(object, "player")?,
1937 is_team_0: json_required_bool(object, "is_team_0")?,
1938 dodge_time: json_required_f32(object, "dodge_time")?,
1939 dodge_frame: json_required_usize(object, "dodge_frame")?,
1940 time_since_dodge: json_required_f32(object, "time_since_dodge")?,
1941 dodge_position: json_required_vec3(object, "dodge_position")?,
1942 landing_position: json_required_vec3(object, "landing_position")?,
1943 start_speed: json_required_f32(object, "start_speed")?,
1944 landing_speed: json_required_f32(object, "landing_speed")?,
1945 horizontal_speed_gain: json_required_f32(object, "horizontal_speed_gain")?,
1946 landing_uprightness: json_required_f32(object, "landing_uprightness")?,
1947 confidence: json_required_f32(object, "confidence")?,
1948 })
1949}
1950
1951fn parse_whiff_event(value: &Value) -> SubtrActorResult<WhiffEvent> {
1952 let object = json_object(value, "whiff event")?;
1953 Ok(WhiffEvent {
1954 time: json_required_f32(object, "time")?,
1955 frame: json_required_usize(object, "frame")?,
1956 player: json_required_remote_id(object, "player")?,
1957 is_team_0: json_required_bool(object, "is_team_0")?,
1958 closest_approach_distance: json_required_f32(object, "closest_approach_distance")?,
1959 forward_alignment: json_required_f32(object, "forward_alignment")?,
1960 approach_speed: json_required_f32(object, "approach_speed")?,
1961 dodge_active: json_required_bool(object, "dodge_active")?,
1962 aerial: json_required_bool(object, "aerial")?,
1963 })
1964}
1965
1966fn parse_boost_pickup_comparison_event(
1967 value: &Value,
1968) -> SubtrActorResult<BoostPickupComparisonEvent> {
1969 let object = json_object(value, "boost pickup comparison event")?;
1970 Ok(BoostPickupComparisonEvent {
1971 comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
1972 frame: json_required_usize(object, "frame")?,
1973 time: json_required_f32(object, "time")?,
1974 player_id: json_required_remote_id(object, "player_id")?,
1975 is_team_0: json_required_bool(object, "is_team_0")?,
1976 pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
1977 field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
1978 activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
1979 reported_frame: json_optional_usize(object.get("reported_frame"))?,
1980 reported_time: json_optional_f32(object.get("reported_time"))?,
1981 inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
1982 inferred_time: json_optional_f32(object.get("inferred_time"))?,
1983 boost_before: json_optional_f32(object.get("boost_before"))?,
1984 boost_after: json_optional_f32(object.get("boost_after"))?,
1985 })
1986}
1987
1988fn json_object<'a>(
1989 value: &'a Value,
1990 context: &str,
1991) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
1992 value.as_object().ok_or_else(|| {
1993 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1994 "Expected {context} to be a JSON object"
1995 )))
1996 })
1997}
1998
1999fn json_required_value<'a>(
2000 object: &'a serde_json::Map<String, Value>,
2001 field: &str,
2002) -> SubtrActorResult<&'a Value> {
2003 object.get(field).ok_or_else(|| {
2004 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2005 "Missing JSON field '{field}'"
2006 )))
2007 })
2008}
2009
2010fn json_required_array<'a>(
2011 object: &'a serde_json::Map<String, Value>,
2012 field: &str,
2013) -> SubtrActorResult<&'a Vec<Value>> {
2014 json_required_value(object, field)?
2015 .as_array()
2016 .ok_or_else(|| {
2017 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2018 "Expected JSON field '{field}' to be an array"
2019 )))
2020 })
2021}
2022
2023fn json_optional_array<'a>(value: Option<&'a Value>) -> SubtrActorResult<&'a [Value]> {
2024 match value {
2025 Some(Value::Array(values)) => Ok(values),
2026 Some(_) => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2027 "Expected optional JSON value to be an array".to_owned(),
2028 )),
2029 None => Ok(&[]),
2030 }
2031}
2032
2033fn json_f32(value: &Value) -> Option<f32> {
2034 value.as_f64().map(|number| number as f32)
2035}
2036
2037fn json_config_f32(
2038 config: Option<&Map<String, Value>>,
2039 key: &str,
2040 legacy_key: &str,
2041) -> Option<f32> {
2042 config.and_then(|config| {
2043 config
2044 .get(key)
2045 .or_else(|| config.get(legacy_key))
2046 .and_then(json_f32)
2047 })
2048}
2049
2050fn json_required_f32(
2051 object: &serde_json::Map<String, Value>,
2052 field: &str,
2053) -> SubtrActorResult<f32> {
2054 json_f32(json_required_value(object, field)?).ok_or_else(|| {
2055 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2056 "Expected JSON field '{field}' to be a float"
2057 )))
2058 })
2059}
2060
2061fn json_required_usize(
2062 object: &serde_json::Map<String, Value>,
2063 field: &str,
2064) -> SubtrActorResult<usize> {
2065 json_required_value(object, field)?
2066 .as_u64()
2067 .map(|number| number as usize)
2068 .ok_or_else(|| {
2069 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2070 "Expected JSON field '{field}' to be an unsigned integer"
2071 )))
2072 })
2073}
2074
2075fn json_required_bool(
2076 object: &serde_json::Map<String, Value>,
2077 field: &str,
2078) -> SubtrActorResult<bool> {
2079 json_required_value(object, field)?
2080 .as_bool()
2081 .ok_or_else(|| {
2082 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2083 "Expected JSON field '{field}' to be a bool"
2084 )))
2085 })
2086}
2087
2088fn json_required_str<'a>(
2089 object: &'a serde_json::Map<String, Value>,
2090 field: &str,
2091) -> SubtrActorResult<&'a str> {
2092 json_required_value(object, field)?.as_str().ok_or_else(|| {
2093 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2094 "Expected JSON field '{field}' to be a string"
2095 )))
2096 })
2097}
2098
2099fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
2100 value.and_then(Value::as_bool)
2101}
2102
2103fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
2104 match value {
2105 None | Some(Value::Null) => Ok(None),
2106 Some(value) => json_f32(value).map(Some).ok_or_else(|| {
2107 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2108 "Expected optional JSON value to be a float".to_owned(),
2109 ))
2110 }),
2111 }
2112}
2113
2114fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
2115 match value {
2116 None | Some(Value::Null) => Ok(None),
2117 Some(value) => value
2118 .as_u64()
2119 .map(|number| Some(number as usize))
2120 .ok_or_else(|| {
2121 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2122 "Expected optional JSON value to be an unsigned integer".to_owned(),
2123 ))
2124 }),
2125 }
2126}
2127
2128fn json_goal_context_position(value: &Value) -> SubtrActorResult<GoalContextPosition> {
2129 let object = json_object(value, "goal context position")?;
2130 Ok(GoalContextPosition {
2131 x: json_required_f32(object, "x")?,
2132 y: json_required_f32(object, "y")?,
2133 z: json_required_f32(object, "z")?,
2134 })
2135}
2136
2137fn json_optional_goal_context_position(
2138 value: Option<&Value>,
2139) -> SubtrActorResult<Option<GoalContextPosition>> {
2140 match value {
2141 None | Some(Value::Null) => Ok(None),
2142 Some(value) => json_goal_context_position(value).map(Some),
2143 }
2144}
2145
2146fn json_required_vec3(
2147 object: &serde_json::Map<String, Value>,
2148 field: &str,
2149) -> SubtrActorResult<[f32; 3]> {
2150 let array = json_required_value(object, field)?
2151 .as_array()
2152 .ok_or_else(|| {
2153 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2154 "Expected JSON field '{field}' to be a 3-element array"
2155 )))
2156 })?;
2157 if array.len() != 3 {
2158 return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2159 format!("Expected JSON field '{field}' to contain exactly 3 elements"),
2160 ));
2161 }
2162 Ok([
2163 json_f32(&array[0]).ok_or_else(|| {
2164 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2165 "Expected JSON field '{field}[0]' to be a float"
2166 )))
2167 })?,
2168 json_f32(&array[1]).ok_or_else(|| {
2169 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2170 "Expected JSON field '{field}[1]' to be a float"
2171 )))
2172 })?,
2173 json_f32(&array[2]).ok_or_else(|| {
2174 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
2175 "Expected JSON field '{field}[2]' to be a float"
2176 )))
2177 })?,
2178 ])
2179}
2180
2181fn json_required_remote_id(
2182 object: &serde_json::Map<String, Value>,
2183 field: &str,
2184) -> SubtrActorResult<PlayerId> {
2185 json_remote_id(json_required_value(object, field)?)
2186}
2187
2188fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
2189 match value {
2190 None | Some(Value::Null) => Ok(None),
2191 Some(value) => Ok(Some(json_remote_id(value)?)),
2192 }
2193}
2194
2195fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
2196 let object = json_object(value, "remote id")?;
2197 if object.len() != 1 {
2198 return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2199 "Expected remote id to contain exactly one variant".to_owned(),
2200 ));
2201 }
2202
2203 let (variant, payload) = object.iter().next().expect("validated single variant");
2204 match variant.as_str() {
2205 "PlayStation" => {
2206 let payload = json_object(payload, "playstation remote id")?;
2207 Ok(RemoteId::PlayStation(Ps4Id {
2208 online_id: json_u64(json_required_value(payload, "online_id")?)?,
2209 name: json_required_value(payload, "name")?
2210 .as_str()
2211 .ok_or_else(|| {
2212 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2213 "Expected PlayStation name to be a string".to_owned(),
2214 ))
2215 })?
2216 .to_owned(),
2217 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
2218 }))
2219 }
2220 "PsyNet" => {
2221 let payload = json_object(payload, "psynet remote id")?;
2222 Ok(RemoteId::PsyNet(PsyNetId {
2223 online_id: json_u64(json_required_value(payload, "online_id")?)?,
2224 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
2225 }))
2226 }
2227 "SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
2228 "Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
2229 "Switch" => {
2230 let payload = json_object(payload, "switch remote id")?;
2231 Ok(RemoteId::Switch(SwitchId {
2232 online_id: json_u64(json_required_value(payload, "online_id")?)?,
2233 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
2234 }))
2235 }
2236 "Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
2237 "QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
2238 "Epic" => Ok(RemoteId::Epic(
2239 payload
2240 .as_str()
2241 .ok_or_else(|| {
2242 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2243 "Expected Epic remote id payload to be a string".to_owned(),
2244 ))
2245 })?
2246 .to_owned(),
2247 )),
2248 variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
2249 format!("Unknown remote id variant '{variant}'"),
2250 )),
2251 }
2252}
2253
2254fn json_u64(value: &Value) -> SubtrActorResult<u64> {
2255 value
2256 .as_u64()
2257 .or_else(|| value.as_str().and_then(|text| text.parse().ok()))
2258 .ok_or_else(|| {
2259 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2260 "Expected JSON value to be a u64".to_owned(),
2261 ))
2262 })
2263}
2264
2265fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
2266 value
2267 .as_array()
2268 .ok_or_else(|| {
2269 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2270 "Expected JSON value to be an array of bytes".to_owned(),
2271 ))
2272 })?
2273 .iter()
2274 .map(|entry| {
2275 entry
2276 .as_u64()
2277 .and_then(|number| u8::try_from(number).ok())
2278 .ok_or_else(|| {
2279 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2280 "Expected JSON array entry to be a byte".to_owned(),
2281 ))
2282 })
2283 })
2284 .collect()
2285}