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